Dactyloidae iOS initial commit
3
mobile/ios/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Don't include third party files in the Github language stats!
|
||||
ThirdParty/* linguist-vendored=true
|
||||
FxA/* linguist-vendored=true
|
||||
77
mobile/ios/.gitignore
vendored
Normal file
|
|
@ -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
|
||||
92
mobile/ios/.swiftlint.yml
Normal file
|
|
@ -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)
|
||||
25
mobile/ios/AUTHORS
Normal file
|
|
@ -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
|
||||
7
mobile/ios/Account/Account-Bridging-Header.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#ifndef Client_Account_Bridging_Header_h
|
||||
#define Client_Account_Bridging_Header_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "Shared-Bridging-Header.h"
|
||||
|
||||
#endif
|
||||
425
mobile/ios/Account/FirefoxAccount.swift
Normal file
|
|
@ -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<FxAState>
|
||||
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<FxAState>?
|
||||
|
||||
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<FxAState>) {
|
||||
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<FxAState> {
|
||||
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<FxAState> = 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<Maybe<MarriedState>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
266
mobile/ios/Account/FirefoxAccountConfiguration.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
562
mobile/ios/Account/FxAClient10.swift
Normal file
|
|
@ -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 "<FxAClientError.Remote \(error.code)/\(error.errno): \(errorString) (\(messageString))>"
|
||||
case let .local(error):
|
||||
return "<FxAClientError.Local Error Domain=\(error.domain) Code=\(error.code) \"\(error.localizedDescription)\">"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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..<KeyLength)
|
||||
let respXORKey = bytes?.subdata(in: KeyLength..<(3 * KeyLength))
|
||||
|
||||
guard let hmacKey = respHMACKey,
|
||||
ciphertext.hmacSha256WithKey(hmacKey) == MAC else {
|
||||
NSLog("Bad HMAC in /keys response!")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let xorKey = respXORKey,
|
||||
let xoredBytes = ciphertext.xoredWith(xorKey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let kA = xoredBytes.subdata(in: 0..<KeyLength)
|
||||
let wrapkB = xoredBytes.subdata(in: KeyLength..<(2 * KeyLength))
|
||||
return FxAKeysResponse(kA: kA, wrapkB: wrapkB)
|
||||
}
|
||||
|
||||
fileprivate class func signResponse(fromJSON json: JSON) -> 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<Maybe<FxALoginResponse>> {
|
||||
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<Maybe<FxAStatusResponse>> {
|
||||
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<Maybe<FxADevicesResponse>> {
|
||||
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<Maybe<FxANotifyResponse>> {
|
||||
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<Maybe<FxANotifyResponse>> {
|
||||
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<Maybe<FxANotifyResponse>> {
|
||||
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<Maybe<FxADeviceDestroyResponse>> {
|
||||
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<Maybe<FxADevice>> {
|
||||
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<Maybe<FxAOAuthResponse>> {
|
||||
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<Maybe<FxAProfileResponse>> {
|
||||
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<T>(_ request: URLRequest, responseHandler: @escaping (JSON) -> T?) -> Deferred<Maybe<T>> {
|
||||
let deferred = Deferred<Maybe<T>>()
|
||||
|
||||
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<Maybe<KeyPair>> {
|
||||
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<Maybe<FxAKeysResponse>> {
|
||||
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<Maybe<FxASignResponse>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
66
mobile/ios/Account/FxADevice.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
184
mobile/ios/Account/FxADeviceRegistration.swift
Normal file
|
|
@ -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<Maybe<FxADeviceRegistrationResult>> {
|
||||
// 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<Maybe<FxADeviceRegistration>> = 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<Maybe<FxADeviceRegistration>> {
|
||||
// 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<Maybe<FxADeviceRegistration>> {
|
||||
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<Maybe<FxADeviceRegistration>> {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
182
mobile/ios/Account/FxALoginStateMachine.swift
Normal file
|
|
@ -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<Maybe<KeyPair>>
|
||||
func keys(_ keyFetchToken: Data) -> Deferred<Maybe<FxAKeysResponse>>
|
||||
func sign(_ sessionToken: Data, publicKey: PublicKey) -> Deferred<Maybe<FxASignResponse>>
|
||||
}
|
||||
|
||||
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<FxAState> {
|
||||
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<FxAState> {
|
||||
// For convenience. Without type annotation, Swift complains about types not being exact.
|
||||
let separated: Deferred<FxAState> = Deferred(value: SeparatedState())
|
||||
let doghouse: Deferred<FxAState> = Deferred(value: DoghouseState())
|
||||
let same: Deferred<FxAState> = 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)
|
||||
}
|
||||
}
|
||||
294
mobile/ios/Account/FxAPushMessageHandler.swift
Normal file
|
|
@ -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<Maybe<PushMessage>>
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
338
mobile/ios/Account/FxAState.swift
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
150
mobile/ios/Account/HawkHelper.swift
Normal file
|
|
@ -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..<KeyLength)
|
||||
let reqHMACKey = bytes.subdata(in: KeyLength..<(2 * KeyLength))
|
||||
let hawkHelper = HawkHelper(id: tokenId.hexEncodedString, key: reqHMACKey)
|
||||
let hawkValue = hawkHelper.getAuthorizationValueFor(self)
|
||||
setValue(hawkValue, forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
}
|
||||
26
mobile/ios/Account/Info.plist
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>10.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
149
mobile/ios/Account/SyncAuthState.swift
Normal file
|
|
@ -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<Maybe<(token: TokenServerToken, forKey: Data)>>
|
||||
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<SyncAuthStateCache>
|
||||
public var deviceID: String? {
|
||||
return account.deviceRegistration?.id
|
||||
}
|
||||
|
||||
init(account: FirefoxAccount, cache: KeychainCache<SyncAuthStateCache>) {
|
||||
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<Maybe<TokenServerToken>> {
|
||||
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<Maybe<(token: TokenServerToken, forKey: Data)>> {
|
||||
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!)
|
||||
}
|
||||
}
|
||||
}
|
||||
189
mobile/ios/Account/TokenServerClient.swift
Normal file
|
|
@ -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 "<TokenServerError.Remote \(code): \(status)>"
|
||||
} else {
|
||||
return "<TokenServerError.Remote \(code)>"
|
||||
}
|
||||
case let .local(error):
|
||||
return "<TokenServerError.Local Error Domain=\(error.domain) Code=\(error.code) \"\(error.localizedDescription)\">"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<Maybe<TokenServerToken>> {
|
||||
let deferred = Deferred<Maybe<TokenServerToken>>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
63
mobile/ios/AccountTests/FirefoxAccountTests.swift
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
165
mobile/ios/AccountTests/FxAClient10Tests.swift
Normal file
|
|
@ -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,
|
||||
"<FxAClientError.Local Error Domain=test Code=123 \"The operation couldn’t be completed. (test error 123.)\">")
|
||||
XCTAssertEqual(
|
||||
"\(localError)",
|
||||
"<FxAClientError.Local Error Domain=test Code=123 \"The operation couldn’t be completed. (test error 123.)\">")
|
||||
|
||||
let remoteError = FxAClientError.remote(RemoteError(code: 401, errno: 104,
|
||||
error: "error", message: "message", info: "info"))
|
||||
XCTAssertEqual(
|
||||
remoteError.description,
|
||||
"<FxAClientError.Remote 401/104: error (message)>")
|
||||
XCTAssertEqual(
|
||||
"\(remoteError)",
|
||||
"<FxAClientError.Remote 401/104: error (message)>")
|
||||
}
|
||||
|
||||
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<Maybe<FxALoginResponse>> = client.login(emailUTF8, quickStretchedPW: quickStretchedPW, getKeys: true)
|
||||
let keys: Deferred<Maybe<FxAKeysResponse>> = login.bind { (result: Maybe<FxALoginResponse>) 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<Maybe<FxALoginResponse>> = client.login(emailUTF8, quickStretchedPW: quickStretchedPW, getKeys: true)
|
||||
let sign: Deferred<Maybe<FxASignResponse>> = login.bind { (result: Maybe<FxALoginResponse>) 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
249
mobile/ios/AccountTests/FxALoginStateMachineTests.swift
Normal file
|
|
@ -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<Maybe<KeyPair>> {
|
||||
let keyPair: KeyPair = RSAKeyPair.generate(withModulusSize: 512)
|
||||
return Deferred(value: Maybe(success: keyPair))
|
||||
}
|
||||
|
||||
func keys(_ keyFetchToken: Data) -> Deferred<Maybe<FxAKeysResponse>> {
|
||||
let response = FxAKeysResponse(kA: kA, wrapkB: wrapkB)
|
||||
return Deferred(value: Maybe(success: response))
|
||||
}
|
||||
|
||||
func sign(_ sessionToken: Data, publicKey: PublicKey) -> Deferred<Maybe<FxASignResponse>> {
|
||||
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<Maybe<FxAKeysResponse>> {
|
||||
// Fail!
|
||||
return Deferred(value: Maybe(failure: FxAClientError.local(NSError(domain: NSURLErrorDomain, code: -1000, userInfo: nil))))
|
||||
}
|
||||
|
||||
override func sign(_ sessionToken: Data, publicKey: PublicKey) -> Deferred<Maybe<FxASignResponse>> {
|
||||
// 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<Maybe<FxAKeysResponse>> {
|
||||
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<Maybe<FxASignResponse>> {
|
||||
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<Maybe<FxAKeysResponse>> {
|
||||
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<Maybe<FxASignResponse>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
81
mobile/ios/AccountTests/FxAStateTests.swift
Normal file
|
|
@ -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!))
|
||||
}
|
||||
}
|
||||
90
mobile/ios/AccountTests/HawkHelperTests.swift
Normal file
|
|
@ -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!
|
||||
}
|
||||
}
|
||||
24
mobile/ios/AccountTests/Info.plist
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>10.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
158
mobile/ios/AccountTests/LiveAccountTest.swift
Normal file
|
|
@ -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<Maybe<FxALoginResponse>> = client.login(emailUTF8, quickStretchedPW: quickStretchedPW, getKeys: true)
|
||||
let sign: Deferred<Maybe<FxASignResponse>> = login.bind { (result: Maybe<FxALoginResponse>) 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<Maybe<FirefoxAccount>> {
|
||||
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<Maybe<FirefoxAccount>> {
|
||||
// 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<Maybe<SyncAuthState>> {
|
||||
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<Maybe<(token: TokenServerToken, forKey: Data)>> {
|
||||
return getAuthState(now).bind { result in
|
||||
if let authState = result.successValue {
|
||||
return authState.token(now, canBeExpired: false)
|
||||
}
|
||||
return Deferred(value: Maybe(failure: result.failureValue!))
|
||||
}
|
||||
}
|
||||
}
|
||||
32
mobile/ios/AccountTests/SyncAuthStateTests.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
112
mobile/ios/AccountTests/TokenServerClientTests.swift
Normal file
|
|
@ -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 Domain=test Code=123 \"The operation couldn’t be completed. (test error 123.)\">",
|
||||
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)
|
||||
}
|
||||
}
|
||||
18
mobile/ios/Cartfile
Normal file
|
|
@ -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"
|
||||
18
mobile/ios/Cartfile.resolved
Normal file
|
|
@ -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"
|
||||
18
mobile/ios/Client-Bridging-Header.h
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#ifndef Client_Client_Bridging_Header_h
|
||||
#define Client_Client_Bridging_Header_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CommonCrypto/CommonCrypto.h>
|
||||
|
||||
#import "FSReadingList.h"
|
||||
#import "Try.h"
|
||||
|
||||
#import "ThirdParty/UIImageViewAligned/UIImageViewAligned/UIImageViewAligned.h"
|
||||
#import "ThirdParty/Apple/UIImage+ImageEffects.h"
|
||||
|
||||
#import <BuddyBuildSDK/BuddyBuildSDK.h>
|
||||
|
||||
#import "Shared-Bridging-Header.h"
|
||||
#import "Storage-Bridging-Header.h"
|
||||
|
||||
#endif
|
||||
9468
mobile/ios/Client.xcodeproj/project.pbxproj
Normal file
7
mobile/ios/Client.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:Client.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>classNames</key>
|
||||
<dict>
|
||||
<key>TestSQLiteHistoryFrecencyPerf</key>
|
||||
<dict>
|
||||
<key>testFrecencyPerf()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.16</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string> 30 Jul 2015, Jul 30 07:02:07 </string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>runDestinationsByUUID</key>
|
||||
<dict>
|
||||
<key>FE8885C6-BB1A-48D2-9B90-E9F952C98F6B</key>
|
||||
<dict>
|
||||
<key>localComputer</key>
|
||||
<dict>
|
||||
<key>busSpeedInMHz</key>
|
||||
<integer>100</integer>
|
||||
<key>cpuCount</key>
|
||||
<integer>1</integer>
|
||||
<key>cpuKind</key>
|
||||
<string>Intel Core i7</string>
|
||||
<key>cpuSpeedInMHz</key>
|
||||
<integer>3100</integer>
|
||||
<key>logicalCPUCoresPerPackage</key>
|
||||
<integer>4</integer>
|
||||
<key>modelCode</key>
|
||||
<string>MacBookPro12,1</string>
|
||||
<key>physicalCPUCoresPerPackage</key>
|
||||
<integer>2</integer>
|
||||
<key>platformIdentifier</key>
|
||||
<string>com.apple.platform.macosx</string>
|
||||
</dict>
|
||||
<key>targetArchitecture</key>
|
||||
<string>x86_64</string>
|
||||
<key>targetDevice</key>
|
||||
<dict>
|
||||
<key>modelCode</key>
|
||||
<string>iPhone7,2</string>
|
||||
<key>platformIdentifier</key>
|
||||
<string>com.apple.platform.iphonesimulator</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>classNames</key>
|
||||
<dict>
|
||||
<key>EffectiveTLDUtilsTests</key>
|
||||
<dict>
|
||||
<key>testTLDEntriesLoadFromDiskPerformance()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.36</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string>Local Baseline</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>runDestinationsByUUID</key>
|
||||
<dict>
|
||||
<key>F9839E4F-67D2-4920-8CE8-94CEE3A6C3D7</key>
|
||||
<dict>
|
||||
<key>localComputer</key>
|
||||
<dict>
|
||||
<key>busSpeedInMHz</key>
|
||||
<integer>100</integer>
|
||||
<key>cpuCount</key>
|
||||
<integer>1</integer>
|
||||
<key>cpuKind</key>
|
||||
<string>Intel Core i7</string>
|
||||
<key>cpuSpeedInMHz</key>
|
||||
<integer>2800</integer>
|
||||
<key>logicalCPUCoresPerPackage</key>
|
||||
<integer>8</integer>
|
||||
<key>modelCode</key>
|
||||
<string>MacBookPro11,3</string>
|
||||
<key>physicalCPUCoresPerPackage</key>
|
||||
<integer>4</integer>
|
||||
<key>platformIdentifier</key>
|
||||
<string>com.apple.platform.macosx</string>
|
||||
</dict>
|
||||
<key>targetArchitecture</key>
|
||||
<string>x86_64</string>
|
||||
<key>targetDevice</key>
|
||||
<dict>
|
||||
<key>modelCode</key>
|
||||
<string>iPhone7,2</string>
|
||||
<key>platformIdentifier</key>
|
||||
<string>com.apple.platform.iphonesimulator</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FA435FA1ABB83B4008031D1"
|
||||
BuildableName = "Account.framework"
|
||||
BlueprintName = "Account"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FA436041ABB83B4008031D1"
|
||||
BuildableName = "AccountTests.xctest"
|
||||
BlueprintName = "AccountTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "SyncAuthStateTests/testLive()">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FA435FA1ABB83B4008031D1"
|
||||
BuildableName = "Account.framework"
|
||||
BlueprintName = "Account"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FA435FA1ABB83B4008031D1"
|
||||
BuildableName = "Account.framework"
|
||||
BlueprintName = "Account"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FA435FA1ABB83B4008031D1"
|
||||
BuildableName = "Account.framework"
|
||||
BlueprintName = "Account"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = "en"
|
||||
region = "US"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB07C1E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetryTests.xctest"
|
||||
BlueprintName = "SyncTelemetryTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FA436041ABB83B4008031D1"
|
||||
BuildableName = "AccountTests.xctest"
|
||||
BlueprintName = "AccountTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "SyncAuthStateTests">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21D21A090F8100AAB793"
|
||||
BuildableName = "ClientTests.xctest"
|
||||
BlueprintName = "ClientTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E6F9650B1B2F1CF20034B023"
|
||||
BuildableName = "SharedTests.xctest"
|
||||
BlueprintName = "SharedTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FCAE2231ABB51F800877008"
|
||||
BuildableName = "StorageTests.xctest"
|
||||
BlueprintName = "StorageTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "282731671ABC9BE700AA1954"
|
||||
BuildableName = "SyncTests.xctest"
|
||||
BlueprintName = "SyncTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "LiveAccountTest">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "LiveStorageClientTests">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "28F9520519D0F9FB00DCE892"
|
||||
BuildableName = "FxATests.xctest"
|
||||
BlueprintName = "FxATests"
|
||||
ReferencedContainer = "container:FxA/FxA.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3BFE4B061D342FB800DDF53F"
|
||||
BuildableName = "XCUITests.xctest"
|
||||
BlueprintName = "XCUITests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "${DEBUG_ACTIVITY_MODE}"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "DYLD_PRINT_STATISTICS"
|
||||
value = "1"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = "en"
|
||||
region = "US"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21D21A090F8100AAB793"
|
||||
BuildableName = "ClientTests.xctest"
|
||||
BlueprintName = "ClientTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4D567211ADECE2700F1EFE7"
|
||||
BuildableName = "ReadingListTests.xctest"
|
||||
BlueprintName = "ReadingListTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E6F9650B1B2F1CF20034B023"
|
||||
BuildableName = "SharedTests.xctest"
|
||||
BlueprintName = "SharedTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FCAE2231ABB51F800877008"
|
||||
BuildableName = "StorageTests.xctest"
|
||||
BlueprintName = "StorageTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "282731671ABC9BE700AA1954"
|
||||
BuildableName = "SyncTests.xctest"
|
||||
BlueprintName = "SyncTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "LiveAccountTest">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "LiveStorageClientTests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "LiveStorageClientTests/testLive()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "LiveStorageClientTests/testStateMachine()">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "28F9520519D0F9FB00DCE892"
|
||||
BuildableName = "FxATests.xctest"
|
||||
BlueprintName = "FxATests"
|
||||
ReferencedContainer = "container:FxA/FxA.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B43E3CF1D95C48D00BBA9DB"
|
||||
BuildableName = "StoragePerfTests.xctest"
|
||||
BlueprintName = "StoragePerfTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FA436041ABB83B4008031D1"
|
||||
BuildableName = "AccountTests.xctest"
|
||||
BlueprintName = "AccountTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "SyncAuthStateTests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "SyncAuthStateTests/testLive()">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB07C1E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetryTests.xctest"
|
||||
BlueprintName = "SyncTelemetryTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "${DEBUG_ACTIVITY_MODE}"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec_Enterprise">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FA436041ABB83B4008031D1"
|
||||
BuildableName = "AccountTests.xctest"
|
||||
BlueprintName = "AccountTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21D21A090F8100AAB793"
|
||||
BuildableName = "ClientTests.xctest"
|
||||
BlueprintName = "ClientTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E6F9650B1B2F1CF20034B023"
|
||||
BuildableName = "SharedTests.xctest"
|
||||
BlueprintName = "SharedTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B43E3CF1D95C48D00BBA9DB"
|
||||
BuildableName = "StoragePerfTests.xctest"
|
||||
BlueprintName = "StoragePerfTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FCAE2231ABB51F800877008"
|
||||
BuildableName = "StorageTests.xctest"
|
||||
BlueprintName = "StorageTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "282731671ABC9BE700AA1954"
|
||||
BuildableName = "SyncTests.xctest"
|
||||
BlueprintName = "SyncTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4D567211ADECE2700F1EFE7"
|
||||
BuildableName = "ReadingListTests.xctest"
|
||||
BlueprintName = "ReadingListTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3BFE4B061D342FB800DDF53F"
|
||||
BuildableName = "XCUITests.xctest"
|
||||
BlueprintName = "XCUITests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB07C1E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetryTests.xctest"
|
||||
BlueprintName = "SyncTelemetryTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = "en"
|
||||
region = "US"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Script"
|
||||
scriptText = "/usr/local/bin/carthage checkout">
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D39FA15E1A83E0EC00EE869C"
|
||||
BuildableName = "UITests.xctest"
|
||||
BlueprintName = "UITests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "BookmarkingTests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "BrowserTests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "ClearPrivateDataTests/testClearsCache()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "FindInPageTests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "HomePageSettingsUITests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "LoginManagerTests/testLoginsDetailsPromptsForPasscodeOnReentryFromBackground()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "NavigationTests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "PrivateBrowsingTests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "ReaderViewUITests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "SearchSettingsUITests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "SearchTests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "SessionRestoreTests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "SettingsTests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "TopTabsTests">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "ViewMemoryLeakTests">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "FIREFOX_CLEAR_PROFILE"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "FIREFOX_TEST"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "DYLD_INSERT_LIBRARIES"
|
||||
value = "@executable_path/EarlGrey.framework/EarlGrey"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "NO">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "${DEBUG_ACTIVITY_MODE}"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec_Enterprise">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Script"
|
||||
scriptText = "/usr/local/bin/carthage checkout">
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FA436041ABB83B4008031D1"
|
||||
BuildableName = "AccountTests.xctest"
|
||||
BlueprintName = "AccountTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21D21A090F8100AAB793"
|
||||
BuildableName = "ClientTests.xctest"
|
||||
BlueprintName = "ClientTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E6F9650B1B2F1CF20034B023"
|
||||
BuildableName = "SharedTests.xctest"
|
||||
BlueprintName = "SharedTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B43E3CF1D95C48D00BBA9DB"
|
||||
BuildableName = "StoragePerfTests.xctest"
|
||||
BlueprintName = "StoragePerfTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FCAE2231ABB51F800877008"
|
||||
BuildableName = "StorageTests.xctest"
|
||||
BlueprintName = "StorageTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "282731671ABC9BE700AA1954"
|
||||
BuildableName = "SyncTests.xctest"
|
||||
BlueprintName = "SyncTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4D567211ADECE2700F1EFE7"
|
||||
BuildableName = "ReadingListTests.xctest"
|
||||
BlueprintName = "ReadingListTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D39FA15E1A83E0EC00EE869C"
|
||||
BuildableName = "UITests.xctest"
|
||||
BlueprintName = "UITests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB07C1E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetryTests.xctest"
|
||||
BlueprintName = "SyncTelemetryTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = "en"
|
||||
region = "US"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Script"
|
||||
scriptText = "/usr/local/bin/carthage checkout">
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3BFE4B061D342FB800DDF53F"
|
||||
BuildableName = "XCUITests.xctest"
|
||||
BlueprintName = "XCUITests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "HomePageUITest">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "SearchTests/testDismissPromptPresence()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "SiteLoadTest">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "TopTabsTest/testAddPrivateTabByLongPressTabsButton()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "TopTabsTest/testAddTabByLongPressTabsButton()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "NavigationTest/testNavigationPreservesDesktopSiteOnSameHost()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "NavigationTest/testReloadPreservesMobileOrDesktopSite()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "NavigationTest/testScrollsToTopWithMultipleTabs()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "NavigationTest/testToggleBetweenMobileAndDesktopSiteFromSite()">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "FIREFOX_CLEAR_PROFILE"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "FIREFOX_TEST"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "1"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "NO">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "${DEBUG_ACTIVITY_MODE}"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec_Enterprise">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Script"
|
||||
scriptText = "/usr/local/bin/carthage checkout">
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FA436041ABB83B4008031D1"
|
||||
BuildableName = "AccountTests.xctest"
|
||||
BlueprintName = "AccountTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21D21A090F8100AAB793"
|
||||
BuildableName = "ClientTests.xctest"
|
||||
BlueprintName = "ClientTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E6F9650B1B2F1CF20034B023"
|
||||
BuildableName = "SharedTests.xctest"
|
||||
BlueprintName = "SharedTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B43E3CF1D95C48D00BBA9DB"
|
||||
BuildableName = "StoragePerfTests.xctest"
|
||||
BlueprintName = "StoragePerfTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FCAE2231ABB51F800877008"
|
||||
BuildableName = "StorageTests.xctest"
|
||||
BlueprintName = "StorageTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "282731671ABC9BE700AA1954"
|
||||
BuildableName = "SyncTests.xctest"
|
||||
BlueprintName = "SyncTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4D567211ADECE2700F1EFE7"
|
||||
BuildableName = "ReadingListTests.xctest"
|
||||
BlueprintName = "ReadingListTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D39FA15E1A83E0EC00EE869C"
|
||||
BuildableName = "UITests.xctest"
|
||||
BlueprintName = "UITests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB07C1E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetryTests.xctest"
|
||||
BlueprintName = "SyncTelemetryTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = "en"
|
||||
region = "US"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Script"
|
||||
scriptText = "/usr/local/bin/carthage checkout">
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3BFE4B061D342FB800DDF53F"
|
||||
BuildableName = "XCUITests.xctest"
|
||||
BlueprintName = "XCUITests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "HomePageUITest">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "NavigationTest/testNavigationPreservesDesktopSiteOnSameHost()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "NavigationTest/testReloadPreservesMobileOrDesktopSite()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "NavigationTest/testScrollsToTopWithMultipleTabs()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "NavigationTest/testToggleBetweenMobileAndDesktopSiteFromSite()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "SearchTests/testDismissPromptPresence()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "SiteLoadTest">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "TopTabsTest/testAddPrivateTabByLongPressTabsButton()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "TopTabsTest/testAddTabByLongPressTabsButton()">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "FIREFOX_CLEAR_PROFILE"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "FIREFOX_TEST"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "1"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "NO">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "${DEBUG_ACTIVITY_MODE}"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec_Enterprise">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec_Enterprise"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Script"
|
||||
scriptText = "/usr/local/bin/carthage checkout">
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Firefox"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = "en"
|
||||
region = "US"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FA436041ABB83B4008031D1"
|
||||
BuildableName = "AccountTests.xctest"
|
||||
BlueprintName = "AccountTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "SyncAuthStateTests">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21D21A090F8100AAB793"
|
||||
BuildableName = "ClientTests.xctest"
|
||||
BlueprintName = "ClientTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "SearchTests/testURIFixupPunyCode()">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4D567211ADECE2700F1EFE7"
|
||||
BuildableName = "ReadingListTests.xctest"
|
||||
BlueprintName = "ReadingListTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E6F9650B1B2F1CF20034B023"
|
||||
BuildableName = "SharedTests.xctest"
|
||||
BlueprintName = "SharedTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "FeatureSwitchTests">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B43E3CF1D95C48D00BBA9DB"
|
||||
BuildableName = "StoragePerfTests.xctest"
|
||||
BlueprintName = "StoragePerfTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FCAE2231ABB51F800877008"
|
||||
BuildableName = "StorageTests.xctest"
|
||||
BlueprintName = "StorageTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB07C1E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetryTests.xctest"
|
||||
BlueprintName = "SyncTelemetryTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "282731671ABC9BE700AA1954"
|
||||
BuildableName = "SyncTests.xctest"
|
||||
BlueprintName = "SyncTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "LiveStorageClientTests">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Firefox"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "${DEBUG_ACTIVITY_MODE}"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Firefox"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Firefox">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Firefox"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "NO"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "FirefoxBeta"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = "en"
|
||||
region = "US"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FA436041ABB83B4008031D1"
|
||||
BuildableName = "AccountTests.xctest"
|
||||
BlueprintName = "AccountTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "SyncAuthStateTests">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21D21A090F8100AAB793"
|
||||
BuildableName = "ClientTests.xctest"
|
||||
BlueprintName = "ClientTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "SearchTests/testURIFixupPunyCode()">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4D567211ADECE2700F1EFE7"
|
||||
BuildableName = "ReadingListTests.xctest"
|
||||
BlueprintName = "ReadingListTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E6F9650B1B2F1CF20034B023"
|
||||
BuildableName = "SharedTests.xctest"
|
||||
BlueprintName = "SharedTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "FeatureSwitchTests">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B43E3CF1D95C48D00BBA9DB"
|
||||
BuildableName = "StoragePerfTests.xctest"
|
||||
BlueprintName = "StoragePerfTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FCAE2231ABB51F800877008"
|
||||
BuildableName = "StorageTests.xctest"
|
||||
BlueprintName = "StorageTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB07C1E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetryTests.xctest"
|
||||
BlueprintName = "SyncTelemetryTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "282731671ABC9BE700AA1954"
|
||||
BuildableName = "SyncTests.xctest"
|
||||
BlueprintName = "SyncTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "LiveStorageClientTests">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "FirefoxBeta"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "${DEBUG_ACTIVITY_MODE}"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "FirefoxBeta"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "FirefoxBeta">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "FirefoxBeta"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7BEB64401C7345600092C02E"
|
||||
BuildableName = "L10nSnapshotTests.xctest"
|
||||
BlueprintName = "L10nSnapshotTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "FirefoxBetaEnterprise"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
systemAttachmentLifetime = "keepAlways"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7BEB64401C7345600092C02E"
|
||||
BuildableName = "L10nSnapshotTests.xctest"
|
||||
BlueprintName = "L10nSnapshotTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "FirefoxBetaEnterprise"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7BEB64401C7345600092C02E"
|
||||
BuildableName = "L10nSnapshotTests.xctest"
|
||||
BlueprintName = "L10nSnapshotTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "FirefoxBetaEnterprise"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7BEB64401C7345600092C02E"
|
||||
BuildableName = "L10nSnapshotTests.xctest"
|
||||
BlueprintName = "L10nSnapshotTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "FirefoxBetaEnterprise">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "FirefoxBetaEnterprise"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7BEB644F1C7345990092C02E"
|
||||
BuildableName = "MarketingUITests.xctest"
|
||||
BlueprintName = "MarketingUITests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Firefox"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
systemAttachmentLifetime = "keepAlways"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7BEB644F1C7345990092C02E"
|
||||
BuildableName = "MarketingUITests.xctest"
|
||||
BlueprintName = "MarketingUITests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Firefox"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7BEB644F1C7345990092C02E"
|
||||
BuildableName = "MarketingUITests.xctest"
|
||||
BlueprintName = "MarketingUITests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Firefox"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7BEB644F1C7345990092C02E"
|
||||
BuildableName = "MarketingUITests.xctest"
|
||||
BlueprintName = "MarketingUITests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Firefox">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Firefox"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4D567171ADECE2700F1EFE7"
|
||||
BuildableName = "ReadingList.framework"
|
||||
BlueprintName = "ReadingList"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4D567211ADECE2700F1EFE7"
|
||||
BuildableName = "ReadingListTests.xctest"
|
||||
BlueprintName = "ReadingListTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4D567211ADECE2700F1EFE7"
|
||||
BuildableName = "ReadingListTests.xctest"
|
||||
BlueprintName = "ReadingListTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4D567171ADECE2700F1EFE7"
|
||||
BuildableName = "ReadingList.framework"
|
||||
BlueprintName = "ReadingList"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4D567171ADECE2700F1EFE7"
|
||||
BuildableName = "ReadingList.framework"
|
||||
BlueprintName = "ReadingList"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4D567171ADECE2700F1EFE7"
|
||||
BuildableName = "ReadingList.framework"
|
||||
BlueprintName = "ReadingList"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B225B1A09210A00AAB793"
|
||||
BuildableName = "SendTo.appex"
|
||||
BlueprintName = "SendTo"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B225B1A09210A00AAB793"
|
||||
BuildableName = "SendTo.appex"
|
||||
BlueprintName = "SendTo"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "${DEBUG_ACTIVITY_MODE}"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B225B1A09210A00AAB793"
|
||||
BuildableName = "SendTo.appex"
|
||||
BlueprintName = "SendTo"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B22481A0920C600AAB793"
|
||||
BuildableName = "ShareTo.appex"
|
||||
BlueprintName = "ShareTo"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B22481A0920C600AAB793"
|
||||
BuildableName = "ShareTo.appex"
|
||||
BlueprintName = "ShareTo"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "${DEBUG_ACTIVITY_MODE}"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B22481A0920C600AAB793"
|
||||
BuildableName = "ShareTo.appex"
|
||||
BlueprintName = "ShareTo"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "288A2D851AB8B3260023ABC3"
|
||||
BuildableName = "Shared.framework"
|
||||
BlueprintName = "Shared"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E6F9650B1B2F1CF20034B023"
|
||||
BuildableName = "SharedTests.xctest"
|
||||
BlueprintName = "SharedTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "288A2D851AB8B3260023ABC3"
|
||||
BuildableName = "Shared.framework"
|
||||
BlueprintName = "Shared"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "288A2D851AB8B3260023ABC3"
|
||||
BuildableName = "Shared.framework"
|
||||
BlueprintName = "Shared"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "288A2D851AB8B3260023ABC3"
|
||||
BuildableName = "Shared.framework"
|
||||
BlueprintName = "Shared"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FCAE2191ABB51F800877008"
|
||||
BuildableName = "Storage.framework"
|
||||
BlueprintName = "Storage"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FCAE2231ABB51F800877008"
|
||||
BuildableName = "StorageTests.xctest"
|
||||
BlueprintName = "StorageTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B43E3CF1D95C48D00BBA9DB"
|
||||
BuildableName = "StoragePerfTests.xctest"
|
||||
BlueprintName = "StoragePerfTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FCAE2191ABB51F800877008"
|
||||
BuildableName = "Storage.framework"
|
||||
BlueprintName = "Storage"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FCAE2191ABB51F800877008"
|
||||
BuildableName = "Storage.framework"
|
||||
BlueprintName = "Storage"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2FCAE2191ABB51F800877008"
|
||||
BuildableName = "Storage.framework"
|
||||
BlueprintName = "Storage"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
121
mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Sync.xcscheme
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2827315D1ABC9BE600AA1954"
|
||||
BuildableName = "Sync.framework"
|
||||
BlueprintName = "Sync"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "282731671ABC9BE700AA1954"
|
||||
BuildableName = "SyncTests.xctest"
|
||||
BlueprintName = "SyncTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "282731671ABC9BE700AA1954"
|
||||
BuildableName = "SyncTests.xctest"
|
||||
BlueprintName = "SyncTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "LiveAccountTest">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "LiveStorageClientTests">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2827315D1ABC9BE600AA1954"
|
||||
BuildableName = "Sync.framework"
|
||||
BlueprintName = "Sync"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2827315D1ABC9BE600AA1954"
|
||||
BuildableName = "Sync.framework"
|
||||
BlueprintName = "Sync"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2827315D1ABC9BE600AA1954"
|
||||
BuildableName = "Sync.framework"
|
||||
BlueprintName = "Sync"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0830"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB0741E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetry.framework"
|
||||
BlueprintName = "SyncTelemetry"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB07C1E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetryTests.xctest"
|
||||
BlueprintName = "SyncTelemetryTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB0741E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetry.framework"
|
||||
BlueprintName = "SyncTelemetry"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB0741E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetry.framework"
|
||||
BlueprintName = "SyncTelemetry"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB0741E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetry.framework"
|
||||
BlueprintName = "SyncTelemetry"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "390527491C874D35007E0BB7"
|
||||
BuildableName = "Today.appex"
|
||||
BlueprintName = "Today"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E69DB07C1E97DEA9008A67E6"
|
||||
BuildableName = "SyncTelemetryTests.xctest"
|
||||
BlueprintName = "SyncTelemetryTests"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "${DEBUG_ACTIVITY_MODE}"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4BA8A2A1B4B0A1600BC2E95"
|
||||
BuildableName = "ViewLater.appex"
|
||||
BlueprintName = "ViewLater"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F84B21BD1A090F8100AAB793"
|
||||
BuildableName = "Client.app"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Fennec"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4BA8A2A1B4B0A1600BC2E95"
|
||||
BuildableName = "ViewLater.appex"
|
||||
BlueprintName = "ViewLater"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "${DEBUG_ACTIVITY_MODE}"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Fennec"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E4BA8A2A1B4B0A1600BC2E95"
|
||||
BuildableName = "ViewLater.appex"
|
||||
BlueprintName = "ViewLater"
|
||||
ReferencedContainer = "container:Client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Fennec">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Fennec"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
195
mobile/ios/Client/Application/AdjustIntegration.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
884
mobile/ios/Client/Application/AppDelegate.swift
Normal file
|
|
@ -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=<token>&someQuery=<data>...
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
mobile/ios/Client/Application/Crasher.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// Crasher.h
|
||||
// Client
|
||||
//
|
||||
// Created by Steph Leroux on 2015-10-08.
|
||||
// Copyright © 2015 Mozilla. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface Crasher : NSObject
|
||||
|
||||
@end
|
||||
13
mobile/ios/Client/Application/Crasher.m
Normal file
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
34
mobile/ios/Client/Application/LaunchScreen.xib
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="7706" systemVersion="14E26a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="splash" translatesAutoresizingMaskIntoConstraints="NO" id="8jg-1f-0sB">
|
||||
<rect key="frame" x="175" y="175" width="130" height="130"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="130" id="V8U-lj-86g"/>
|
||||
<constraint firstAttribute="height" constant="130" id="eV3-E9-QZk"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstItem="8jg-1f-0sB" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="ITK-hU-DwR"/>
|
||||
<constraint firstItem="8jg-1f-0sB" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="grm-jt-xY2"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="404" y="445"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="splash" width="400" height="390"/>
|
||||
</resources>
|
||||
</document>
|
||||
320
mobile/ios/Client/Application/LeanplumIntegration.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
142
mobile/ios/Client/Application/QuickActions.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
57
mobile/ios/Client/Application/Settings.bundle/Root.plist
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>StringsTable</key>
|
||||
<string>Root</string>
|
||||
<key>PreferenceSpecifiers</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Debug Settings</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSToggleSwitchSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Email Saved Tab State</string>
|
||||
<key>Key</key>
|
||||
<string>SettingsBundleEmailTabState</string>
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSToggleSwitchSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Email Logs On Launch</string>
|
||||
<key>Key</key>
|
||||
<string>SettingsBundleEmailLogsOnLaunch</string>
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSToggleSwitchSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Disable Local Web Server</string>
|
||||
<key>Key</key>
|
||||
<string>SettingsBundleDisableLocalWebServer</string>
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSToggleSwitchSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Skip Session Restore</string>
|
||||
<key>Key</key>
|
||||
<string>SettingsBundleSkipSessionRestore</string>
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
96
mobile/ios/Client/Application/TestAppDelegate.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
88
mobile/ios/Client/Application/WebServer.swift
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
16
mobile/ios/Client/Application/main.swift
Normal file
|
|
@ -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<Int8>.self, capacity: Int(CommandLine.argc))
|
||||
UIApplicationMain(CommandLine.argc, pointer, NSStringFromClass(UIApplication.self), NSStringFromClass(appDelegate))
|
||||
674
mobile/ios/Client/Assets/About/Licenses.html
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: sans-serif;
|
||||
src: url('/reader-mode/fonts/FiraSans-Regular.ttf');
|
||||
}
|
||||
body,p,h1,h2,h3 {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
h2 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.link {
|
||||
margin: 0;
|
||||
text-size: 55%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<h1>Licenses</h1>
|
||||
|
||||
<h2>Firefox for iOS</h2>
|
||||
<p class="link"><a href="https://github.com/mozilla/firefox-ios">github.com/mozilla/firefox-ios</a></p>
|
||||
<div class="text">
|
||||
|
||||
|
||||
|
||||
|
||||
<h3 id="mozilla-public-license-version-2.0">Mozilla Public License<br>Version 2.0</h3>
|
||||
<h4 id="definitions">1. Definitions</h4>
|
||||
<dl>
|
||||
<dt>1.1. “Contributor”</dt>
|
||||
<dd><p>means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software.</p>
|
||||
</dd>
|
||||
<dt>1.2. “Contributor Version”</dt>
|
||||
<dd><p>means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution.</p>
|
||||
</dd>
|
||||
<dt>1.3. “Contribution”</dt>
|
||||
<dd><p>means Covered Software of a particular Contributor.</p>
|
||||
</dd>
|
||||
<dt>1.4. “Covered Software”</dt>
|
||||
<dd><p>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.</p>
|
||||
</dd>
|
||||
<dt>1.5. “Incompatible With Secondary Licenses”</dt>
|
||||
<dd><p>means</p>
|
||||
<ol type="a">
|
||||
<li><p>that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or</p></li>
|
||||
<li><p>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.</p></li>
|
||||
</ol>
|
||||
</dd>
|
||||
<dt>1.6. “Executable Form”</dt>
|
||||
<dd><p>means any form of the work other than Source Code Form.</p>
|
||||
</dd>
|
||||
<dt>1.7. “Larger Work”</dt>
|
||||
<dd><p>means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software.</p>
|
||||
</dd>
|
||||
<dt>1.8. “License”</dt>
|
||||
<dd><p>means this document.</p>
|
||||
</dd>
|
||||
<dt>1.9. “Licensable”</dt>
|
||||
<dd><p>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.</p>
|
||||
</dd>
|
||||
<dt>1.10. “Modifications”</dt>
|
||||
<dd><p>means any of the following:</p>
|
||||
<ol type="a">
|
||||
<li><p>any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or</p></li>
|
||||
<li><p>any new file in Source Code Form that contains any Covered Software.</p></li>
|
||||
</ol>
|
||||
</dd>
|
||||
<dt>1.11. “Patent Claims” of a Contributor</dt>
|
||||
<dd><p>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.</p>
|
||||
</dd>
|
||||
<dt>1.12. “Secondary License”</dt>
|
||||
<dd><p>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.</p>
|
||||
</dd>
|
||||
<dt>1.13. “Source Code Form”</dt>
|
||||
<dd><p>means the form of the work preferred for making modifications.</p>
|
||||
</dd>
|
||||
<dt>1.14. “You” (or “Your”)</dt>
|
||||
<dd><p>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.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
<h4 id="license-grants-and-conditions">2. License Grants and Conditions</h4>
|
||||
<h3 id="grants">2.1. Grants</h3>
|
||||
<p>Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:</p>
|
||||
<ol type="a">
|
||||
<li><p>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</p></li>
|
||||
<li><p>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.</p></li>
|
||||
</ol>
|
||||
<h3 id="effective-date">2.2. Effective Date</h3>
|
||||
<p>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.</p>
|
||||
<h3 id="limitations-on-grant-scope">2.3. Limitations on Grant Scope</h3>
|
||||
<p>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:</p>
|
||||
<ol type="a">
|
||||
<li><p>for any code that a Contributor has removed from Covered Software; or</p></li>
|
||||
<li><p>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</p></li>
|
||||
<li><p>under Patent Claims infringed by Covered Software in the absence of its Contributions.</p></li>
|
||||
</ol>
|
||||
<p>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).</p>
|
||||
<h3 id="subsequent-licenses">2.4. Subsequent Licenses</h3>
|
||||
<p>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).</p>
|
||||
<h3 id="representation">2.5. Representation</h3>
|
||||
<p>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.</p>
|
||||
<h3 id="fair-use">2.6. Fair Use</h3>
|
||||
<p>This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents.</p>
|
||||
<h3 id="conditions">2.7. Conditions</h3>
|
||||
<p>Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1.</p>
|
||||
<h4 id="responsibilities">3. Responsibilities</h4>
|
||||
<h3 id="distribution-of-source-form">3.1. Distribution of Source Form</h3>
|
||||
<p>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.</p>
|
||||
<h3 id="distribution-of-executable-form">3.2. Distribution of Executable Form</h3>
|
||||
<p>If You distribute Covered Software in Executable Form then:</p>
|
||||
<ol type="a">
|
||||
<li><p>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</p></li>
|
||||
<li><p>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.</p></li>
|
||||
</ol>
|
||||
<h3 id="distribution-of-a-larger-work">3.3. Distribution of a Larger Work</h3>
|
||||
<p>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).</p>
|
||||
<h3 id="notices">3.4. Notices</h3>
|
||||
<p>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.</p>
|
||||
<h3 id="application-of-additional-terms">3.5. Application of Additional Terms</h3>
|
||||
<p>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.</p>
|
||||
<h4 id="inability-to-comply-due-to-statute-or-regulation">4. Inability to Comply Due to Statute or Regulation</h4>
|
||||
<p>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.</p>
|
||||
<h4 id="termination">5. Termination</h4>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h4 id="disclaimer-of-warranty">6. Disclaimer of Warranty</h4>
|
||||
<p><em>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.</em></p>
|
||||
<h4 id="limitation-of-liability">7. Limitation of Liability</h4>
|
||||
<p><em>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.</em></p>
|
||||
<h4 id="litigation">8. Litigation</h4>
|
||||
<p>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.</p>
|
||||
<h4 id="miscellaneous">9. Miscellaneous</h4>
|
||||
<p>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.</p>
|
||||
<h4 id="versions-of-the-license">10. Versions of the License</h4>
|
||||
<h3 id="new-versions">10.1. New Versions</h3>
|
||||
<p>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.</p>
|
||||
<h3 id="effect-of-new-versions">10.2. Effect of New Versions</h3>
|
||||
<p>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.</p>
|
||||
<h3 id="modified-versions">10.3. Modified Versions</h3>
|
||||
<p>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).</p>
|
||||
<h3 id="distributing-source-code-form-that-is-incompatible-with-secondary-licenses">10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses</h3>
|
||||
<p>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.</p>
|
||||
<h4 id="exhibit-a---source-code-form-license-notice">Exhibit A - Source Code Form License Notice</h4>
|
||||
<blockquote>
|
||||
<p>This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.</p>
|
||||
</blockquote>
|
||||
<p>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.</p>
|
||||
<p>You may add additional accurate notices of copyright ownership.</p>
|
||||
<h4 id="exhibit-b---incompatible-with-secondary-licenses-notice">Exhibit B - “Incompatible With Secondary Licenses” Notice</h4>
|
||||
<blockquote>
|
||||
<p>This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0.</p>
|
||||
</blockquote>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<center><p>-</p></center>
|
||||
|
||||
<!-- -->
|
||||
|
||||
<h2>Alamofire</h2>
|
||||
<p class="link"><a href="http://alamofire.org">alamofire.org</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2014 Alamofire (http://alamofire.org/)</p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
<!-- -->
|
||||
|
||||
<h2>Base32</h2>
|
||||
<p class="link"><a href="https://github.com/norio-nomura/Base32">github.com/norio-nomura/Base32</a></p>
|
||||
<div class="text">
|
||||
<p>The MIT License (MIT)</p>
|
||||
|
||||
<p>Copyright (c) 2015 Norio Nomura</p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
<h2>Box</h2>
|
||||
<p class="link"><a href="https://github.com/robrix/Box">github.com/robrix/Box</a></p>
|
||||
<div class="text">
|
||||
<p>The MIT License (MIT)</p>
|
||||
|
||||
<p>Copyright (c) 2014 Rob Rix</p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
<h2>Deferred</h2>
|
||||
<p class="link"><a href="https://github.com/bignerdranch/Deferred">github.com/bignerdranch/Deferred</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2014 John Gallagher <jgallagher@bignerdranch.com>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
|
||||
<h2>FilledPageControl</h2>
|
||||
<p>Copyright (c) 2016 Kyle Zaragoza <popwarsweet@gmail.com></p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
|
||||
<h2>GCDWebServer</h2>
|
||||
<p class="link"><a href="https://github.com/swisspol/GCDWebServer">github.com/swisspol/GCDWebServer</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2012-2014, Pierre-Olivier Latour</p>
|
||||
<p>All rights reserved.</p>
|
||||
|
||||
<p>Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:</p>
|
||||
|
||||
<ul>
|
||||
<li>Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.</li>
|
||||
<li>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.</li>
|
||||
<li>The name of Pierre-Olivier Latour may not be used to endorse
|
||||
or promote products derived from this software without specific
|
||||
prior written permission.</li>
|
||||
</ul>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>KIF</h2>
|
||||
<p class="link"><a href="https://github.com/kif-framework/KIF">https://github.com/kif-framework/KIF</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright 2011 Square, Inc.</p>
|
||||
<p>A full list of contributors is available at https://github.com/square/KIF/contributors</p>
|
||||
|
||||
<p>Licensed under the Apache License, Version 2.0 (the "License");</p>
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at</p>
|
||||
|
||||
<p>http://www.apache.org/licenses/LICENSE-2.0</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
|
||||
<h2>Result</h2>
|
||||
<p class="link"><a href="https://github.com/bignerdranch/Result">github.com/bignerdranch/Result</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2014 John Gallagher <jgallagher@bignerdranch.com></p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
|
||||
<h2>SDWebImage</h2>
|
||||
<p class="link"><a href="https://github.com/rs/SDWebImage">github.com/rs/SDWebImage</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2009 Olivier Poitrey <rs@dailymotion.com></p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>SQLite.swift</h2>
|
||||
<p class="link"><a href="https://github.com/stephencelis/SQLite.swift">github.com/stephencelis/SQLite.swift</a></p>
|
||||
<div class="text">
|
||||
<p>(The MIT License)</p>
|
||||
|
||||
<p>Copyright (c) 2014-2015 Stephen Celis (<stephen@stephencelis.com>)</p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>Fuzi</h2>
|
||||
<p class="link"><a href="https://github.com/cezheng/Fuzi">github.com/cezheng/Fuzi</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2015 Ce Zheng</p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>Snap</h2>
|
||||
<p class="link"><a href="https://github.com/SnapKit">github.com/SnapKit</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit</p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>XCGLogger</h2>
|
||||
<p class="link"><a href="https://github.com/DaveWoodCom/XCGLogger">github.com/DaveWoodCom/XCGLogger</a></p>
|
||||
<div class="text">
|
||||
<p>The MIT License (MIT)</p>
|
||||
|
||||
<p>Copyright (c) 2014 Dave Wood, Cerebral Gardens http://www.cerebralgardens.com/</p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>Readability</h2>
|
||||
<p class="link"><a href="https://github.com/mozilla/readability">github.com/mozilla/readability</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2010 Arc90 Inc</p>
|
||||
|
||||
<p>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</p>
|
||||
|
||||
<p>http://www.apache.org/licenses/LICENSE-2.0</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>sqlchipher</h2>
|
||||
<p class="link"><a href="https://github.com/sqlcipher/sqlcipher">github.com/sqlcipher/sqlcipher</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2008, ZETETIC LLC</p>
|
||||
<p>All rights reserved.</p>
|
||||
|
||||
<p>Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:</p>
|
||||
|
||||
<ul>
|
||||
<li>Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.</li>
|
||||
<li>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.</li>
|
||||
<li>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.</li>
|
||||
</ul>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>SwiftKeychainWrapper</h2>
|
||||
<p class="link"><a href="https://github.com/jrendel/SwiftKeychainWrapper">github.com/jrendel/SwiftKeychainWrapper</a></p>
|
||||
<div class="text">
|
||||
<p>The MIT License (MIT)</p>
|
||||
|
||||
<p>Copyright (c) 2014 Jason</p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>Swift-JSON</h2>
|
||||
<p class="link"><a href="https://github.com/dankogai/swift-json">https://github.com/dankogai/swift-json</a></p>
|
||||
<div class="text">
|
||||
<p>The MIT License (MIT)</p>
|
||||
|
||||
<p>Copyright (c) 2014 Dan Kogai</p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>UIImageColors</h2>
|
||||
<p class="link"><a href="https://github.com/jathu/UIImageColors">github.com/jathu/UIImageColors</a></p>
|
||||
|
||||
<p>Created by Jathu Satkunarajah (@jathu) on 2015-06-11 - Toronto</p>
|
||||
<p>Original Cocoa version by Panic Inc. - Portland</p>
|
||||
|
||||
<p>MIT License</p>
|
||||
|
||||
<p>Copyright (c) 2015 Jathu Satkunarajah</p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
|
||||
<h2>UIImageViewAligned</h2>
|
||||
<p class="link"><a href="https://github.com/reydanro/UIImageViewAligned">github.com/reydanro/UIImageViewAligned</a></p>
|
||||
<div class="text">
|
||||
<p>The MIT License (MIT)</p>
|
||||
|
||||
<p>Copyright (c) 2013 Andrei Stanescu</p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
76
mobile/ios/Client/Assets/CertError.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
49
mobile/ios/Client/Assets/CertError.html
Normal file
|
|
@ -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/. -->
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>%error_title%</title>
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<link rel="stylesheet" href="CertError.css" type="text/css" media="all" />
|
||||
|
||||
<script type="application/javascript">
|
||||
// If the user swipes back and forth to this tab, we want to make sure we make an attempt to reload the original page.
|
||||
var fresh = true;
|
||||
window.addEventListener('popstate', function () {
|
||||
if (fresh)
|
||||
fresh = false;
|
||||
else
|
||||
webkit.messageHandlers.localRequestHelper.postMessage({ type: "reload" });
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="errorPageContainer">
|
||||
<h1 id="errorTitleText">%error_title%</h1>
|
||||
<p id="actions">%actions%</p>
|
||||
<p>%long_description%</p>
|
||||
<button id="advancedButton">%advanced_button%</button>
|
||||
|
||||
<div id="advancedContent">
|
||||
<p id="certErrorCode">%cert_error%</p>
|
||||
<p>%warning_advanced1%</p>
|
||||
<p>%warning_advanced2%</p>
|
||||
<div>%warning_actions%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="decoration"></div>
|
||||
|
||||
<script>
|
||||
var advancedContent = document.getElementById("advancedContent");
|
||||
advancedContent.style.display = "none";
|
||||
document.getElementById("advancedButton").onclick = function () {
|
||||
this.style.display = "none";
|
||||
advancedContent.style.display = "block";
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
28
mobile/ios/Client/Assets/ContextMenu.js
Normal file
|
|
@ -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);
|
||||
42
mobile/ios/Client/Assets/CustomSearchHelper.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
60
mobile/ios/Client/Assets/Favicons.js
Normal file
|
|
@ -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
|
||||
};
|
||||
})()
|
||||
});
|
||||
292
mobile/ios/Client/Assets/FindInPage.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
BIN
mobile/ios/Client/Assets/Fonts/CharisSILB.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/CharisSILBI.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/CharisSILI.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/CharisSILR.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-Bold.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-BoldItalic.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-Book.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-Italic.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-Light.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-Medium.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-Regular.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-SemiBold.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-UltraLight.ttf
Normal file
15
mobile/ios/Client/Assets/FxASignIn.js
Normal file
|
|
@ -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);
|
||||
36
mobile/ios/Client/Assets/HistoryStateHelper.js
Normal file
|
|
@ -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
|
||||
});
|
||||
};
|
||||
})();
|
||||
BIN
mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch.png
vendored
Normal file
|
After Width: | Height: | Size: 449 B |
BIN
mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 831 B |
BIN
mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
23
mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/Contents.json
vendored
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |