mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-09 09:18:42 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
41
mobile/ios/Shared/Accessibility.swift
Normal file
41
mobile/ios/Shared/Accessibility.swift
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
|
||||
public protocol AccessibilityActionsSource: class {
|
||||
func accessibilityCustomActionsForView(_ view: UIView) -> [UIAccessibilityCustomAction]?
|
||||
}
|
||||
|
||||
open class AccessibleAction {
|
||||
open let name: String
|
||||
open let handler: () -> Bool
|
||||
|
||||
public init(name: String, handler: @escaping () -> Bool) {
|
||||
self.name = name
|
||||
self.handler = handler
|
||||
}
|
||||
}
|
||||
|
||||
extension AccessibleAction { // UIAccessibilityCustomAction
|
||||
@objc private func SELperformAccessibilityAction() -> Bool {
|
||||
return handler()
|
||||
}
|
||||
|
||||
public var accessibilityCustomAction: UIAccessibilityCustomAction {
|
||||
return UIAccessibilityCustomAction(name: name, target: self, selector: #selector(AccessibleAction.SELperformAccessibilityAction))
|
||||
}
|
||||
}
|
||||
|
||||
extension AccessibleAction { // UIAlertAction
|
||||
private var alertActionHandler: (UIAlertAction!) -> Void {
|
||||
return { (_: UIAlertAction!) -> Void in
|
||||
_ = self.handler()
|
||||
}
|
||||
}
|
||||
|
||||
public func alertAction(style: UIAlertActionStyle) -> UIAlertAction {
|
||||
return UIAlertAction(title: name, style: style, handler: alertActionHandler)
|
||||
}
|
||||
}
|
||||
230
mobile/ios/Shared/AppConstants.swift
Normal file
230
mobile/ios/Shared/AppConstants.swift
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
|
||||
public enum AppBuildChannel: String {
|
||||
case release = "release"
|
||||
case beta = "beta"
|
||||
case developer = "developer"
|
||||
}
|
||||
|
||||
public enum KVOConstants: String {
|
||||
case loading = "loading"
|
||||
case estimatedProgress = "estimatedProgress"
|
||||
case URL = "URL"
|
||||
case title = "title"
|
||||
case canGoBack = "canGoBack"
|
||||
case canGoForward = "canGoForward"
|
||||
case contentSize = "contentSize"
|
||||
}
|
||||
|
||||
public struct AppConstants {
|
||||
public static let IsRunningTest = NSClassFromString("XCTestCase") != nil || ProcessInfo.processInfo.arguments.contains(LaunchArguments.Test)
|
||||
|
||||
public static let FxAiOSClientId = "1b1a3e44c54fbb58"
|
||||
|
||||
/// Build Channel.
|
||||
public static let BuildChannel: AppBuildChannel = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return AppBuildChannel.release
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return AppBuildChannel.beta
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return AppBuildChannel.developer
|
||||
#endif
|
||||
}()
|
||||
|
||||
public static let scheme: String = {
|
||||
guard let identifier = Bundle.main.bundleIdentifier else {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
let scheme = identifier.replacingOccurrences(of: "org.mozilla.ios.", with: "")
|
||||
if scheme == "FirefoxNightly.enterprise" {
|
||||
return "FirefoxNightly"
|
||||
}
|
||||
return scheme
|
||||
}()
|
||||
|
||||
public static let PrefSendUsageData = "settings.sendUsageData"
|
||||
|
||||
/// Whether we just mirror (false) or actively do a full bookmark merge and upload (true).
|
||||
public static var shouldMergeBookmarks = false
|
||||
|
||||
/// Should we try to sync (no merging) the Mobile Folder (if shouldMergeBookmarks is false).
|
||||
public static let MOZ_SIMPLE_BOOKMARKS_SYNCING: Bool = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Should we send a repair request to other clients when the bookmarks buffer validation fails.
|
||||
public static let MOZ_BOOKMARKS_REPAIR_REQUEST: Bool = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return false
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Flag indicating if we are running in Debug mode or not.
|
||||
public static let isDebug: Bool = {
|
||||
#if MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Enables support for International Domain Names (IDN)
|
||||
/// Disabled because of https://bugzilla.mozilla.org/show_bug.cgi?id=1312294
|
||||
public static let MOZ_PUNYCODE: Bool = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return false
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return false
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Enables/disables deep linking form fill for FxA
|
||||
public static let MOZ_FXA_DEEP_LINK_FORM_FILL: Bool = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Toggles reporting our ad-hoc bookmark sync ping
|
||||
public static let MOZ_ADHOC_SYNC_REPORTING: Bool = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return false
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return false
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Toggles the ability to add a custom search engine
|
||||
public static let MOZ_CUSTOM_SEARCH_ENGINE: Bool = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Enables/disables push notificatuibs for FxA
|
||||
public static let MOZ_FXA_PUSH: Bool = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Toggle the feature that shows the blue 'Open copied link' banner
|
||||
public static let MOZ_CLIPBOARD_BAR: Bool = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Toggle pocket stories feature
|
||||
public static let MOZ_POCKET_STORIES: Bool = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Toggle the use of Leanplum.
|
||||
public static let MOZ_ENABLE_LEANPLUM: Bool = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Toggle the feature that shows updated FxA preferences cell
|
||||
public static let MOZ_SHOW_FXA_AVATAR: Bool = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// The maximum length of a URL stored by Firefox. Shared with Places on desktop.
|
||||
public static let DB_URL_LENGTH_MAX = 65536
|
||||
|
||||
/// The maximum length of a page title stored by Firefox. Shared with Places on desktop.
|
||||
public static let DB_TITLE_LENGTH_MAX = 4096
|
||||
|
||||
/// The maximum length of a bookmark description stored by Firefox. Shared with Places on desktop.
|
||||
public static let DB_DESCRIPTION_LENGTH_MAX = 1024
|
||||
|
||||
/// Toggle FxA Leanplum A/B test for prompting push permissions
|
||||
public static let MOZ_FXA_LEANPLUM_AB_PUSH_TEST: Bool = {
|
||||
#if MOZ_CHANNEL_RELEASE
|
||||
return false
|
||||
#elseif MOZ_CHANNEL_BETA
|
||||
return true
|
||||
#elseif MOZ_CHANNEL_FENNEC
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}()
|
||||
}
|
||||
86
mobile/ios/Shared/AppInfo.swift
Normal file
86
mobile/ios/Shared/AppInfo.swift
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
open class AppInfo {
|
||||
/// Return the main application bundle. If this is called from an extension, the containing app bundle is returned.
|
||||
open static var applicationBundle: Bundle {
|
||||
let bundle = Bundle.main
|
||||
switch bundle.bundleURL.pathExtension {
|
||||
case "app":
|
||||
return bundle
|
||||
case "appex":
|
||||
// .../Client.app/PlugIns/SendTo.appex
|
||||
return Bundle(url: bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent())!
|
||||
default:
|
||||
fatalError("Unable to get application Bundle (Bundle.main.bundlePath=\(bundle.bundlePath))")
|
||||
}
|
||||
}
|
||||
|
||||
open static var displayName: String {
|
||||
return applicationBundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as! String
|
||||
}
|
||||
|
||||
open static var appVersion: String {
|
||||
return applicationBundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
|
||||
}
|
||||
|
||||
open static var buildNumber: String {
|
||||
return applicationBundle.object(forInfoDictionaryKey: String(kCFBundleVersionKey)) as! String
|
||||
}
|
||||
|
||||
open static var majorAppVersion: String {
|
||||
return appVersion.components(separatedBy: ".").first!
|
||||
}
|
||||
|
||||
/// Return the shared container identifier (also known as the app group) to be used with for example background
|
||||
/// http requests. It is the base bundle identifier with a "group." prefix.
|
||||
open static var sharedContainerIdentifier: String {
|
||||
var bundleIdentifier = baseBundleIdentifier
|
||||
if bundleIdentifier == "org.mozilla.ios.FennecEnterprise" {
|
||||
// Bug 1373726 - Base bundle identifier incorrectly generated for Nightly builds
|
||||
// This can be removed when we are able to fix the app group in the developer portal
|
||||
bundleIdentifier = "org.mozilla.ios.Fennec.enterprise"
|
||||
}
|
||||
return "group." + bundleIdentifier
|
||||
}
|
||||
|
||||
/// Return the keychain access group.
|
||||
open static func keychainAccessGroupWithPrefix(_ prefix: String) -> String {
|
||||
var bundleIdentifier = baseBundleIdentifier
|
||||
if bundleIdentifier == "org.mozilla.ios.FennecEnterprise" {
|
||||
// Bug 1373726 - Base bundle identifier incorrectly generated for Nightly builds
|
||||
// This can be removed when we are able to fix the app group in the developer portal
|
||||
bundleIdentifier = "org.mozilla.ios.Fennec.enterprise"
|
||||
}
|
||||
return prefix + "." + bundleIdentifier
|
||||
}
|
||||
|
||||
/// Return the base bundle identifier.
|
||||
///
|
||||
/// This function is smart enough to find out if it is being called from an extension or the main application. In
|
||||
/// case of the former, it will chop off the extension identifier from the bundle since that is a suffix not part
|
||||
/// of the *base* bundle identifier.
|
||||
open static var baseBundleIdentifier: String {
|
||||
let bundle = Bundle.main
|
||||
let packageType = bundle.object(forInfoDictionaryKey: "CFBundlePackageType") as! String
|
||||
let baseBundleIdentifier = bundle.bundleIdentifier!
|
||||
if packageType == "XPC!" {
|
||||
let components = baseBundleIdentifier.components(separatedBy: ".")
|
||||
return components[0..<components.count-1].joined(separator: ".")
|
||||
}
|
||||
return baseBundleIdentifier
|
||||
}
|
||||
|
||||
// Return the MozWhatsNewTopic key from the Info.plist
|
||||
open static var whatsNewTopic: String? {
|
||||
return Bundle.main.object(forInfoDictionaryKey: "MozWhatsNewTopic") as? String
|
||||
}
|
||||
|
||||
// Return whether the currently executing code is running in an Application
|
||||
open static var isApplication: Bool {
|
||||
return Bundle.main.object(forInfoDictionaryKey: "CFBundlePackageType") as! String == "APPL"
|
||||
}
|
||||
}
|
||||
14
mobile/ios/Shared/AssertionUtils.swift
Normal file
14
mobile/ios/Shared/AssertionUtils.swift
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Assertion for checking that the call is being made on the main thread.
|
||||
|
||||
- parameter message: Message to display in case of assertion.
|
||||
*/
|
||||
public func assertIsMainThread(_ message: String) {
|
||||
assert(Thread.isMainThread, message)
|
||||
}
|
||||
139
mobile/ios/Shared/AsyncReducer.swift
Normal file
139
mobile/ios/Shared/AsyncReducer.swift
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If 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
|
||||
|
||||
private let DefaultDispatchQueue = DispatchQueue.global(qos: DispatchQoS.default.qosClass)
|
||||
|
||||
public func asyncReducer<T, U>(_ initialValue: T, combine: @escaping (T, U) -> Deferred<Maybe<T>>) -> AsyncReducer<T, U> {
|
||||
return AsyncReducer(initialValue: initialValue, combine: combine)
|
||||
}
|
||||
|
||||
/**
|
||||
* A appendable, async `reduce`.
|
||||
*
|
||||
* The reducer starts empty. New items need to be `append`ed.
|
||||
*
|
||||
* The constructor takes an `initialValue`, a `dispatch_queue_t`, and a `combine` function.
|
||||
*
|
||||
* The reduced value can be accessed via the `reducer.terminal` `Deferred<Maybe<T>>`, which is
|
||||
* run once all items have been combined.
|
||||
*
|
||||
* The terminal will never be filled if no items have been appended.
|
||||
*
|
||||
* Once the terminal has been filled, no more items can be appended, and `append` methods will error.
|
||||
*/
|
||||
open class AsyncReducer<T, U> {
|
||||
// T is the accumulator. U is the input value. The returned T is the new accumulated value.
|
||||
public typealias Combine = (T, U) -> Deferred<Maybe<T>>
|
||||
fileprivate let lock = NSRecursiveLock()
|
||||
|
||||
private let dispatchQueue: DispatchQueue
|
||||
private let combine: Combine
|
||||
|
||||
private let initialValueDeferred: Deferred<Maybe<T>>
|
||||
open let terminal: Deferred<Maybe<T>> = Deferred()
|
||||
|
||||
private var queuedItems: [U] = []
|
||||
|
||||
private var isStarted: Bool = false
|
||||
|
||||
/**
|
||||
* Has this task queue finished?
|
||||
* Once the task queue has finished, it cannot have more tasks appended.
|
||||
*/
|
||||
open var isFilled: Bool {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return terminal.isFilled
|
||||
}
|
||||
|
||||
public convenience init(initialValue: T, queue: DispatchQueue = DefaultDispatchQueue, combine: @escaping Combine) {
|
||||
self.init(initialValue: deferMaybe(initialValue), queue: queue, combine: combine)
|
||||
}
|
||||
|
||||
public init(initialValue: Deferred<Maybe<T>>, queue: DispatchQueue = DefaultDispatchQueue, combine: @escaping Combine) {
|
||||
self.dispatchQueue = queue
|
||||
self.combine = combine
|
||||
self.initialValueDeferred = initialValue
|
||||
}
|
||||
|
||||
// This is always protected by a lock, so we don't need to
|
||||
// take another one.
|
||||
fileprivate func ensureStarted() {
|
||||
if self.isStarted {
|
||||
return
|
||||
}
|
||||
|
||||
func queueNext(_ deferredValue: Deferred<Maybe<T>>) {
|
||||
deferredValue.uponQueue(dispatchQueue, block: continueMaybe)
|
||||
}
|
||||
|
||||
func nextItem() -> U? {
|
||||
// Because popFirst is only available on array slices.
|
||||
// removeFirst is fine for range-replaceable collections.
|
||||
return queuedItems.isEmpty ? nil : queuedItems.removeFirst()
|
||||
}
|
||||
|
||||
func continueMaybe(_ res: Maybe<T>) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
if res.isFailure {
|
||||
self.queuedItems.removeAll()
|
||||
self.terminal.fill(Maybe(failure: res.failureValue!))
|
||||
return
|
||||
}
|
||||
|
||||
let accumulator = res.successValue!
|
||||
|
||||
guard let item = nextItem() else {
|
||||
self.terminal.fill(Maybe(success: accumulator))
|
||||
return
|
||||
}
|
||||
|
||||
let combineItem = deferDispatchAsync(dispatchQueue) { _ in
|
||||
return self.combine(accumulator, item)
|
||||
}
|
||||
|
||||
queueNext(combineItem)
|
||||
}
|
||||
|
||||
queueNext(self.initialValueDeferred)
|
||||
self.isStarted = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Append one or more tasks onto the end of the queue.
|
||||
*
|
||||
* @throws AlreadyFilled if the queue has finished already.
|
||||
*/
|
||||
open func append(_ items: U...) throws -> Deferred<Maybe<T>> {
|
||||
return try append(items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a list of tasks onto the end of the queue.
|
||||
*
|
||||
* @throws AlreadyFilled if the queue has already finished.
|
||||
*/
|
||||
open func append(_ items: [U]) throws -> Deferred<Maybe<T>> {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
if terminal.isFilled {
|
||||
throw ReducerError.alreadyFilled
|
||||
}
|
||||
|
||||
queuedItems.append(contentsOf: items)
|
||||
ensureStarted()
|
||||
|
||||
return terminal
|
||||
}
|
||||
}
|
||||
|
||||
enum ReducerError: Error {
|
||||
case alreadyFilled
|
||||
}
|
||||
157
mobile/ios/Shared/AuthenticationKeychainInfo.swift
Normal file
157
mobile/ios/Shared/AuthenticationKeychainInfo.swift
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import SwiftKeychainWrapper
|
||||
|
||||
public let KeychainKeyAuthenticationInfo = "authenticationInfo"
|
||||
public let AllowedPasscodeFailedAttempts = 3
|
||||
|
||||
// Passcode intervals with rawValue in seconds.
|
||||
public enum PasscodeInterval: Int {
|
||||
case immediately = 2
|
||||
case oneMinute = 60
|
||||
case fiveMinutes = 300
|
||||
case tenMinutes = 600
|
||||
case fifteenMinutes = 900
|
||||
case oneHour = 3600
|
||||
}
|
||||
|
||||
// MARK: - Helper methods for accessing Authentication information from the Keychain
|
||||
public extension KeychainWrapper {
|
||||
func authenticationInfo() -> AuthenticationKeychainInfo? {
|
||||
NSKeyedUnarchiver.setClass(AuthenticationKeychainInfo.self, forClassName: "AuthenticationKeychainInfo")
|
||||
return object(forKey: KeychainKeyAuthenticationInfo) as? AuthenticationKeychainInfo
|
||||
}
|
||||
|
||||
func setAuthenticationInfo(_ info: AuthenticationKeychainInfo?) {
|
||||
NSKeyedArchiver.setClassName("AuthenticationKeychainInfo", for: AuthenticationKeychainInfo.self)
|
||||
if let info = info {
|
||||
set(info, forKey: KeychainKeyAuthenticationInfo)
|
||||
} else {
|
||||
removeObject(forKey: KeychainKeyAuthenticationInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class AuthenticationKeychainInfo: NSObject, NSCoding {
|
||||
fileprivate(set) open var lastPasscodeValidationInterval: TimeInterval?
|
||||
fileprivate(set) open var passcode: String?
|
||||
fileprivate(set) open var requiredPasscodeInterval: PasscodeInterval?
|
||||
fileprivate(set) open var lockOutInterval: TimeInterval?
|
||||
fileprivate(set) open var failedAttempts: Int
|
||||
open var useTouchID: Bool
|
||||
|
||||
// Timeout period before user can retry entering passcodes
|
||||
open var lockTimeInterval: TimeInterval = 15 * 60
|
||||
|
||||
public init(passcode: String) {
|
||||
self.passcode = passcode
|
||||
self.requiredPasscodeInterval = .immediately
|
||||
self.failedAttempts = 0
|
||||
self.useTouchID = false
|
||||
}
|
||||
|
||||
open func encode(with aCoder: NSCoder) {
|
||||
if let lastPasscodeValidationInterval = lastPasscodeValidationInterval {
|
||||
let interval = NSNumber(value: lastPasscodeValidationInterval as Double)
|
||||
aCoder.encode(interval, forKey: "lastPasscodeValidationInterval")
|
||||
}
|
||||
|
||||
if let lockOutInterval = lockOutInterval, isLocked() {
|
||||
let interval = NSNumber(value: lockOutInterval as Double)
|
||||
aCoder.encode(interval, forKey: "lockOutInterval")
|
||||
}
|
||||
|
||||
aCoder.encode(passcode, forKey: "passcode")
|
||||
aCoder.encode(requiredPasscodeInterval?.rawValue, forKey: "requiredPasscodeInterval")
|
||||
aCoder.encode(failedAttempts, forKey: "failedAttempts")
|
||||
aCoder.encode(useTouchID, forKey: "useTouchID")
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
self.lastPasscodeValidationInterval = aDecoder.decodeAsDouble(forKey: "lastPasscodeValidationInterval")
|
||||
if let lockOutInterval = aDecoder.decodeObject(forKey: "lockOutInterval") as? NSNumber {
|
||||
self.lockOutInterval = lockOutInterval.doubleValue
|
||||
}
|
||||
self.passcode = aDecoder.decodeObject(forKey: "passcode") as? String
|
||||
self.failedAttempts = aDecoder.decodeAsInt(forKey: "failedAttempts")
|
||||
self.useTouchID = aDecoder.decodeAsBool(forKey: "useTouchID")
|
||||
if var interval = aDecoder.decodeObject(forKey: "requiredPasscodeInterval") as? NSNumber {
|
||||
// We have updated the immediate lockout value to 2 from 0 due to timing issues with systemUptime()
|
||||
interval = interval == 0 ? 2 : interval
|
||||
self.requiredPasscodeInterval = PasscodeInterval(rawValue: interval.intValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - API
|
||||
public extension AuthenticationKeychainInfo {
|
||||
private func resetLockoutState() {
|
||||
self.failedAttempts = 0
|
||||
self.lockOutInterval = nil
|
||||
}
|
||||
|
||||
func updatePasscode(_ passcode: String) {
|
||||
self.passcode = passcode
|
||||
self.lastPasscodeValidationInterval = nil
|
||||
}
|
||||
|
||||
func updateRequiredPasscodeInterval(_ interval: PasscodeInterval) {
|
||||
self.requiredPasscodeInterval = interval
|
||||
self.lastPasscodeValidationInterval = nil
|
||||
}
|
||||
|
||||
func recordValidation() {
|
||||
// Save the timestamp to remember the last time we successfully
|
||||
// validated and clear out the failed attempts counter.
|
||||
self.lastPasscodeValidationInterval = SystemUtils.systemUptime()
|
||||
resetLockoutState()
|
||||
}
|
||||
|
||||
func lockOutUser() {
|
||||
self.lockOutInterval = SystemUtils.systemUptime()
|
||||
}
|
||||
|
||||
func recordFailedAttempt() {
|
||||
if self.failedAttempts >= AllowedPasscodeFailedAttempts {
|
||||
//This is a failed attempt after a lockout period. Reset the lockout state
|
||||
//This prevents failedAttemps from being higher than 3
|
||||
self.resetLockoutState()
|
||||
}
|
||||
self.failedAttempts += 1
|
||||
}
|
||||
|
||||
func isLocked() -> Bool {
|
||||
guard let lockOutInterval = self.lockOutInterval else {
|
||||
return false
|
||||
}
|
||||
|
||||
if SystemUtils.systemUptime() < lockOutInterval {
|
||||
// Unlock and require passcode input
|
||||
resetLockoutState()
|
||||
return false
|
||||
}
|
||||
return (SystemUtils.systemUptime() - (self.lockOutInterval ?? 0)) < lockTimeInterval
|
||||
}
|
||||
|
||||
func requiresValidation() -> Bool {
|
||||
// If there isn't a passcode, don't need validation.
|
||||
guard let _ = passcode else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Need to make sure we've validated in the past. If not, its a definite yes.
|
||||
guard let lastValidationInterval = lastPasscodeValidationInterval,
|
||||
let requireInterval = requiredPasscodeInterval
|
||||
else {
|
||||
return true
|
||||
}
|
||||
|
||||
// We've authenticated before so lets see how long since. If the uptime is less than the last validation stamp,
|
||||
// we probably restarted which means we should require validation.
|
||||
return SystemUtils.systemUptime() - lastValidationInterval > Double(requireInterval.rawValue) ||
|
||||
SystemUtils.systemUptime() < lastValidationInterval
|
||||
}
|
||||
}
|
||||
49
mobile/ios/Shared/Bytes.swift
Normal file
49
mobile/ios/Shared/Bytes.swift
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/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public typealias GUID = String
|
||||
|
||||
/**
|
||||
* Utilities for futzing with bytes and such.
|
||||
*/
|
||||
open class Bytes {
|
||||
open class func generateRandomBytes(_ len: UInt) -> Data {
|
||||
let len = Int(len)
|
||||
var data = Data(count: len)
|
||||
data.withUnsafeMutableBytes { (p: UnsafeMutablePointer<UInt8>) in
|
||||
if (SecRandomCopyBytes(kSecRandomDefault, len, p) != errSecSuccess) {
|
||||
fatalError("Random byte generation failed.")
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
open class func generateGUID() -> GUID {
|
||||
// Turns the standard NSData encoding into the URL-safe variant that Sync expects.
|
||||
return generateRandomBytes(9)
|
||||
.base64EncodedString(options: NSData.Base64EncodingOptions())
|
||||
.replacingOccurrences(of: "/", with: "_", options: NSString.CompareOptions(), range: nil)
|
||||
.replacingOccurrences(of: "+", with: "-", options: NSString.CompareOptions(), range: nil)
|
||||
}
|
||||
|
||||
open class func decodeBase64(_ b64: String) -> Data? {
|
||||
return Data(base64Encoded: b64,
|
||||
options: NSData.Base64DecodingOptions())
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a string of base64 characters into an NSData *without decoding*.
|
||||
* This is to allow HMAC to be computed of the raw base64 string.
|
||||
*/
|
||||
open class func dataFromBase64(_ b64: String) -> Data? {
|
||||
return b64.data(using: String.Encoding.ascii, allowLossyConversion: false)
|
||||
}
|
||||
|
||||
func fromHex(_ str: String) -> Data {
|
||||
// TODO
|
||||
return Data()
|
||||
}
|
||||
}
|
||||
9
mobile/ios/Shared/Cancellable.swift
Normal file
9
mobile/ios/Shared/Cancellable.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
public protocol Cancellable: class {
|
||||
func cancel()
|
||||
var cancelled: Bool { get }
|
||||
var running: Bool { get set }
|
||||
}
|
||||
11
mobile/ios/Shared/CrashSimulator.h
Normal file
11
mobile/ios/Shared/CrashSimulator.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface CrashSimulator : NSObject
|
||||
|
||||
+ (void)forceCrash;
|
||||
|
||||
@end
|
||||
14
mobile/ios/Shared/CrashSimulator.m
Normal file
14
mobile/ios/Shared/CrashSimulator.m
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#import "CrashSimulator.h"
|
||||
|
||||
@implementation CrashSimulator
|
||||
|
||||
+ (void)forceCrash
|
||||
{
|
||||
@throw [[NSException alloc] initWithName:@"Simulated Crash" reason:@"This is a simulated crash." userInfo:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
199
mobile/ios/Shared/DeferredUtils.swift
Normal file
199
mobile/ios/Shared/DeferredUtils.swift
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Deferred
|
||||
// Haskell, baby.
|
||||
|
||||
// Monadic bind/flatMap operator for Deferred.
|
||||
precedencegroup MonadicBindPrecedence {
|
||||
associativity: left
|
||||
higherThan: MonadicDoPrecedence
|
||||
lowerThan: BitwiseShiftPrecedence
|
||||
}
|
||||
|
||||
precedencegroup MonadicDoPrecedence {
|
||||
associativity: left
|
||||
higherThan: MultiplicationPrecedence
|
||||
}
|
||||
|
||||
infix operator >>== : MonadicBindPrecedence
|
||||
infix operator >>> : MonadicDoPrecedence
|
||||
|
||||
@discardableResult public func >>== <T, U>(x: Deferred<Maybe<T>>, f: @escaping (T) -> Deferred<Maybe<U>>) -> Deferred<Maybe<U>> {
|
||||
return chainDeferred(x, f: f)
|
||||
}
|
||||
|
||||
// A termination case.
|
||||
public func >>== <T>(x: Deferred<Maybe<T>>, f: @escaping (T) -> Void) {
|
||||
return x.upon { result in
|
||||
if let v = result.successValue {
|
||||
f(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Monadic `do` for Deferred.
|
||||
@discardableResult public func >>> <T, U>(x: Deferred<Maybe<T>>, f: @escaping () -> Deferred<Maybe<U>>) -> Deferred<Maybe<U>> {
|
||||
return x.bind { res in
|
||||
if res.isSuccess {
|
||||
return f()
|
||||
}
|
||||
return deferMaybe(res.failureValue!)
|
||||
}
|
||||
}
|
||||
|
||||
// Another termination case.
|
||||
public func >>> <T>(x: Deferred<Maybe<T>>, f: @escaping () -> Void) {
|
||||
return x.upon { res in
|
||||
if res.isSuccess {
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a thunk that return a Deferred that resolves to the provided value.
|
||||
*/
|
||||
public func always<T>(_ t: T) -> () -> Deferred<Maybe<T>> {
|
||||
return { deferMaybe(t) }
|
||||
}
|
||||
|
||||
public func deferMaybe<T>(_ s: T) -> Deferred<Maybe<T>> {
|
||||
return Deferred(value: Maybe(success: s))
|
||||
}
|
||||
|
||||
public func deferMaybe<T>(_ e: MaybeErrorType) -> Deferred<Maybe<T>> {
|
||||
return Deferred(value: Maybe(failure: e))
|
||||
}
|
||||
|
||||
public typealias Success = Deferred<Maybe<Void>>
|
||||
|
||||
@discardableResult public func succeed() -> Success {
|
||||
return deferMaybe(())
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single Deferred that represents the sequential chaining
|
||||
* of f over the provided items.
|
||||
*/
|
||||
public func walk<T>(_ items: [T], f: @escaping (T) -> Success) -> Success {
|
||||
return items.reduce(succeed()) { success, item -> Success in
|
||||
success >>> { f(item) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `all`, but thanks to its taking thunks as input, each result is
|
||||
* generated in strict sequence. Fails immediately if any result is failure.
|
||||
*/
|
||||
public func accumulate<T>(_ thunks: [() -> Deferred<Maybe<T>>]) -> Deferred<Maybe<[T]>> {
|
||||
if thunks.isEmpty {
|
||||
return deferMaybe([])
|
||||
}
|
||||
|
||||
let combined = Deferred<Maybe<[T]>>()
|
||||
var results: [T] = []
|
||||
results.reserveCapacity(thunks.count)
|
||||
|
||||
var onValue: ((T) -> Void)!
|
||||
var onResult: ((Maybe<T>) -> Void)!
|
||||
|
||||
// onValue and onResult both hold references to each other niling them out before exiting breaks a reference cycle
|
||||
// We also cannot use unowned here because the thunks are not class types.
|
||||
onValue = { t in
|
||||
results.append(t)
|
||||
if results.count == thunks.count {
|
||||
onResult = nil
|
||||
combined.fill(Maybe(success: results))
|
||||
} else {
|
||||
thunks[results.count]().upon(onResult)
|
||||
}
|
||||
}
|
||||
|
||||
onResult = { r in
|
||||
if r.isFailure {
|
||||
onValue = nil
|
||||
combined.fill(Maybe(failure: r.failureValue!))
|
||||
return
|
||||
}
|
||||
onValue(r.successValue!)
|
||||
}
|
||||
|
||||
thunks[0]().upon(onResult)
|
||||
|
||||
return combined
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a function and turn it into a side-effect that can appear
|
||||
* in a chain of async operations without producing its own value.
|
||||
*/
|
||||
public func effect<T, U>(_ f: @escaping (T) -> U) -> (T) -> Deferred<Maybe<T>> {
|
||||
return { t in
|
||||
_ = f(t)
|
||||
return deferMaybe(t)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single Deferred that represents the sequential chaining of
|
||||
* f over the provided items, with the return value chained through.
|
||||
*/
|
||||
public func walk<T, U, S: Sequence>(_ items: S, start: Deferred<Maybe<U>>, f: @escaping (T, U) -> Deferred<Maybe<U>>) -> Deferred<Maybe<U>> where S.Iterator.Element == T {
|
||||
let fs = items.map { item in
|
||||
return { val in
|
||||
f(item, val)
|
||||
}
|
||||
}
|
||||
return fs.reduce(start, >>==)
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `all`, but doesn't accrue individual values.
|
||||
*/
|
||||
extension Array where Element: Success {
|
||||
public func allSucceed() -> Success {
|
||||
return all(self).bind { results -> Success in
|
||||
if let failure = results.find({ $0.isFailure }) {
|
||||
return deferMaybe(failure.failureValue!)
|
||||
}
|
||||
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func chainDeferred<T, U>(_ a: Deferred<Maybe<T>>, f: @escaping (T) -> Deferred<Maybe<U>>) -> Deferred<Maybe<U>> {
|
||||
return a.bind { res in
|
||||
if let v = res.successValue {
|
||||
return f(v)
|
||||
}
|
||||
return Deferred(value: Maybe<U>(failure: res.failureValue!))
|
||||
}
|
||||
}
|
||||
|
||||
public func chainResult<T, U>(_ a: Deferred<Maybe<T>>, f: @escaping (T) -> Maybe<U>) -> Deferred<Maybe<U>> {
|
||||
return a.map { res in
|
||||
if let v = res.successValue {
|
||||
return f(v)
|
||||
}
|
||||
return Maybe<U>(failure: res.failureValue!)
|
||||
}
|
||||
}
|
||||
|
||||
public func chain<T, U>(_ a: Deferred<Maybe<T>>, f: @escaping (T) -> U) -> Deferred<Maybe<U>> {
|
||||
return chainResult(a, f: { Maybe<U>(success: f($0)) })
|
||||
}
|
||||
|
||||
/// Defer-ifies a block to an async dispatch queue.
|
||||
public func deferDispatchAsync<T>(_ queue: DispatchQueue, f: @escaping () -> Deferred<Maybe<T>>) -> Deferred<Maybe<T>> {
|
||||
let deferred = Deferred<Maybe<T>>()
|
||||
queue.async(execute: {
|
||||
f().upon { result in
|
||||
deferred.fill(result)
|
||||
}
|
||||
})
|
||||
|
||||
return deferred
|
||||
}
|
||||
72
mobile/ios/Shared/DeviceInfo.swift
Normal file
72
mobile/ios/Shared/DeviceInfo.swift
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
|
||||
open class DeviceInfo {
|
||||
// List of device names that don't support advanced visual settings
|
||||
static let lowGraphicsQualityModels = ["iPad", "iPad1,1", "iPhone1,1", "iPhone1,2", "iPhone2,1", "iPhone3,1", "iPhone3,2", "iPhone3,3", "iPod1,1", "iPod2,1", "iPod2,2", "iPod3,1", "iPod4,1", "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4", "iPad3,1", "iPad3,2", "iPad3,3"]
|
||||
|
||||
open static var specificModelName: String {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
|
||||
let machine = systemInfo.machine
|
||||
let mirror = Mirror(reflecting: machine)
|
||||
var identifier = ""
|
||||
|
||||
// Parses the string for the model name via NSUTF8StringEncoding, refer to
|
||||
// http://stackoverflow.com/questions/26028918/ios-how-to-determine-iphone-model-in-swift
|
||||
for child in mirror.children.enumerated() {
|
||||
if let value = child.1.value as? Int8, value != 0 {
|
||||
identifier.append(String(UnicodeScalar(UInt8(value))))
|
||||
}
|
||||
}
|
||||
return identifier
|
||||
}
|
||||
|
||||
/// Return the client name, which can be either "Fennec on Stefan's iPod" or simply "Stefan's iPod" if the application display name cannot be obtained.
|
||||
open class func defaultClientName() -> String {
|
||||
let format = NSLocalizedString("%@ on %@", tableName: "Shared", comment: "A brief descriptive name for this app on this device, used for Send Tab and Synced Tabs. The first argument is the app name. The second argument is the device name.")
|
||||
return String(format: format, AppInfo.displayName, UIDevice.current.name)
|
||||
}
|
||||
|
||||
open class func clientIdentifier(_ prefs: Prefs) -> String {
|
||||
if let id = prefs.stringForKey("clientIdentifier") {
|
||||
return id
|
||||
}
|
||||
let id = UUID().uuidString
|
||||
prefs.setString(id, forKey: "clientIdentifier")
|
||||
return id
|
||||
}
|
||||
|
||||
open class func deviceModel() -> String {
|
||||
return UIDevice.current.model
|
||||
}
|
||||
|
||||
open class func isSimulator() -> Bool {
|
||||
return ProcessInfo.processInfo.environment["SIMULATOR_ROOT"] != nil
|
||||
}
|
||||
|
||||
open class func isBlurSupported() -> Bool {
|
||||
// We've tried multiple ways to make this change visible on simulators, but we
|
||||
// haven't found a solution that worked:
|
||||
// 1. http://stackoverflow.com/questions/21603475/how-can-i-detect-if-the-iphone-my-app-is-on-is-going-to-use-a-simple-transparen
|
||||
// 2. https://gist.github.com/conradev/8655650
|
||||
// Thus, testing has to take place on actual devices.
|
||||
return !lowGraphicsQualityModels.contains(specificModelName)
|
||||
}
|
||||
|
||||
open class func hasConnectivity() -> Bool {
|
||||
let status = Reach().connectionStatus()
|
||||
switch status {
|
||||
case .online(.wwan):
|
||||
return true
|
||||
case .online(.wiFi):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
70
mobile/ios/Shared/Extensions/ArrayExtensions.swift
Normal file
70
mobile/ios/Shared/Extensions/ArrayExtensions.swift
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Array where Element: Comparable {
|
||||
func sameElements(_ arr: [Element]) -> Bool {
|
||||
guard self.count == arr.count else { return false }
|
||||
let sorted = self.sorted(by: <)
|
||||
let arrSorted = arr.sorted(by: <)
|
||||
for elements in sorted.zip(arrSorted) where elements.0 != elements.1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public extension Array {
|
||||
|
||||
func find(_ f: (Iterator.Element) -> Bool) -> Iterator.Element? {
|
||||
for x in self {
|
||||
if f(x) {
|
||||
return x
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains(_ x: Element, f: (Element, Element) -> Bool) -> Bool {
|
||||
for y in self {
|
||||
if f(x, y) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Performs a union operator using the result of f(Element) as the value to base uniqueness on.
|
||||
func union<T: Hashable>(_ arr: [Element], f: ((Element) -> T)) -> [Element] {
|
||||
let result = self + arr
|
||||
return result.unique(f)
|
||||
}
|
||||
|
||||
// Returns unique values in an array using the result of f()
|
||||
func unique<T: Hashable>(_ f: ((Element) -> T)) -> [Element] {
|
||||
var map: [T: Element] = [T: Element]()
|
||||
return self.flatMap { a in
|
||||
let t = f(a)
|
||||
if map[t] == nil {
|
||||
map[t] = a
|
||||
return a
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension Sequence {
|
||||
func every(_ f: (Self.Iterator.Element) -> Bool) -> Bool {
|
||||
for x in self {
|
||||
if !f(x) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
19
mobile/ios/Shared/Extensions/DataExtensions.swift
Normal file
19
mobile/ios/Shared/Extensions/DataExtensions.swift
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Data {
|
||||
public mutating func appendBytes(fromData data: Data) {
|
||||
var bytes = [UInt8](repeating: 0, count: data.count)
|
||||
data.copyBytes(to: &bytes, count: data.count)
|
||||
self.append(bytes, count: bytes.count)
|
||||
}
|
||||
|
||||
public func getBytes() -> [UInt8] {
|
||||
var bytes = [UInt8](repeating: 0, count: self.count)
|
||||
self.copyBytes(to: &bytes, count: self.count)
|
||||
return bytes
|
||||
}
|
||||
}
|
||||
9
mobile/ios/Shared/Extensions/DictionaryExtensions.swift
Normal file
9
mobile/ios/Shared/Extensions/DictionaryExtensions.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
extension Dictionary {
|
||||
public mutating func merge(with dictionary: Dictionary) {
|
||||
dictionary.forEach { updateValue($1, forKey: $0) }
|
||||
}
|
||||
}
|
||||
74
mobile/ios/Shared/Extensions/HashExtensions.swift
Normal file
74
mobile/ios/Shared/Extensions/HashExtensions.swift
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
public var sha1: Data {
|
||||
let len = Int(CC_SHA1_DIGEST_LENGTH)
|
||||
let digest = UnsafeMutablePointer<UInt8>.allocate(capacity: len)
|
||||
CC_SHA1((self as NSData).bytes, CC_LONG(self.count), digest)
|
||||
return Data(bytes: UnsafePointer<UInt8>(digest), count: len)
|
||||
}
|
||||
|
||||
public var sha256: Data {
|
||||
let len = Int(CC_SHA256_DIGEST_LENGTH)
|
||||
let digest = UnsafeMutablePointer<UInt8>.allocate(capacity: len)
|
||||
CC_SHA256((self as NSData).bytes, CC_LONG(self.count), digest)
|
||||
return Data(bytes: UnsafePointer<UInt8>(digest), count: len)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
public var sha1: Data {
|
||||
let data = self.data(using: String.Encoding.utf8)!
|
||||
return data.sha1
|
||||
}
|
||||
|
||||
public var sha256: Data {
|
||||
let data = self.data(using: String.Encoding.utf8)!
|
||||
return data.sha256
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
public func hmacSha256WithKey(_ key: Data) -> Data {
|
||||
let len = Int(CC_SHA256_DIGEST_LENGTH)
|
||||
|
||||
let digest = UnsafeMutablePointer<UInt8>.allocate(capacity: len)
|
||||
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256),
|
||||
(key as NSData).bytes, Int(key.count),
|
||||
(self as NSData).bytes, Int(self.count),
|
||||
digest)
|
||||
return Data(bytes: UnsafePointer<UInt8>(digest), count: len)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
public var utf8EncodedData: Data {
|
||||
return self.data(using: String.Encoding.utf8, allowLossyConversion: false)!
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
public var utf8EncodedString: String? {
|
||||
return NSString(data: self, encoding: String.Encoding.utf8.rawValue) as String?
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
public func xoredWith(_ other: Data) -> Data? {
|
||||
if self.count != other.count {
|
||||
return nil
|
||||
}
|
||||
var xoredBytes = [UInt8](repeating: 0, count: self.count)
|
||||
let selfBytes = (self as NSData).bytes.bindMemory(to: UInt8.self, capacity: self.count)
|
||||
let otherBytes = (other as NSData).bytes.bindMemory(to: UInt8.self, capacity: other.count)
|
||||
for i in 0..<self.count {
|
||||
xoredBytes[i] = selfBytes[i] ^ otherBytes[i]
|
||||
}
|
||||
return Data(bytes: UnsafePointer<UInt8>(xoredBytes), count: self.count)
|
||||
}
|
||||
|
||||
}
|
||||
70
mobile/ios/Shared/Extensions/HexExtensions.swift
Normal file
70
mobile/ios/Shared/Extensions/HexExtensions.swift
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
public var hexDecodedData: Data {
|
||||
// Convert to a CString and make sure it has an even number of characters (terminating 0 is included, so we
|
||||
// check for uneven!)
|
||||
guard let cString = self.cString(using: String.Encoding.ascii), (cString.count % 2) == 1 else {
|
||||
return Data()
|
||||
}
|
||||
|
||||
var result = Data(capacity: (cString.count - 1) / 2)
|
||||
for i in stride(from: 0, to: (cString.count - 1), by: 2) {
|
||||
guard let l = hexCharToByte(cString[i]), let r = hexCharToByte(cString[i+1]) else {
|
||||
return Data()
|
||||
}
|
||||
var value: UInt8 = (l << 4) | r
|
||||
result.append(&value, count: MemoryLayout.size(ofValue: value))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func hexCharToByte(_ c: CChar) -> UInt8? {
|
||||
if c >= 48 && c <= 57 { // 0 - 9
|
||||
return UInt8(c - 48)
|
||||
}
|
||||
if c >= 97 && c <= 102 { // a - f
|
||||
return UInt8(10) + UInt8(c - 97)
|
||||
}
|
||||
if c >= 65 && c <= 70 { // A - F
|
||||
return UInt8(10) + UInt8(c - 65)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private let HexDigits: [String] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"]
|
||||
|
||||
extension Data {
|
||||
public var hexEncodedString: String {
|
||||
var result = String()
|
||||
result.reserveCapacity(count * 2)
|
||||
withUnsafeBytes { (p: UnsafePointer<UInt8>) in
|
||||
for i in 0..<count {
|
||||
result.append(HexDigits[Int((p[i] & 0xf0) >> 4)])
|
||||
result.append(HexDigits[Int(p[i] & 0x0f)])
|
||||
}
|
||||
}
|
||||
return String(result)
|
||||
}
|
||||
|
||||
public static func randomOfLength(_ length: UInt) -> Data? {
|
||||
let length = Int(length)
|
||||
var data = Data(count: length)
|
||||
var result: Int32 = 0
|
||||
data.withUnsafeMutableBytes { (p: UnsafeMutablePointer<UInt8>) in
|
||||
result = SecRandomCopyBytes(kSecRandomDefault, length, p)
|
||||
}
|
||||
return result == 0 ? data : nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
public var base64EncodedString: String {
|
||||
return self.base64EncodedString(options: NSData.Base64EncodingOptions())
|
||||
}
|
||||
}
|
||||
65
mobile/ios/Shared/Extensions/JSONExtensions.swift
Normal file
65
mobile/ios/Shared/Extensions/JSONExtensions.swift
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
public extension JSON {
|
||||
func isStringOrNull() -> Bool {
|
||||
return self.isString() ||
|
||||
self.isNull()
|
||||
}
|
||||
|
||||
func isError() -> Bool {
|
||||
return self.error != nil
|
||||
}
|
||||
|
||||
func isString() -> Bool {
|
||||
// SwiftyJSON doesn't link values to types; it's possible for `self.type == .string` but
|
||||
// `self.string` to return `nil`. Validate both.
|
||||
return self.type == .string &&
|
||||
self.string != nil
|
||||
}
|
||||
|
||||
func isBool() -> Bool {
|
||||
return self.type == .bool
|
||||
}
|
||||
|
||||
func isArray() -> Bool {
|
||||
return self.type == .array
|
||||
}
|
||||
|
||||
func isDictionary() -> Bool {
|
||||
return self.type == .dictionary
|
||||
}
|
||||
|
||||
// Bear in mind that for this function to work you need to set the value to NSNull:
|
||||
// ```
|
||||
// var myObj = JSON(…)
|
||||
// myObj["foo"] = someOptional ?? NSNull()
|
||||
// ```
|
||||
// This is… easy to get wrong.
|
||||
func isNull() -> Bool {
|
||||
return self.type == .null
|
||||
}
|
||||
|
||||
func isInt() -> Bool {
|
||||
return self.type == .number && self.int != nil
|
||||
}
|
||||
|
||||
func isNumber() -> Bool {
|
||||
return self.type == .number && self.number != nil
|
||||
}
|
||||
|
||||
func isDouble() -> Bool {
|
||||
return self.type == .number && self.double != nil
|
||||
}
|
||||
|
||||
// SwiftyJSON pretty prints the string value by default. Since all of our
|
||||
// existing code required the string to not be pretty printed, this helper
|
||||
// can be used as a shorthand for non-pretty printed strings.
|
||||
func stringValue() -> String? {
|
||||
return self.rawString(.utf8, options: [])
|
||||
}
|
||||
}
|
||||
62
mobile/ios/Shared/Extensions/KeychainWrapperExtensions.swift
Normal file
62
mobile/ios/Shared/Extensions/KeychainWrapperExtensions.swift
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import XCGLogger
|
||||
import SwiftKeychainWrapper
|
||||
|
||||
private let log = Logger.keychainLogger
|
||||
|
||||
public extension KeychainWrapper {
|
||||
static var sharedAppContainerKeychain: KeychainWrapper {
|
||||
let baseBundleIdentifier = AppInfo.baseBundleIdentifier
|
||||
let accessGroupPrefix = Bundle.main.object(forInfoDictionaryKey: "MozDevelopmentTeam") as! String
|
||||
let accessGroupIdentifier = AppInfo.keychainAccessGroupWithPrefix(accessGroupPrefix)
|
||||
return KeychainWrapper(serviceName: baseBundleIdentifier, accessGroup: accessGroupIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
public extension KeychainWrapper {
|
||||
func ensureStringItemAccessibility(_ accessibility: SwiftKeychainWrapper.KeychainItemAccessibility, forKey key: String) {
|
||||
if self.hasValue(forKey: key) {
|
||||
if self.accessibilityOfKey(key) != .afterFirstUnlock {
|
||||
log.debug("updating item \(key) with \(accessibility)")
|
||||
|
||||
guard let value = self.string(forKey: key) else {
|
||||
log.error("failed to get item \(key)")
|
||||
return
|
||||
}
|
||||
|
||||
if !self.removeObject(forKey: key) {
|
||||
log.warning("failed to remove item \(key)")
|
||||
}
|
||||
|
||||
if !self.set(value, forKey: key, withAccessibility: accessibility) {
|
||||
log.warning("failed to update item \(key)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ensureObjectItemAccessibility(_ accessibility: SwiftKeychainWrapper.KeychainItemAccessibility, forKey key: String) {
|
||||
if self.hasValue(forKey: key) {
|
||||
if self.accessibilityOfKey(key) != .afterFirstUnlock {
|
||||
log.debug("updating item \(key) with \(accessibility)")
|
||||
|
||||
guard let value = self.object(forKey: key) else {
|
||||
log.error("failed to get item \(key)")
|
||||
return
|
||||
}
|
||||
|
||||
if !self.removeObject(forKey: key) {
|
||||
log.warning("failed to remove item \(key)")
|
||||
}
|
||||
|
||||
if !self.set(value, forKey: key, withAccessibility: accessibility) {
|
||||
log.warning("failed to update item \(key)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
mobile/ios/Shared/Extensions/NSCharacterSetExtensions.swift
Normal file
15
mobile/ios/Shared/Extensions/NSCharacterSetExtensions.swift
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/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension CharacterSet {
|
||||
public static func URLAllowedCharacterSet() -> CharacterSet {
|
||||
return CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%")
|
||||
}
|
||||
|
||||
public static func SearchTermsAllowedCharacterSet() -> CharacterSet {
|
||||
return CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*-_.")
|
||||
}
|
||||
}
|
||||
41
mobile/ios/Shared/Extensions/NSCoderExtensions.swift
Normal file
41
mobile/ios/Shared/Extensions/NSCoderExtensions.swift
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* There are some oddnesses around the different ways that NSKeyedArchiver decodes objects based on whether or not they were
|
||||
* originally encoded using Swift 2.x or Swift 3.
|
||||
* If the object was encoded on Swift 2.x, then you need to use decodeObject to unwrap it. But that will return a nil if the object was encoded on Swift 3
|
||||
* For swift 3 encoded objects to you need to use decode<Type>
|
||||
* These helper functions provide a unified way of achieving that
|
||||
**/
|
||||
extension NSCoder {
|
||||
/**
|
||||
* Decode as Int regardless of which Swift version was used to encode it
|
||||
**/
|
||||
open func decodeAsInt(forKey key: String) -> Int {
|
||||
return self.decodeObject(forKey: key) as? Int ?? self.decodeInteger(forKey: key)
|
||||
}
|
||||
/**
|
||||
* Decode as UInt64 regardless of which Swift version was used to encode it
|
||||
**/
|
||||
open func decodeAsUInt64(forKey key: String) -> UInt64 {
|
||||
return (self.decodeObject(forKey: key) as? NSNumber)?.uint64Value ?? UInt64(self.decodeInt64(forKey: key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode as Bool regardless of which Swift version was used to encode it
|
||||
**/
|
||||
open func decodeAsBool(forKey key: String) -> Bool {
|
||||
return self.decodeObject(forKey: key) as? Bool ?? self.decodeBool(forKey: key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode as Double regardless of which Swift version was used to encode it
|
||||
**/
|
||||
open func decodeAsDouble(forKey key: String) -> Double {
|
||||
return (self.decodeObject(forKey: key) as? NSNumber)?.doubleValue ?? self.decodeDouble(forKey: key)
|
||||
}
|
||||
}
|
||||
112
mobile/ios/Shared/Extensions/NSFileManagerExtensions.swift
Normal file
112
mobile/ios/Shared/Extensions/NSFileManagerExtensions.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/. */
|
||||
|
||||
/* Created and contributed by Nikolai Ruhe and rewritten in Swift.
|
||||
* https://github.com/NikolaiRuhe/NRFoundation */
|
||||
|
||||
import Foundation
|
||||
|
||||
public let NSFileManagerExtensionsDomain = "org.mozilla.NSFileManagerExtensions"
|
||||
|
||||
public enum NSFileManagerExtensionsErrorCodes: Int {
|
||||
case enumeratorFailure = 0
|
||||
case enumeratorElementNotURL = 1
|
||||
case errorEnumeratingDirectory = 2
|
||||
}
|
||||
|
||||
public extension FileManager {
|
||||
|
||||
private func directoryEnumeratorForURL(_ url: URL) throws -> FileManager.DirectoryEnumerator {
|
||||
let prefetchedProperties = [
|
||||
URLResourceKey.isRegularFileKey,
|
||||
URLResourceKey.fileAllocatedSizeKey,
|
||||
URLResourceKey.totalFileAllocatedSizeKey
|
||||
]
|
||||
|
||||
// If we run into an issue getting an enumerator for the given URL, capture the error and bail out later.
|
||||
var enumeratorError: NSError?
|
||||
let errorHandler: (URL, Error) -> Bool = { _, error in
|
||||
enumeratorError = error as NSError
|
||||
return false
|
||||
}
|
||||
|
||||
guard let directoryEnumerator = FileManager.default.enumerator(at: url,
|
||||
includingPropertiesForKeys: prefetchedProperties,
|
||||
options: [],
|
||||
errorHandler: errorHandler) else {
|
||||
throw errorWithCode(.enumeratorFailure)
|
||||
}
|
||||
|
||||
// Bail out if we encountered an issue getting the enumerator.
|
||||
if let _ = enumeratorError {
|
||||
throw errorWithCode(.errorEnumeratingDirectory, underlyingError: enumeratorError)
|
||||
}
|
||||
|
||||
return directoryEnumerator
|
||||
}
|
||||
|
||||
private func sizeForItemURL(_ url: Any, withPrefix prefix: String) throws -> Int64 {
|
||||
guard let itemURL = url as? URL else {
|
||||
throw errorWithCode(.enumeratorElementNotURL)
|
||||
}
|
||||
|
||||
// Skip files that are not regular and don't match our prefix
|
||||
guard itemURL.isRegularFile && itemURL.lastComponentIsPrefixedBy(prefix) else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return itemURL.allocatedFileSize()
|
||||
}
|
||||
|
||||
func allocatedSizeOfDirectoryAtURL(_ url: URL, forFilesPrefixedWith prefix: String, isLargerThanBytes threshold: Int64) throws -> Bool {
|
||||
let directoryEnumerator = try directoryEnumeratorForURL(url)
|
||||
var acc: Int64 = 0
|
||||
for item in directoryEnumerator {
|
||||
acc += try sizeForItemURL(item as AnyObject, withPrefix: prefix)
|
||||
if acc > threshold {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the precise size of the given directory on disk.
|
||||
|
||||
- parameter url: Directory URL
|
||||
- parameter prefix: Prefix of files to check for size
|
||||
|
||||
- throws: Error reading/operating on disk.
|
||||
*/
|
||||
func getAllocatedSizeOfDirectoryAtURL(_ url: URL, forFilesPrefixedWith prefix: String) throws -> Int64 {
|
||||
let directoryEnumerator = try directoryEnumeratorForURL(url)
|
||||
return try directoryEnumerator.reduce(0) {
|
||||
let size = try sizeForItemURL($1 as AnyObject, withPrefix: prefix)
|
||||
return $0 + size
|
||||
}
|
||||
}
|
||||
|
||||
func contentsOfDirectoryAtPath(_ path: String, withFilenamePrefix prefix: String) throws -> [String] {
|
||||
return try FileManager.default.contentsOfDirectory(atPath: path)
|
||||
.filter { $0.hasPrefix("\(prefix).") }
|
||||
.sorted { $0 < $1 }
|
||||
}
|
||||
|
||||
func removeItemInDirectory(_ directory: String, named: String) throws {
|
||||
let file = URL(fileURLWithPath: directory).appendingPathComponent(named).path
|
||||
try self.removeItem(atPath: file)
|
||||
}
|
||||
|
||||
private func errorWithCode(_ code: NSFileManagerExtensionsErrorCodes, underlyingError error: NSError? = nil) -> NSError {
|
||||
var userInfo = [String: AnyObject]()
|
||||
if let _ = error {
|
||||
userInfo[NSUnderlyingErrorKey] = error
|
||||
}
|
||||
|
||||
return NSError(
|
||||
domain: NSFileManagerExtensionsDomain,
|
||||
code: code.rawValue,
|
||||
userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
public func colorSubstring(_ substring: String, withColor color: UIColor) {
|
||||
self.attributeSubstring(substring, forAttribute: NSForegroundColorAttributeName, withValue: color)
|
||||
}
|
||||
|
||||
public func pitchSubstring(_ substring: String, withPitch pitch: Double) {
|
||||
let pitchValue = NSNumber(value: pitch as Double)
|
||||
self.attributeSubstring(substring, forAttribute: UIAccessibilitySpeechAttributePitch, withValue: pitchValue)
|
||||
}
|
||||
|
||||
private func attributeSubstring(_ substring: String, forAttribute attribute: String, withValue value: AnyObject) {
|
||||
let nsString = self.string as NSString
|
||||
let range = nsString.range(of: substring)
|
||||
self.addAttribute(attribute, value: value, range: range)
|
||||
}
|
||||
}
|
||||
31
mobile/ios/Shared/Extensions/NSScannerExtensions.swift
Normal file
31
mobile/ios/Shared/Extensions/NSScannerExtensions.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Scanner {
|
||||
public func scanUnsignedLongLong() -> UInt64? {
|
||||
var value: UInt64 = 0
|
||||
if scanUnsignedLongLong(&value) {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func scanLongLong() -> Int64? {
|
||||
var value: Int64 = 0
|
||||
if scanInt64(&value) {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func scanDouble() -> Double? {
|
||||
var value: Double = 0
|
||||
if scanDouble(&value) {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
20
mobile/ios/Shared/Extensions/NSStringExtensions.swift
Normal file
20
mobile/ios/Shared/Extensions/NSStringExtensions.swift
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
public static func contentsOfFileWithResourceName(_ name: String, ofType type: String, fromBundle bundle: Bundle, encoding: String.Encoding, error: NSErrorPointer) -> String? {
|
||||
if let path = bundle.path(forResource: name, ofType: type) {
|
||||
do {
|
||||
return try String(contentsOfFile: path, encoding: encoding)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
498
mobile/ios/Shared/Extensions/NSURLExtensions.swift
Normal file
498
mobile/ios/Shared/Extensions/NSURLExtensions.swift
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
|
||||
private struct ETLDEntry: CustomStringConvertible {
|
||||
let entry: String
|
||||
|
||||
var isNormal: Bool { return isWild || !isException }
|
||||
var isWild: Bool = false
|
||||
var isException: Bool = false
|
||||
|
||||
init(entry: String) {
|
||||
self.entry = entry
|
||||
self.isWild = entry.hasPrefix("*")
|
||||
self.isException = entry.hasPrefix("!")
|
||||
}
|
||||
|
||||
fileprivate var description: String {
|
||||
return "{ Entry: \(entry), isWildcard: \(isWild), isException: \(isException) }"
|
||||
}
|
||||
}
|
||||
|
||||
private typealias TLDEntryMap = [String: ETLDEntry]
|
||||
|
||||
private func loadEntriesFromDisk() -> TLDEntryMap? {
|
||||
if let data = String.contentsOfFileWithResourceName("effective_tld_names", ofType: "dat", fromBundle: Bundle(identifier: "org.mozilla.Shared")!, encoding: String.Encoding.utf8, error: nil) {
|
||||
let lines = data.components(separatedBy: "\n")
|
||||
let trimmedLines = lines.filter { !$0.hasPrefix("//") && $0 != "\n" && $0 != "" }
|
||||
|
||||
var entries = TLDEntryMap()
|
||||
for line in trimmedLines {
|
||||
let entry = ETLDEntry(entry: line)
|
||||
let key: String
|
||||
if entry.isWild {
|
||||
// Trim off the '*.' part of the line
|
||||
key = line.substring(from: line.characters.index(line.startIndex, offsetBy: 2))
|
||||
} else if entry.isException {
|
||||
// Trim off the '!' part of the line
|
||||
key = line.substring(from: line.characters.index(line.startIndex, offsetBy: 1))
|
||||
} else {
|
||||
key = line
|
||||
}
|
||||
entries[key] = entry
|
||||
}
|
||||
return entries
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private var etldEntries: TLDEntryMap? = {
|
||||
return loadEntriesFromDisk()
|
||||
}()
|
||||
|
||||
// MARK: - Local Resource URL Extensions
|
||||
extension URL {
|
||||
|
||||
public func allocatedFileSize() -> Int64 {
|
||||
// First try to get the total allocated size and in failing that, get the file allocated size
|
||||
return getResourceLongLongForKey(URLResourceKey.totalFileAllocatedSizeKey.rawValue)
|
||||
?? getResourceLongLongForKey(URLResourceKey.fileAllocatedSizeKey.rawValue)
|
||||
?? 0
|
||||
}
|
||||
|
||||
public func getResourceValueForKey(_ key: String) -> Any? {
|
||||
let resourceKey = URLResourceKey(key)
|
||||
let keySet = Set<URLResourceKey>([resourceKey])
|
||||
|
||||
var val: Any?
|
||||
do {
|
||||
let values = try resourceValues(forKeys: keySet)
|
||||
val = values.allValues[resourceKey]
|
||||
} catch _ {
|
||||
return nil
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
public func getResourceLongLongForKey(_ key: String) -> Int64? {
|
||||
return (getResourceValueForKey(key) as? NSNumber)?.int64Value
|
||||
}
|
||||
|
||||
public func getResourceBoolForKey(_ key: String) -> Bool? {
|
||||
return getResourceValueForKey(key) as? Bool
|
||||
}
|
||||
|
||||
public var isRegularFile: Bool {
|
||||
return getResourceBoolForKey(URLResourceKey.isRegularFileKey.rawValue) ?? false
|
||||
}
|
||||
|
||||
public func lastComponentIsPrefixedBy(_ prefix: String) -> Bool {
|
||||
return (pathComponents.last?.hasPrefix(prefix) ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
// The list of permanent URI schemes has been taken from http://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
|
||||
private let permanentURISchemes = ["aaa", "aaas", "about", "acap", "acct", "cap", "cid", "coap", "coaps", "crid", "data", "dav", "dict", "dns", "example", "file", "ftp", "geo", "go", "gopher", "h323", "http", "https", "iax", "icap", "im", "imap", "info", "ipp", "ipps", "iris", "iris.beep", "iris.lwz", "iris.xpc", "iris.xpcs", "jabber", "ldap", "mailto", "mid", "msrp", "msrps", "mtqp", "mupdate", "news", "nfs", "ni", "nih", "nntp", "opaquelocktoken", "pkcs11", "pop", "pres", "reload", "rtsp", "rtsps", "rtspu", "service", "session", "shttp", "sieve", "sip", "sips", "sms", "snmp", "soap.beep", "soap.beeps", "stun", "stuns", "tag", "tel", "telnet", "tftp", "thismessage", "tip", "tn3270", "turn", "turns", "tv", "urn", "vemmi", "vnc", "ws", "wss", "xcon", "xcon-userid", "xmlrpc.beep", "xmlrpc.beeps", "xmpp", "z39.50r", "z39.50s"]
|
||||
|
||||
extension URL {
|
||||
|
||||
public func withQueryParams(_ params: [URLQueryItem]) -> URL {
|
||||
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)!
|
||||
var items = (components.queryItems ?? [])
|
||||
for param in params {
|
||||
items.append(param)
|
||||
}
|
||||
components.queryItems = items
|
||||
return components.url!
|
||||
}
|
||||
|
||||
public func withQueryParam(_ name: String, value: String) -> URL {
|
||||
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)!
|
||||
let item = URLQueryItem(name: name, value: value)
|
||||
components.queryItems = (components.queryItems ?? []) + [item]
|
||||
return components.url!
|
||||
}
|
||||
|
||||
public func getQuery() -> [String: String] {
|
||||
var results = [String: String]()
|
||||
let keyValues = self.query?.components(separatedBy: "&")
|
||||
|
||||
if keyValues?.count ?? 0 > 0 {
|
||||
for pair in keyValues! {
|
||||
let kv = pair.components(separatedBy: "=")
|
||||
if kv.count > 1 {
|
||||
results[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
public var hostPort: String? {
|
||||
if let host = self.host {
|
||||
if let port = (self as NSURL).port?.int32Value {
|
||||
return "\(host):\(port)"
|
||||
}
|
||||
return host
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public var origin: String? {
|
||||
guard isWebPage(includeDataURIs: false), let hostPort = self.hostPort, let scheme = scheme else {
|
||||
return nil
|
||||
}
|
||||
return "\(scheme)://\(hostPort)"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the second level domain (SLD) of a url. It removes any subdomain/TLD
|
||||
*
|
||||
* E.g., https://m.foo.com/bar/baz?noo=abc#123 => foo
|
||||
**/
|
||||
public var hostSLD: String {
|
||||
guard let publicSuffix = self.publicSuffix, let baseDomain = self.baseDomain else {
|
||||
return self.normalizedHost ?? self.absoluteString
|
||||
}
|
||||
return baseDomain.replacingOccurrences(of: ".\(publicSuffix)", with: "")
|
||||
}
|
||||
|
||||
public var normalizedHostAndPath: String? {
|
||||
if let normalizedHost = self.normalizedHost {
|
||||
return normalizedHost + self.path
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public var absoluteDisplayString: String {
|
||||
var urlString = self.absoluteString
|
||||
// For http URLs, get rid of the trailing slash if the path is empty or '/'
|
||||
if (self.scheme == "http" || self.scheme == "https") && (self.path == "/") && urlString.endsWith("/") {
|
||||
urlString = urlString.substring(to: urlString.characters.index(urlString.endIndex, offsetBy: -1))
|
||||
}
|
||||
// If it's basic http, strip out the string but leave anything else in
|
||||
if urlString.hasPrefix("http://") {
|
||||
return urlString.substring(from: urlString.characters.index(urlString.startIndex, offsetBy: 7))
|
||||
} else {
|
||||
return urlString
|
||||
}
|
||||
}
|
||||
|
||||
/// String suitable for displaying outside of the app, for example in notifications, were Data Detectors will
|
||||
/// linkify the text and make it into a openable-in-Safari link.
|
||||
public var absoluteDisplayExternalString: String {
|
||||
return self.absoluteDisplayString.replacingOccurrences(of: ".", with: "\u{2024}")
|
||||
}
|
||||
|
||||
public var displayURL: URL? {
|
||||
if self.isReaderModeURL {
|
||||
return self.decodeReaderModeURL?.havingRemovedAuthorisationComponents()
|
||||
}
|
||||
|
||||
if self.isErrorPageURL {
|
||||
if let decodedURL = self.originalURLFromErrorURL {
|
||||
return decodedURL.displayURL
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if !self.isAboutURL {
|
||||
return self.havingRemovedAuthorisationComponents()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the base domain from a given hostname. The base domain name is defined as the public domain suffix
|
||||
with the base private domain attached to the front. For example, for the URL www.bbc.co.uk, the base domain
|
||||
would be bbc.co.uk. The base domain includes the public suffix (co.uk) + one level down (bbc).
|
||||
|
||||
:returns: The base domain string for the given host name.
|
||||
*/
|
||||
public var baseDomain: String? {
|
||||
guard !isIPv6, let host = host else { return nil }
|
||||
|
||||
// If this is just a hostname and not a FQDN, use the entire hostname.
|
||||
if !host.contains(".") {
|
||||
return host
|
||||
}
|
||||
|
||||
return publicSuffixFromHost(host, withAdditionalParts: 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns just the domain, but with the same scheme, and a trailing '/'.
|
||||
*
|
||||
* E.g., https://m.foo.com/bar/baz?noo=abc#123 => https://foo.com/
|
||||
*
|
||||
* Any failure? Return this URL.
|
||||
*/
|
||||
public var domainURL: URL {
|
||||
if let normalized = self.normalizedHost {
|
||||
// Use NSURLComponents instead of NSURL since the former correctly preserves
|
||||
// brackets for IPv6 hosts, whereas the latter escapes them.
|
||||
var components = URLComponents()
|
||||
components.scheme = self.scheme
|
||||
components.host = normalized
|
||||
components.path = "/"
|
||||
return components.url ?? self
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
public var normalizedHost: String? {
|
||||
// Use components.host instead of self.host since the former correctly preserves
|
||||
// brackets for IPv6 hosts, whereas the latter strips them.
|
||||
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false), var host = components.host, host != "" else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let range = host.range(of: "^(www|mobile|m)\\.", options: .regularExpression) {
|
||||
host.replaceSubrange(range, with: "")
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the public portion of the host name determined by the public suffix list found here: https://publicsuffix.org/list/.
|
||||
For example for the url www.bbc.co.uk, based on the entries in the TLD list, the public suffix would return co.uk.
|
||||
|
||||
:returns: The public suffix for within the given hostname.
|
||||
*/
|
||||
public var publicSuffix: String? {
|
||||
if let host = self.host {
|
||||
return publicSuffixFromHost(host, withAdditionalParts: 0)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func isWebPage(includeDataURIs: Bool = true) -> Bool {
|
||||
let schemes = includeDataURIs ? ["http", "https", "data"] : ["http", "https"]
|
||||
if let scheme = scheme, schemes.contains(scheme) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// This helps find local urls that we do not want to show loading bars on.
|
||||
// These utility pages should be invisible to the user
|
||||
public var isLocalUtility: Bool {
|
||||
guard self.isLocal else {
|
||||
return false
|
||||
}
|
||||
let utilityURLs = ["/errors", "/about/sessionrestore", "/about/home", "/reader-mode"]
|
||||
return utilityURLs.contains { self.path.startsWith($0) }
|
||||
}
|
||||
|
||||
public var isLocal: Bool {
|
||||
guard isWebPage(includeDataURIs: false) else {
|
||||
return false
|
||||
}
|
||||
// iOS forwards hostless URLs (e.g., http://:6571) to localhost.
|
||||
guard let host = host, !host.isEmpty else {
|
||||
return true
|
||||
}
|
||||
|
||||
return host.lowercased() == "localhost" || host == "127.0.0.1"
|
||||
}
|
||||
|
||||
public var isIPv6: Bool {
|
||||
return host?.contains(":") ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
Returns whether the URL's scheme is one of those listed on the official list of URI schemes.
|
||||
This only accepts permanent schemes: historical and provisional schemes are not accepted.
|
||||
*/
|
||||
public var schemeIsValid: Bool {
|
||||
guard let scheme = scheme else { return false }
|
||||
return permanentURISchemes.contains(scheme.lowercased())
|
||||
}
|
||||
|
||||
public func havingRemovedAuthorisationComponents() -> URL {
|
||||
guard var urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false) else {
|
||||
return self
|
||||
}
|
||||
urlComponents.user = nil
|
||||
urlComponents.password = nil
|
||||
if let url = urlComponents.url {
|
||||
return url
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// Extensions to deal with ReaderMode URLs
|
||||
|
||||
extension URL {
|
||||
public var isReaderModeURL: Bool {
|
||||
let scheme = self.scheme, host = self.host, path = self.path
|
||||
return scheme == "http" && host == "localhost" && path == "/reader-mode/page"
|
||||
}
|
||||
|
||||
public var decodeReaderModeURL: URL? {
|
||||
if self.isReaderModeURL {
|
||||
if let components = URLComponents(url: self, resolvingAgainstBaseURL: false), let queryItems = components.queryItems, queryItems.count == 1 {
|
||||
if let queryItem = queryItems.first, let value = queryItem.value {
|
||||
return URL(string: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func encodeReaderModeURL(_ baseReaderModeURL: String) -> URL? {
|
||||
if let encodedURL = absoluteString.addingPercentEncoding(withAllowedCharacters: CharacterSet.alphanumerics) {
|
||||
if let aboutReaderURL = URL(string: "\(baseReaderModeURL)?url=\(encodedURL)") {
|
||||
return aboutReaderURL
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers to deal with ErrorPage URLs
|
||||
|
||||
extension URL {
|
||||
public var isErrorPageURL: Bool {
|
||||
if let host = self.host {
|
||||
return self.scheme == "http" && host == "localhost" && path == "/errors/error.html"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public var originalURLFromErrorURL: URL? {
|
||||
let components = URLComponents(url: self, resolvingAgainstBaseURL: false)
|
||||
if let queryURL = components?.queryItems?.find({ $0.name == "url" })?.value {
|
||||
return URL(string: queryURL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers to deal with About URLs
|
||||
extension URL {
|
||||
public var isAboutHomeURL: Bool {
|
||||
if let urlString = self.getQuery()["url"]?.unescape(), isErrorPageURL {
|
||||
let url = URL(string: urlString) ?? self
|
||||
return url.aboutComponent == "home"
|
||||
}
|
||||
return self.aboutComponent == "home"
|
||||
}
|
||||
|
||||
public var isAboutURL: Bool {
|
||||
return self.aboutComponent != nil
|
||||
}
|
||||
|
||||
/// If the URI is an about: URI, return the path after "about/" in the URI.
|
||||
/// For example, return "home" for "http://localhost:1234/about/home/#panel=0".
|
||||
public var aboutComponent: String? {
|
||||
let aboutPath = "/about/"
|
||||
guard let scheme = self.scheme, let host = self.host else {
|
||||
return nil
|
||||
}
|
||||
if scheme == "http" && host == "localhost" && path.startsWith(aboutPath) {
|
||||
return path.substring(from: aboutPath.endIndex)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: Private Helpers
|
||||
private extension URL {
|
||||
func publicSuffixFromHost( _ host: String, withAdditionalParts additionalPartCount: Int) -> String? {
|
||||
if host.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check edge case where the host is either a single or double '.'.
|
||||
if host.isEmpty || NSString(string: host).lastPathComponent == "." {
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* The following algorithm breaks apart the domain and checks each sub domain against the effective TLD
|
||||
* entries from the effective_tld_names.dat file. It works like this:
|
||||
*
|
||||
* Example Domain: test.bbc.co.uk
|
||||
* TLD Entry: bbc
|
||||
*
|
||||
* 1. Start off by checking the current domain (test.bbc.co.uk)
|
||||
* 2. Also store the domain after the next dot (bbc.co.uk)
|
||||
* 3. If we find an entry that matches the current domain (test.bbc.co.uk), perform the following checks:
|
||||
* i. If the domain is a wildcard AND the previous entry is not nil, then the current domain matches
|
||||
* since it satisfies the wildcard requirement.
|
||||
* ii. If the domain is normal (no wildcard) and we don't have anything after the next dot, then
|
||||
* currentDomain is a valid TLD
|
||||
* iii. If the entry we matched is an exception case, then the base domain is the part after the next dot
|
||||
*
|
||||
* On the next run through the loop, we set the new domain to check as the part after the next dot,
|
||||
* update the next dot reference to be the string after the new next dot, and check the TLD entries again.
|
||||
* If we reach the end of the host (nextDot = nil) and we haven't found anything, then we've hit the
|
||||
* top domain level so we use it by default.
|
||||
*/
|
||||
|
||||
let tokens = host.components(separatedBy: ".")
|
||||
let tokenCount = tokens.count
|
||||
var suffix: String?
|
||||
var previousDomain: String? = nil
|
||||
var currentDomain: String = host
|
||||
|
||||
for offset in 0..<tokenCount {
|
||||
// Store the offset for use outside of this scope so we can add additional parts if needed
|
||||
let nextDot: String? = offset + 1 < tokenCount ? tokens[offset + 1..<tokenCount].joined(separator: ".") : nil
|
||||
|
||||
if let entry = etldEntries?[currentDomain] {
|
||||
if entry.isWild && (previousDomain != nil) {
|
||||
suffix = previousDomain
|
||||
break
|
||||
} else if entry.isNormal || (nextDot == nil) {
|
||||
suffix = currentDomain
|
||||
break
|
||||
} else if entry.isException {
|
||||
suffix = nextDot
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
previousDomain = currentDomain
|
||||
if let nextDot = nextDot {
|
||||
currentDomain = nextDot
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var baseDomain: String?
|
||||
if additionalPartCount > 0 {
|
||||
if let suffix = suffix {
|
||||
// Take out the public suffixed and add in the additional parts we want.
|
||||
let literalFromEnd: NSString.CompareOptions = [NSString.CompareOptions.literal, // Match the string exactly.
|
||||
NSString.CompareOptions.backwards, // Search from the end.
|
||||
NSString.CompareOptions.anchored] // Stick to the end.
|
||||
let suffixlessHost = host.replacingOccurrences(of: suffix, with: "", options: literalFromEnd, range: nil)
|
||||
let suffixlessTokens = suffixlessHost.components(separatedBy: ".").filter { $0 != "" }
|
||||
let maxAdditionalCount = max(0, suffixlessTokens.count - additionalPartCount)
|
||||
let additionalParts = suffixlessTokens[maxAdditionalCount..<suffixlessTokens.count]
|
||||
let partsString = additionalParts.joined(separator: ".")
|
||||
baseDomain = [partsString, suffix].joined(separator: ".")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
baseDomain = suffix
|
||||
}
|
||||
|
||||
return baseDomain
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URLProtectionSpace {
|
||||
|
||||
public func urlString() -> String {
|
||||
// If our host is empty, return nothing since it doesn't make sense to add the scheme or port.
|
||||
guard !host.isEmpty else {
|
||||
return ""
|
||||
}
|
||||
|
||||
var urlString: String
|
||||
if let p = `protocol` {
|
||||
urlString = "\(p)://\(host)"
|
||||
} else {
|
||||
urlString = host
|
||||
}
|
||||
|
||||
// Check for non-standard ports
|
||||
if port != 0 && port != 443 && port != 80 {
|
||||
urlString += ":\(port)"
|
||||
}
|
||||
|
||||
return urlString
|
||||
}
|
||||
}
|
||||
18
mobile/ios/Shared/Extensions/OptionalExtensions.swift
Normal file
18
mobile/ios/Shared/Extensions/OptionalExtensions.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
/* A smarter ?? operator which allows the left hand/right hand arguments to not be
|
||||
* the same type. This is useful where we want to print the string representation
|
||||
* of an optional value that is not a string but want to return a string value when
|
||||
* a value is absent.
|
||||
*
|
||||
* For more informatin, check out Oleb's post:
|
||||
* https://oleb.net/blog/2016/12/optionals-string-interpolation/ */
|
||||
|
||||
infix operator ???: NilCoalescingPrecedence
|
||||
public func ???<T>(optional: T?, defaultValue: @autoclosure () -> String) -> String {
|
||||
return optional.map { String(describing: $0) } ?? defaultValue()
|
||||
}
|
||||
84
mobile/ios/Shared/Extensions/SetExtensions.swift
Normal file
84
mobile/ios/Shared/Extensions/SetExtensions.swift
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension SetIterator {
|
||||
mutating func take(_ n: Int) -> [Element]? {
|
||||
precondition(n >= 0)
|
||||
|
||||
if n == 0 {
|
||||
return []
|
||||
}
|
||||
|
||||
var count: Int = 0
|
||||
var out: [Element] = []
|
||||
|
||||
while count < n {
|
||||
count += 1
|
||||
guard let val = self.next() else {
|
||||
if out.isEmpty {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
out.append(val)
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
public extension Set {
|
||||
func withSubsetsOfSize(_ n: Int, f: (Set<Iterator.Element>) throws -> Void) rethrows {
|
||||
precondition(n > 0)
|
||||
|
||||
if self.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
if n > self.count {
|
||||
try f(self)
|
||||
return
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
try self.forEach { try f(Set([$0])) }
|
||||
return
|
||||
}
|
||||
|
||||
var generator = self.makeIterator()
|
||||
while let next = generator.take(n) {
|
||||
if !next.isEmpty {
|
||||
try f(Set(next))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func subsetsOfSize(_ n: Int) -> [Set<Iterator.Element>] {
|
||||
precondition(n > 0)
|
||||
|
||||
if self.isEmpty {
|
||||
return []
|
||||
}
|
||||
|
||||
if n > self.count {
|
||||
return [self]
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
// Special case.
|
||||
return self.map({ Set([$0]) })
|
||||
}
|
||||
|
||||
var generator = self.makeIterator()
|
||||
var out: [Set<Iterator.Element>] = []
|
||||
out.reserveCapacity(self.count / n)
|
||||
while let next = generator.take(n) {
|
||||
if !next.isEmpty {
|
||||
out.append(Set(next))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
107
mobile/ios/Shared/Extensions/StringExtensions.swift
Normal file
107
mobile/ios/Shared/Extensions/StringExtensions.swift
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
public func startsWith(_ other: String) -> Bool {
|
||||
// rangeOfString returns nil if other is empty, destroying the analogy with (ordered) sets.
|
||||
if other.isEmpty {
|
||||
return true
|
||||
}
|
||||
if let range = self.range(of: other,
|
||||
options: NSString.CompareOptions.anchored) {
|
||||
return range.lowerBound == self.startIndex
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func endsWith(_ other: String) -> Bool {
|
||||
// rangeOfString returns nil if other is empty, destroying the analogy with (ordered) sets.
|
||||
if other.isEmpty {
|
||||
return true
|
||||
}
|
||||
if let range = self.range(of: other,
|
||||
options: [NSString.CompareOptions.anchored, NSString.CompareOptions.backwards]) {
|
||||
return range.upperBound == self.endIndex
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func escape() -> String? {
|
||||
// We can't guaruntee that strings have a valid string encoding, as this is an entry point for tainted data,
|
||||
// we should be very careful about forcefully dereferencing optional types.
|
||||
// https://stackoverflow.com/questions/33558933/why-is-the-return-value-of-string-addingpercentencoding-optional#33558934
|
||||
let queryItemDividers = CharacterSet(charactersIn: "?=&")
|
||||
let allowedEscapes = CharacterSet.urlQueryAllowed.symmetricDifference(queryItemDividers)
|
||||
return self.addingPercentEncoding(withAllowedCharacters: allowedEscapes)
|
||||
}
|
||||
|
||||
func unescape() -> String? {
|
||||
return self.removingPercentEncoding
|
||||
}
|
||||
|
||||
/**
|
||||
Ellipsizes a String only if it's longer than `maxLength`
|
||||
|
||||
"ABCDEF".ellipsize(4)
|
||||
// "AB…EF"
|
||||
|
||||
:param: maxLength The maximum length of the String.
|
||||
|
||||
:returns: A String with `maxLength` characters or less
|
||||
*/
|
||||
func ellipsize(maxLength: Int) -> String {
|
||||
if (maxLength >= 2) && (self.characters.count > maxLength) {
|
||||
let index1 = self.characters.index(self.startIndex, offsetBy: (maxLength + 1) / 2) // `+ 1` has the same effect as an int ceil
|
||||
let index2 = self.characters.index(self.endIndex, offsetBy: maxLength / -2)
|
||||
|
||||
return self.substring(to: index1) + "…\u{2060}" + self.substring(from: index2)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
private var stringWithAdditionalEscaping: String {
|
||||
return self.replacingOccurrences(of: "|", with: "%7C", options: NSString.CompareOptions(), range: nil)
|
||||
}
|
||||
|
||||
public var asURL: URL? {
|
||||
// Firefox and NSURL disagree about the valid contents of a URL.
|
||||
// Let's escape | for them.
|
||||
// We'd love to use one of the more sophisticated CFURL* or NSString.* functions, but
|
||||
// none seem to be quite suitable.
|
||||
return URL(string: self) ??
|
||||
URL(string: self.stringWithAdditionalEscaping)
|
||||
}
|
||||
|
||||
/// Returns a new string made by removing the leading String characters contained
|
||||
/// in a given character set.
|
||||
public func stringByTrimmingLeadingCharactersInSet(_ set: CharacterSet) -> String {
|
||||
var trimmed = self
|
||||
while trimmed.rangeOfCharacter(from: set)?.lowerBound == trimmed.startIndex {
|
||||
trimmed.remove(at: trimmed.startIndex)
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
/// Adds a newline at the closest space from the middle of a string.
|
||||
/// Example turning "Mark as Read" into "Mark as\n Read"
|
||||
public func stringSplitWithNewline() -> String {
|
||||
let mid = self.characters.count/2
|
||||
|
||||
let arr: [Int] = self.characters.indices.flatMap {
|
||||
if self.characters[$0] == " " {
|
||||
return self.distance(from: startIndex, to: $0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
guard let closest = arr.enumerated().min(by: { abs($0.1 - mid) < abs($1.1 - mid) }) else {
|
||||
return self
|
||||
}
|
||||
var newString = self
|
||||
newString.insert("\n", at: newString.characters.index(newString.characters.startIndex, offsetBy: closest.element))
|
||||
return newString
|
||||
}
|
||||
}
|
||||
31
mobile/ios/Shared/Extensions/UIColorExtensions.swift
Normal file
31
mobile/ios/Shared/Extensions/UIColorExtensions.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private struct Color {
|
||||
var red: CGFloat
|
||||
var green: CGFloat
|
||||
var blue: CGFloat
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
/**
|
||||
* Initializes and returns a color object for the given RGB hex integer.
|
||||
*/
|
||||
public convenience init(rgb: Int) {
|
||||
self.init(
|
||||
red: CGFloat((rgb & 0xFF0000) >> 16) / 255.0,
|
||||
green: CGFloat((rgb & 0x00FF00) >> 8) / 255.0,
|
||||
blue: CGFloat((rgb & 0x0000FF) >> 0) / 255.0,
|
||||
alpha: 1)
|
||||
}
|
||||
|
||||
public convenience init(colorString: String) {
|
||||
var colorInt: UInt32 = 0
|
||||
Scanner(string: colorString).scanHexInt32(&colorInt)
|
||||
self.init(rgb: (Int) (colorInt))
|
||||
}
|
||||
}
|
||||
83
mobile/ios/Shared/Extensions/UIImageExtensions.swift
Normal file
83
mobile/ios/Shared/Extensions/UIImageExtensions.swift
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SDWebImage
|
||||
|
||||
private let imageLock = NSLock()
|
||||
|
||||
extension UIImage {
|
||||
/// Despite docs that say otherwise, UIImage(data: NSData) isn't thread-safe (see bug 1223132).
|
||||
/// As a workaround, synchronize access to this initializer.
|
||||
/// This fix requires that you *always* use this over UIImage(data: NSData)!
|
||||
public static func imageFromDataThreadSafe(_ data: Data) -> UIImage? {
|
||||
imageLock.lock()
|
||||
let image = UIImage(data: data)
|
||||
imageLock.unlock()
|
||||
return image
|
||||
}
|
||||
|
||||
/// Generates a UIImage from GIF data by calling out to SDWebImage. The latter in turn uses UIImage(data: NSData)
|
||||
/// in certain cases so we have to synchronize calls (see bug 1223132).
|
||||
public static func imageFromGIFDataThreadSafe(_ data: Data) -> UIImage? {
|
||||
imageLock.lock()
|
||||
let image = UIImage.sd_animatedGIF(with: data)
|
||||
imageLock.unlock()
|
||||
return image
|
||||
}
|
||||
|
||||
public static func dataIsGIF(_ data: Data) -> Bool {
|
||||
guard data.count > 3 else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Look for "GIF" header to identify GIF images
|
||||
var header = [UInt8](repeating: 0, count: 3)
|
||||
data.copyBytes(to: &header, count: 3 * MemoryLayout<UInt8>.size)
|
||||
return header == [0x47, 0x49, 0x46]
|
||||
}
|
||||
|
||||
public static func createWithColor(_ size: CGSize, color: UIColor) -> UIImage {
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
let rect = CGRect(origin: CGPoint.zero, size: size)
|
||||
color.setFill()
|
||||
context!.fill(rect)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return image!
|
||||
}
|
||||
|
||||
public func createScaled(_ size: CGSize) -> UIImage {
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0)
|
||||
draw(in: CGRect(origin: CGPoint(x: 0, y: 0), size: size))
|
||||
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return scaledImage!
|
||||
}
|
||||
|
||||
public static func templateImageNamed(_ name: String) -> UIImage? {
|
||||
return UIImage(named: name)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
|
||||
// TESTING ONLY: not for use in release/production code.
|
||||
// PNG comparison can return false negatives, be very careful using for non-equal comparison.
|
||||
// PNG comparison requires UIImages to be constructed the same way in order for the metadata block to match,
|
||||
// this function ensures that.
|
||||
//
|
||||
// This can be verified with this code:
|
||||
// let image = UIImage(named: "fxLogo")!
|
||||
// let data = UIImagePNGRepresentation(image)!
|
||||
// assert(data != UIImagePNGRepresentation(UIImage(data: data)!))
|
||||
@available(*, deprecated, message: "use only in testing code")
|
||||
public func isStrictlyEqual(to other: UIImage) -> Bool {
|
||||
// Must use same constructor for PNG metadata block to be the same.
|
||||
let imageA = UIImage(data: UIImagePNGRepresentation(self)!)!
|
||||
let imageB = UIImage(data: UIImagePNGRepresentation(other)!)!
|
||||
let dataA = UIImagePNGRepresentation(imageA)!
|
||||
let dataB = UIImagePNGRepresentation(imageB)!
|
||||
return dataA == dataB
|
||||
}
|
||||
}
|
||||
14
mobile/ios/Shared/Extensions/URLRequestExtensions.swift
Normal file
14
mobile/ios/Shared/Extensions/URLRequestExtensions.swift
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension URLRequest {
|
||||
public enum Method: String {
|
||||
case get = "GET"
|
||||
case post = "POST"
|
||||
case delete = "DELETE"
|
||||
case put = "PUT"
|
||||
}
|
||||
}
|
||||
17
mobile/ios/Shared/FSUtils.h
Normal file
17
mobile/ios/Shared/FSUtils.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@import Foundation;
|
||||
|
||||
@interface FSUtils : NSObject
|
||||
|
||||
/**
|
||||
* Grabs all open file descriptions and returns them in a key-value dictionary where
|
||||
* the key is the descriptor # and the value being the filename.
|
||||
*
|
||||
* @return Dictionary of open file descriptors.
|
||||
*/
|
||||
+ (NSDictionary<NSNumber *, NSString *> * _Nonnull)openFileDescriptors;
|
||||
|
||||
@end
|
||||
42
mobile/ios/Shared/FSUtils.m
Normal file
42
mobile/ios/Shared/FSUtils.m
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/. */
|
||||
|
||||
#import "FSUtils.h"
|
||||
|
||||
#import <sys/types.h>
|
||||
#import <fcntl.h>
|
||||
#import <errno.h>
|
||||
#import <sys/param.h>
|
||||
|
||||
@implementation FSUtils
|
||||
|
||||
+ (NSDictionary<NSNumber *, NSString *> * _Nonnull)openFileDescriptors
|
||||
{
|
||||
int flags;
|
||||
int fd;
|
||||
char buf[MAXPATHLEN+1] ;
|
||||
int n = 1 ;
|
||||
NSMutableDictionary *dict = [@{} mutableCopy];
|
||||
|
||||
for (fd = 0; fd < (int) FD_SETSIZE; fd++) {
|
||||
errno = 0;
|
||||
flags = fcntl(fd, F_GETFD, 0);
|
||||
if (flags == -1 && errno) {
|
||||
if (errno != EBADF) {
|
||||
return @{};
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
fcntl(fd , F_GETPATH, buf ) ;
|
||||
|
||||
dict[@(fd)] = [NSString stringWithCString:buf encoding:NSUTF8StringEncoding];
|
||||
++n ;
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
83
mobile/ios/Shared/FeatureSwitch.swift
Normal file
83
mobile/ios/Shared/FeatureSwitch.swift
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Steadily growing set of feature switches controlling access to features by populations of Release users.
|
||||
open class FeatureSwitches {
|
||||
}
|
||||
|
||||
/// Small class to allow a percentage of users to access a given feature.
|
||||
/// It is deliberately low tech, and is not remotely changeable.
|
||||
/// Randomized bucketing is only applied when the app's release build channel.
|
||||
open class FeatureSwitch {
|
||||
let featureID: String
|
||||
let buildChannel: AppBuildChannel
|
||||
let nonChannelValue: Bool
|
||||
let percentage: Int
|
||||
fileprivate let switchKey: String
|
||||
init(named featureID: String, _ value: Bool = true, allowPercentage percentage: Int, buildChannel: AppBuildChannel = .release) {
|
||||
self.featureID = featureID
|
||||
self.percentage = percentage
|
||||
self.buildChannel = buildChannel
|
||||
self.nonChannelValue = value
|
||||
self.switchKey = "feature_switches.\(self.featureID)"
|
||||
}
|
||||
|
||||
/// Is this user a member of the bucket that is allowed to use this feature.
|
||||
/// Bucketing is decided with the hash of a UUID, which is randomly generated and cached
|
||||
/// in the preferences.
|
||||
/// This gives us stable properties across restarts and new releases.
|
||||
open func isMember(_ prefs: Prefs) -> Bool {
|
||||
// Only use bucketing if we're in the correct build channel, and feature flag is true.
|
||||
guard buildChannel == AppConstants.BuildChannel, nonChannelValue else {
|
||||
return nonChannelValue
|
||||
}
|
||||
|
||||
// Check if this feature has been enabled by the user
|
||||
let key = "\(self.switchKey).enabled"
|
||||
if let isEnabled = prefs.boolForKey(key) {
|
||||
return isEnabled
|
||||
}
|
||||
|
||||
return lowerCaseS(prefs) < self.percentage
|
||||
}
|
||||
|
||||
/// Is this user always a member of the test set, whatever the percentage probability?
|
||||
/// This _only_ tests the probabilities, not the other conditions.
|
||||
open func alwaysMembership(_ prefs: Prefs) -> Bool {
|
||||
return lowerCaseS(prefs) == 99
|
||||
}
|
||||
|
||||
/// Reset the random component of this switch (`lowerCaseS`). This is primarily useful for testing.
|
||||
open func resetMembership(_ prefs: Prefs) {
|
||||
let uuidKey = "\(self.switchKey).uuid"
|
||||
prefs.removeObjectForKey(uuidKey)
|
||||
}
|
||||
|
||||
// If the set of all possible values the switch can be in is `S` (integers between 0 and 99)
|
||||
// then the specific value is `s`.
|
||||
// We use this to compare with the probability of membership.
|
||||
fileprivate func lowerCaseS(_ prefs: Prefs) -> Int {
|
||||
// Use a branch of the prefs.
|
||||
let uuidKey = "\(self.switchKey).uuid"
|
||||
|
||||
let uuidString: String
|
||||
if let string = prefs.stringForKey(uuidKey) {
|
||||
uuidString = string
|
||||
} else {
|
||||
uuidString = UUID().uuidString
|
||||
prefs.setString(uuidString, forKey: uuidKey)
|
||||
}
|
||||
|
||||
let hash = abs(uuidString.hashValue)
|
||||
|
||||
return hash % 100
|
||||
}
|
||||
|
||||
open func setMembership(_ isEnabled: Bool, for prefs: Prefs) {
|
||||
let key = "\(self.switchKey).enabled"
|
||||
prefs.setBool(isEnabled, forKey: key)
|
||||
}
|
||||
}
|
||||
253
mobile/ios/Shared/Functions.swift
Normal file
253
mobile/ios/Shared/Functions.swift
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import SwiftyJSON
|
||||
|
||||
// Pipelining.
|
||||
precedencegroup PipelinePrecedence {
|
||||
associativity: left
|
||||
}
|
||||
infix operator |> : PipelinePrecedence
|
||||
|
||||
public func |> <T, U>(x: T, f: (T) -> U) -> U {
|
||||
return f(x)
|
||||
}
|
||||
|
||||
// Basic currying.
|
||||
public func curry<A, B>(_ f: @escaping (A) -> B) -> (A) -> B {
|
||||
return { a in
|
||||
return f(a)
|
||||
}
|
||||
}
|
||||
|
||||
public func curry<A, B, C>(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
|
||||
return { a in
|
||||
return { b in
|
||||
return f(a, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func curry<A, B, C, D>(_ f: @escaping (A, B, C) -> D) -> (A) -> (B) -> (C) -> D {
|
||||
return { a in
|
||||
return { b in
|
||||
return { c in
|
||||
return f(a, b, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func curry<A, B, C, D, E>(_ f: @escaping (A, B, C, D) -> E) -> (A, B, C) -> (D) -> E {
|
||||
return { (a, b, c) in
|
||||
return { d in
|
||||
return f(a, b, c, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function composition.
|
||||
infix operator •
|
||||
|
||||
public func •<T, U, V>(f: @escaping (T) -> U, g: @escaping (U) -> V) -> (T) -> V {
|
||||
return { t in
|
||||
return g(f(t))
|
||||
}
|
||||
}
|
||||
public func •<T, V>(f: @escaping (T) -> Void, g: @escaping () -> V) -> (T) -> V {
|
||||
return { t in
|
||||
f(t)
|
||||
return g()
|
||||
}
|
||||
}
|
||||
public func •<V>(f: @escaping () -> Void, g: @escaping () -> V) -> () -> V {
|
||||
return {
|
||||
f()
|
||||
return g()
|
||||
}
|
||||
}
|
||||
|
||||
// Why not simply provide an override for ==? Well, that's scary, and can accidentally recurse.
|
||||
// This is enough to catch arrays, which Swift will delegate to element-==.
|
||||
public func optArrayEqual<T: Equatable>(_ lhs: [T]?, rhs: [T]?) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.none, .none):
|
||||
return true
|
||||
case (.none, _):
|
||||
return false
|
||||
case (_, .none):
|
||||
return false
|
||||
default:
|
||||
// This delegates to Swift's own array '==', which calls T's == on each element.
|
||||
return lhs! == rhs!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array, return an array of slices of size `by` (possibly excepting the last slice).
|
||||
*
|
||||
* If `by` is longer than the input, returns a single chunk.
|
||||
* If `by` is less than 1, acts as if `by` is 1.
|
||||
* If the length of the array isn't a multiple of `by`, the final slice will
|
||||
* be smaller than `by`, but never empty.
|
||||
*
|
||||
* If the input array is empty, returns an empty array.
|
||||
*/
|
||||
|
||||
public func chunk<T>(_ arr: [T], by: Int) -> [ArraySlice<T>] {
|
||||
var result = [ArraySlice<T>]()
|
||||
var chunk = -1
|
||||
let size = max(1, by)
|
||||
for (index, elem) in arr.enumerated() {
|
||||
if index % size == 0 {
|
||||
result.append(ArraySlice<T>())
|
||||
chunk += 1
|
||||
}
|
||||
result[chunk].append(elem)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func chunkCollection<E, X, T: Collection>(_ items: T, by: Int, f: ([E]) -> [X]) -> [X] where T.Iterator.Element == E {
|
||||
assert(by >= 0)
|
||||
let max = by > 0 ? by : 1
|
||||
var i = 0
|
||||
var acc: [E] = []
|
||||
var results: [X] = []
|
||||
var iter = items.makeIterator()
|
||||
|
||||
while let item = iter.next() {
|
||||
if i >= max {
|
||||
results.append(contentsOf: f(acc))
|
||||
acc = []
|
||||
i = 0
|
||||
}
|
||||
acc.append(item)
|
||||
i += 1
|
||||
}
|
||||
|
||||
if !acc.isEmpty {
|
||||
results.append(contentsOf: f(acc))
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
public extension Sequence {
|
||||
// [T] -> (T -> K) -> [K: [T]]
|
||||
// As opposed to `groupWith` (to follow Haskell's naming), which would be
|
||||
// [T] -> (T -> K) -> [[T]]
|
||||
func groupBy<Key, Value>(_ selector: (Self.Iterator.Element) -> Key, transformer: (Self.Iterator.Element) -> Value) -> [Key: [Value]] {
|
||||
var acc: [Key: [Value]] = [:]
|
||||
for x in self {
|
||||
let k = selector(x)
|
||||
var a = acc[k] ?? []
|
||||
a.append(transformer(x))
|
||||
acc[k] = a
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
func zip<S: Sequence>(_ elems: S) -> [(Self.Iterator.Element, S.Iterator.Element)] {
|
||||
var rights = elems.makeIterator()
|
||||
return self.flatMap { lhs in
|
||||
guard let rhs = rights.next() else {
|
||||
return nil
|
||||
}
|
||||
return (lhs, rhs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func optDictionaryEqual<K, V: Equatable>(_ lhs: [K: V]?, rhs: [K: V]?) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.none, .none):
|
||||
return true
|
||||
case (.none, _):
|
||||
return false
|
||||
case (_, .none):
|
||||
return false
|
||||
default:
|
||||
return lhs! == rhs!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return members of `a` that aren't nil, changing the type of the sequence accordingly.
|
||||
*/
|
||||
public func optFilter<T>(_ a: [T?]) -> [T] {
|
||||
return a.flatMap { $0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new map with only key-value pairs that have a non-nil value.
|
||||
*/
|
||||
public func optFilter<K, V>(_ source: [K: V?]) -> [K: V] {
|
||||
var m = [K: V]()
|
||||
for (k, v) in source {
|
||||
if let v = v {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a function over the values of a map.
|
||||
*/
|
||||
public func mapValues<K, T, U>(_ source: [K: T], f: ((T) -> U)) -> [K: U] {
|
||||
var m = [K: U]()
|
||||
for (k, v) in source {
|
||||
m[k] = f(v)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
public func findOneValue<K, V>(_ map: [K: V], f: (V) -> Bool) -> V? {
|
||||
for v in map.values {
|
||||
if f(v) {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a JSON array, returning the String elements as an array.
|
||||
* It's usually convenient for this to accept an optional.
|
||||
*/
|
||||
public func jsonsToStrings(_ arr: [JSON]?) -> [String]? {
|
||||
return arr?.flatMap { $0.stringValue }
|
||||
}
|
||||
|
||||
// Encapsulate a callback in a way that we can use it with NSTimer.
|
||||
private class Callback {
|
||||
private let handler:() -> Void
|
||||
|
||||
init(handler:@escaping () -> Void) {
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
@objc
|
||||
func go() {
|
||||
handler()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Taken from http://stackoverflow.com/questions/27116684/how-can-i-debounce-a-method-call
|
||||
* Allows creating a block that will fire after a delay. Resets the timer if called again before the delay expires.
|
||||
**/
|
||||
public func debounce(_ delay: TimeInterval, action:@escaping () -> Void) -> () -> Void {
|
||||
let callback = Callback(handler: action)
|
||||
var timer: Timer?
|
||||
|
||||
return {
|
||||
// If calling again, invalidate the last timer.
|
||||
if let timer = timer {
|
||||
timer.invalidate()
|
||||
}
|
||||
timer = Timer(timeInterval: delay, target: callback, selector: #selector(Callback.go), userInfo: nil, repeats: false)
|
||||
RunLoop.current.add(timer!, forMode: RunLoopMode.defaultRunLoopMode)
|
||||
}
|
||||
}
|
||||
126
mobile/ios/Shared/KeyboardHelper.swift
Normal file
126
mobile/ios/Shared/KeyboardHelper.swift
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* The keyboard state at the time of notification.
|
||||
*/
|
||||
public struct KeyboardState {
|
||||
public let animationDuration: Double
|
||||
public let animationCurve: UIViewAnimationCurve
|
||||
private let userInfo: [AnyHashable: Any]
|
||||
|
||||
fileprivate init(_ userInfo: [AnyHashable: Any]) {
|
||||
self.userInfo = userInfo
|
||||
animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as! Double
|
||||
// HACK: UIViewAnimationCurve doesn't expose the keyboard animation used (curveValue = 7),
|
||||
// so UIViewAnimationCurve(rawValue: curveValue) returns nil. As a workaround, get a
|
||||
// reference to an EaseIn curve, then change the underlying pointer data with that ref.
|
||||
var curve = UIViewAnimationCurve.easeIn
|
||||
if let curveValue = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? Int {
|
||||
NSNumber(value: curveValue as Int).getValue(&curve)
|
||||
}
|
||||
self.animationCurve = curve
|
||||
}
|
||||
|
||||
/// Return the height of the keyboard that overlaps with the specified view. This is more
|
||||
/// accurate than simply using the height of UIKeyboardFrameBeginUserInfoKey since for example
|
||||
/// on iPad the overlap may be partial or if an external keyboard is attached, the intersection
|
||||
/// height will be zero. (Even if the height of the *invisible* keyboard will look normal!)
|
||||
public func intersectionHeightForView(_ view: UIView) -> CGFloat {
|
||||
if let keyboardFrameValue = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue {
|
||||
let keyboardFrame = keyboardFrameValue.cgRectValue
|
||||
let convertedKeyboardFrame = view.convert(keyboardFrame, from: nil)
|
||||
let intersection = convertedKeyboardFrame.intersection(view.bounds)
|
||||
return intersection.size.height
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
public protocol KeyboardHelperDelegate: class {
|
||||
func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillShowWithState state: KeyboardState)
|
||||
func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardDidShowWithState state: KeyboardState)
|
||||
func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillHideWithState state: KeyboardState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience class for observing keyboard state.
|
||||
*/
|
||||
open class KeyboardHelper: NSObject {
|
||||
open var currentState: KeyboardState?
|
||||
|
||||
fileprivate var delegates = [WeakKeyboardDelegate]()
|
||||
|
||||
open class var defaultHelper: KeyboardHelper {
|
||||
struct Singleton {
|
||||
static let instance = KeyboardHelper()
|
||||
}
|
||||
return Singleton.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts monitoring the keyboard state.
|
||||
*/
|
||||
open func startObserving() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(KeyboardHelper.SELkeyboardWillShow(_:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(KeyboardHelper.SELkeyboardDidShow(_:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(KeyboardHelper.SELkeyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a delegate to the helper.
|
||||
* Delegates are weakly held.
|
||||
*/
|
||||
open func addDelegate(_ delegate: KeyboardHelperDelegate) {
|
||||
// Reuse any existing slots that have been deallocated.
|
||||
for weakDelegate in delegates where weakDelegate.delegate == nil {
|
||||
weakDelegate.delegate = delegate
|
||||
return
|
||||
}
|
||||
|
||||
delegates.append(WeakKeyboardDelegate(delegate))
|
||||
}
|
||||
|
||||
func SELkeyboardWillShow(_ notification: Notification) {
|
||||
if let userInfo = notification.userInfo {
|
||||
currentState = KeyboardState(userInfo)
|
||||
for weakDelegate in delegates {
|
||||
weakDelegate.delegate?.keyboardHelper(self, keyboardWillShowWithState: currentState!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SELkeyboardDidShow(_ notification: Notification) {
|
||||
if let userInfo = notification.userInfo {
|
||||
currentState = KeyboardState(userInfo)
|
||||
for weakDelegate in delegates {
|
||||
weakDelegate.delegate?.keyboardHelper(self, keyboardDidShowWithState: currentState!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SELkeyboardWillHide(_ notification: Notification) {
|
||||
if let userInfo = notification.userInfo {
|
||||
currentState = KeyboardState(userInfo)
|
||||
for weakDelegate in delegates {
|
||||
weakDelegate.delegate?.keyboardHelper(self, keyboardWillHideWithState: currentState!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class WeakKeyboardDelegate {
|
||||
weak var delegate: KeyboardHelperDelegate?
|
||||
|
||||
init(_ delegate: KeyboardHelperDelegate) {
|
||||
self.delegate = delegate
|
||||
}
|
||||
}
|
||||
65
mobile/ios/Shared/KeychainCache.swift
Normal file
65
mobile/ios/Shared/KeychainCache.swift
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import XCGLogger
|
||||
import SwiftKeychainWrapper
|
||||
import SwiftyJSON
|
||||
|
||||
private let log = Logger.keychainLogger
|
||||
|
||||
public protocol JSONLiteralConvertible {
|
||||
func asJSON() -> JSON
|
||||
}
|
||||
|
||||
open class KeychainCache<T: JSONLiteralConvertible> {
|
||||
open let branch: String
|
||||
open let label: String
|
||||
|
||||
open var value: T? {
|
||||
didSet {
|
||||
checkpoint()
|
||||
}
|
||||
}
|
||||
|
||||
public init(branch: String, label: String, value: T?) {
|
||||
self.branch = branch
|
||||
self.label = label
|
||||
self.value = value
|
||||
}
|
||||
|
||||
open class func fromBranch(_ branch: String, withLabel label: String?, withDefault defaultValue: T? = nil, factory: (JSON) -> T?) -> KeychainCache<T> {
|
||||
if let l = label {
|
||||
let key = "\(branch).\(l)"
|
||||
KeychainWrapper.sharedAppContainerKeychain.ensureStringItemAccessibility(.afterFirstUnlock, forKey: key)
|
||||
if let s = KeychainWrapper.sharedAppContainerKeychain.string(forKey: key) {
|
||||
if let t = factory(JSON(parseJSON: s)) {
|
||||
log.info("Read \(branch) from Keychain with label \(branch).\(l).")
|
||||
return KeychainCache(branch: branch, label: l, value: t)
|
||||
} else {
|
||||
log.warning("Found \(branch) in Keychain with label \(branch).\(l), but could not parse it.")
|
||||
}
|
||||
} else {
|
||||
log.warning("Did not find \(branch) in Keychain with label \(branch).\(l).")
|
||||
}
|
||||
} else {
|
||||
log.warning("Did not find \(branch) label in Keychain.")
|
||||
}
|
||||
// Fall through to missing.
|
||||
log.warning("Failed to read \(branch) from Keychain.")
|
||||
let label = label ?? Bytes.generateGUID()
|
||||
return KeychainCache(branch: branch, label: label, value: defaultValue)
|
||||
}
|
||||
|
||||
open func checkpoint() {
|
||||
log.info("Storing \(self.branch) in Keychain with label \(self.branch).\(self.label).")
|
||||
// TODO: PII logging.
|
||||
if let value = value,
|
||||
let jsonString = value.asJSON().stringValue() {
|
||||
KeychainWrapper.sharedAppContainerKeychain.set(jsonString, forKey: "\(branch).\(label)", withAccessibility: .afterFirstUnlock)
|
||||
} else {
|
||||
KeychainWrapper.sharedAppContainerKeychain.removeObject(forKey: "\(branch).\(label)")
|
||||
}
|
||||
}
|
||||
}
|
||||
12
mobile/ios/Shared/LaunchArguments.swift
Normal file
12
mobile/ios/Shared/LaunchArguments.swift
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct LaunchArguments {
|
||||
public static let Test = "FIREFOX_TEST"
|
||||
public static let SkipIntro = "FIREFOX_SKIP_INTRO"
|
||||
public static let SkipWhatsNew = "FIREFOX_SKIP_WHATS_NEW"
|
||||
public static let ClearProfile = "FIREFOX_CLEAR_PROFILE"
|
||||
}
|
||||
33
mobile/ios/Shared/Loader.swift
Normal file
33
mobile/ios/Shared/Loader.swift
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* Interface for listening to Loader updates.
|
||||
*/
|
||||
public protocol LoaderListener: class {
|
||||
associatedtype T
|
||||
func loader(dataLoaded data: T)
|
||||
}
|
||||
|
||||
/**
|
||||
* Base implementation for a "push" data model.
|
||||
* Interested clients add themselves as listeners for data changes.
|
||||
*/
|
||||
open class Loader<T, ListenerType: LoaderListener> where T == ListenerType.T {
|
||||
private let listeners = WeakList<ListenerType>()
|
||||
|
||||
public init() {}
|
||||
|
||||
open func addListener(_ listener: ListenerType) {
|
||||
listeners.insert(listener)
|
||||
}
|
||||
|
||||
open func load(_ data: T) {
|
||||
for listener in listeners {
|
||||
listener.loader(dataLoaded: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
96
mobile/ios/Shared/Logger.swift
Normal file
96
mobile/ios/Shared/Logger.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 XCGLogger
|
||||
|
||||
public struct Logger {}
|
||||
|
||||
// MARK: - Singleton Logger Instances
|
||||
public extension Logger {
|
||||
static let logPII = false
|
||||
|
||||
/// Logger used for recording happenings with Sync, Accounts, Providers, Storage, and Profiles
|
||||
static let syncLogger = RollingFileLogger(filenameRoot: "sync", logDirectoryPath: Logger.logFileDirectoryPath())
|
||||
|
||||
/// Logger used for recording frontend/browser happenings
|
||||
static let browserLogger = RollingFileLogger(filenameRoot: "browser", logDirectoryPath: Logger.logFileDirectoryPath())
|
||||
|
||||
/// Logger used for recording interactions with the keychain
|
||||
static let keychainLogger: XCGLogger = Logger.fileLoggerWithName("keychain")
|
||||
|
||||
/// Logger used for logging database errors such as corruption
|
||||
static let corruptLogger: RollingFileLogger = {
|
||||
let logger = RollingFileLogger(filenameRoot: "corruptLogger", logDirectoryPath: Logger.logFileDirectoryPath())
|
||||
logger.newLogWithDate(Date())
|
||||
return logger
|
||||
}()
|
||||
|
||||
/**
|
||||
Return the log file directory path. If the directory doesn't exist, make sure it exist first before returning the path.
|
||||
|
||||
:returns: Directory path where log files are stored
|
||||
*/
|
||||
static func logFileDirectoryPath() -> String? {
|
||||
if let cacheDir = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first {
|
||||
let logDir = "\(cacheDir)/Logs"
|
||||
if !FileManager.default.fileExists(atPath: logDir) {
|
||||
do {
|
||||
try FileManager.default.createDirectory(atPath: logDir, withIntermediateDirectories: false, attributes: nil)
|
||||
return logDir
|
||||
} catch _ as NSError {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return logDir
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
static private func fileLoggerWithName(_ name: String) -> XCGLogger {
|
||||
let log = XCGLogger()
|
||||
if let logFileURL = urlForLogNamed(name) {
|
||||
let fileDestination = FileDestination(
|
||||
owner: log,
|
||||
writeToFile: logFileURL.absoluteString,
|
||||
identifier: "com.mozilla.firefox.filelogger.\(name)"
|
||||
)
|
||||
log.add(destination: fileDestination)
|
||||
}
|
||||
return log
|
||||
}
|
||||
|
||||
static private func urlForLogNamed(_ name: String) -> URL? {
|
||||
guard let logDir = Logger.logFileDirectoryPath() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return URL(string: "\(logDir)/\(name).log")
|
||||
}
|
||||
|
||||
/**
|
||||
Grabs all of the configured logs that write to disk and returns them in NSData format along with their
|
||||
associated filename.
|
||||
|
||||
- returns: Tuples of filenames to each file's contexts in a NSData object
|
||||
*/
|
||||
static func diskLogFilenamesAndData() throws -> [(String, Data?)] {
|
||||
var filenamesAndURLs = [(String, URL)]()
|
||||
filenamesAndURLs.append(("browser", urlForLogNamed("browser")!))
|
||||
filenamesAndURLs.append(("keychain", urlForLogNamed("keychain")!))
|
||||
|
||||
// Grab all sync log files
|
||||
do {
|
||||
filenamesAndURLs += try syncLogger.logFilenamesAndURLs()
|
||||
filenamesAndURLs += try corruptLogger.logFilenamesAndURLs()
|
||||
filenamesAndURLs += try browserLogger.logFilenamesAndURLs()
|
||||
} catch _ {
|
||||
}
|
||||
|
||||
return filenamesAndURLs.map { ($0, try? Data(contentsOf: URL(fileURLWithPath: $1.absoluteString))) }
|
||||
}
|
||||
}
|
||||
|
||||
31
mobile/ios/Shared/NotificationConstants.swift
Normal file
31
mobile/ios/Shared/NotificationConstants.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
public let NotificationDataLoginDidChange = Notification.Name("Data:Login:DidChange")
|
||||
|
||||
// add a property to allow the observation of firefox accounts
|
||||
public let NotificationFirefoxAccountChanged = Notification.Name("FirefoxAccountChangedNotification")
|
||||
|
||||
public let NotificationFirefoxAccountProfileChanged = Notification.Name("NotificationFirefoxAccountProfileChanged")
|
||||
|
||||
public let NotificationFirefoxAccountDeviceRegistrationUpdated = Notification.Name("FirefoxAccountDeviceRegistrationUpdated")
|
||||
|
||||
public let NotificationPrivateDataClearedHistory = Notification.Name("PrivateDataClearedHistoryNotification")
|
||||
|
||||
// Fired when the user finishes navigating to a page and the location has changed
|
||||
public let NotificationOnLocationChange = Notification.Name("OnLocationChange")
|
||||
|
||||
// Fired when a the page metadata extraction script has completed and is being passed back to the native client
|
||||
public let NotificationOnPageMetadataFetched = Notification.Name("OnPageMetadataFetched")
|
||||
|
||||
// Fired when the login synchronizer has finished applying remote changes
|
||||
public let NotificationDataRemoteLoginChangesWereApplied = Notification.Name("NotificationDataRemoteLoginChangesWereApplied")
|
||||
|
||||
// Fired when the FxA account has been verified. This should only be fired by the FxALoginStateMachine.
|
||||
public let NotificationFirefoxAccountVerified = Notification.Name("FirefoxAccountVerifiedNotification")
|
||||
|
||||
// MARK: Notification UserInfo Keys
|
||||
public let NotificationUserInfoKeyHasSyncableAccount = Notification.Name("NotificationUserInfoKeyHasSyncableAccount")
|
||||
|
||||
public let NotificationDidRestoreSession = Notification.Name("NotificationDidRestoreSession")
|
||||
193
mobile/ios/Shared/Prefs.swift
Normal file
193
mobile/ios/Shared/Prefs.swift
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct PrefsKeys {
|
||||
public static let KeyLastRemoteTabSyncTime = "lastRemoteTabSyncTime"
|
||||
public static let KeyLastSyncFinishTime = "lastSyncFinishTime"
|
||||
public static let KeyDefaultHomePageURL = "KeyDefaultHomePageURL"
|
||||
public static let KeyNoImageModeStatus = "NoImageModeStatus"
|
||||
public static let KeyNightModeButtonIsInMenu = "NightModeButtonIsInMenuPrefKey"
|
||||
public static let KeyNightModeStatus = "NightModeStatus"
|
||||
public static let KeyMailToOption = "MailToOption"
|
||||
public static let HasFocusInstalled = "HasFocusInstalled"
|
||||
public static let HasPocketInstalled = "HasPocketInstalled"
|
||||
|
||||
//Activity Stream
|
||||
public static let KeyTopSitesCacheIsValid = "topSitesCacheIsValid"
|
||||
public static let KeyTopSitesCacheSize = "topSitesCacheSize"
|
||||
public static let KeyNewTab = "NewTabPrefKey"
|
||||
public static let ASPocketStoriesVisible = "ASPocketStoriesVisible"
|
||||
public static let ASRecentHighlightsVisible = "ASRecentHighlightsVisible"
|
||||
public static let ASBookmarkHighlightsVisible = "ASBookmarkHighlightsVisible"
|
||||
public static let ASLastInvalidation = "ASLastInvalidation"
|
||||
|
||||
public static let KeyUseCustomSyncService = "useCustomSyncService"
|
||||
public static let KeyCustomSyncToken = "customSyncTokenServer"
|
||||
public static let KeyCustomSyncProfile = "customSyncProfileServer"
|
||||
public static let KeyCustomSyncOauth = "customSyncOauthServer"
|
||||
public static let KeyCustomSyncAuth = "customSyncAuthServer"
|
||||
public static let KeyCustomSyncWeb = "customSyncWebServer"
|
||||
|
||||
}
|
||||
|
||||
public struct PrefsDefaults {
|
||||
public static let ChineseHomePageURL = "http://mobile.firefoxchina.cn/"
|
||||
public static let ChineseNewTabDefault = "HomePage"
|
||||
}
|
||||
|
||||
public protocol Prefs {
|
||||
func getBranchPrefix() -> String
|
||||
func branch(_ branch: String) -> Prefs
|
||||
func setTimestamp(_ value: Timestamp, forKey defaultName: String)
|
||||
func setLong(_ value: UInt64, forKey defaultName: String)
|
||||
func setLong(_ value: Int64, forKey defaultName: String)
|
||||
func setInt(_ value: Int32, forKey defaultName: String)
|
||||
func setString(_ value: String, forKey defaultName: String)
|
||||
func setBool(_ value: Bool, forKey defaultName: String)
|
||||
func setObject(_ value: Any?, forKey defaultName: String)
|
||||
func stringForKey(_ defaultName: String) -> String?
|
||||
func objectForKey<T: Any>(_ defaultName: String) -> T?
|
||||
func boolForKey(_ defaultName: String) -> Bool?
|
||||
func intForKey(_ defaultName: String) -> Int32?
|
||||
func timestampForKey(_ defaultName: String) -> Timestamp?
|
||||
func longForKey(_ defaultName: String) -> Int64?
|
||||
func unsignedLongForKey(_ defaultName: String) -> UInt64?
|
||||
func stringArrayForKey(_ defaultName: String) -> [String]?
|
||||
func arrayForKey(_ defaultName: String) -> [Any]?
|
||||
func dictionaryForKey(_ defaultName: String) -> [String: Any]?
|
||||
func removeObjectForKey(_ defaultName: String)
|
||||
func clearAll()
|
||||
}
|
||||
|
||||
open class MockProfilePrefs: Prefs {
|
||||
let prefix: String
|
||||
|
||||
open func getBranchPrefix() -> String {
|
||||
return self.prefix
|
||||
}
|
||||
|
||||
// Public for testing.
|
||||
open var things: NSMutableDictionary = NSMutableDictionary()
|
||||
|
||||
public init(things: NSMutableDictionary, prefix: String) {
|
||||
self.things = things
|
||||
self.prefix = prefix
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.prefix = ""
|
||||
}
|
||||
|
||||
open func branch(_ branch: String) -> Prefs {
|
||||
return MockProfilePrefs(things: self.things, prefix: self.prefix + branch + ".")
|
||||
}
|
||||
|
||||
private func name(_ name: String) -> String {
|
||||
return self.prefix + name
|
||||
}
|
||||
|
||||
open func setTimestamp(_ value: Timestamp, forKey defaultName: String) {
|
||||
self.setLong(value, forKey: defaultName)
|
||||
}
|
||||
|
||||
open func setLong(_ value: UInt64, forKey defaultName: String) {
|
||||
setObject(NSNumber(value: value as UInt64), forKey: defaultName)
|
||||
}
|
||||
|
||||
open func setLong(_ value: Int64, forKey defaultName: String) {
|
||||
setObject(NSNumber(value: value as Int64), forKey: defaultName)
|
||||
}
|
||||
|
||||
open func setInt(_ value: Int32, forKey defaultName: String) {
|
||||
things[name(defaultName)] = NSNumber(value: value as Int32)
|
||||
}
|
||||
|
||||
open func setString(_ value: String, forKey defaultName: String) {
|
||||
things[name(defaultName)] = value
|
||||
}
|
||||
|
||||
open func setBool(_ value: Bool, forKey defaultName: String) {
|
||||
things[name(defaultName)] = value
|
||||
}
|
||||
|
||||
open func setObject(_ value: Any?, forKey defaultName: String) {
|
||||
things[name(defaultName)] = value
|
||||
}
|
||||
|
||||
open func stringForKey(_ defaultName: String) -> String? {
|
||||
return things[name(defaultName)] as? String
|
||||
}
|
||||
|
||||
open func boolForKey(_ defaultName: String) -> Bool? {
|
||||
return things[name(defaultName)] as? Bool
|
||||
}
|
||||
|
||||
open func objectForKey<T: Any>(_ defaultName: String) -> T? {
|
||||
return things[name(defaultName)] as? T
|
||||
}
|
||||
|
||||
open func timestampForKey(_ defaultName: String) -> Timestamp? {
|
||||
return unsignedLongForKey(defaultName)
|
||||
}
|
||||
|
||||
open func unsignedLongForKey(_ defaultName: String) -> UInt64? {
|
||||
let num = things[name(defaultName)] as? UInt64
|
||||
if let num = num {
|
||||
return num
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
open func longForKey(_ defaultName: String) -> Int64? {
|
||||
let num = things[name(defaultName)] as? Int64
|
||||
if let num = num {
|
||||
return num
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
open func intForKey(_ defaultName: String) -> Int32? {
|
||||
let num = things[name(defaultName)] as? Int32
|
||||
if let num = num {
|
||||
return num
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
open func stringArrayForKey(_ defaultName: String) -> [String]? {
|
||||
if let arr = self.arrayForKey(defaultName) {
|
||||
if let arr = arr as? [String] {
|
||||
return arr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
open func arrayForKey(_ defaultName: String) -> [Any]? {
|
||||
let r: Any? = things.object(forKey: name(defaultName)) as Any?
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
if let arr = r as? [Any] {
|
||||
return arr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
open func dictionaryForKey(_ defaultName: String) -> [String: Any]? {
|
||||
return things.object(forKey: name(defaultName)) as? [String: Any]
|
||||
}
|
||||
|
||||
open func removeObjectForKey(_ defaultName: String) {
|
||||
self.things.removeObject(forKey: name(defaultName))
|
||||
}
|
||||
|
||||
open func clearAll() {
|
||||
let dictionary = things as! [String: Any]
|
||||
let keysToDelete: [String] = dictionary.keys.filter { $0.startsWith(self.prefix) }
|
||||
things.removeObjects(forKeys: keysToDelete)
|
||||
}
|
||||
}
|
||||
23
mobile/ios/Shared/RemoteDevices.swift
Normal file
23
mobile/ios/Shared/RemoteDevices.swift
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
public protocol RemoteDevices {
|
||||
func replaceRemoteDevices(_ remoteDevices: [RemoteDevice]) -> Success
|
||||
}
|
||||
|
||||
open class RemoteDevice {
|
||||
public let id: String?
|
||||
public let name: String
|
||||
public let type: String?
|
||||
public let isCurrentDevice: Bool
|
||||
public let lastAccessTime: Timestamp?
|
||||
|
||||
public init(id: String?, name: String, type: String?, isCurrentDevice: Bool, lastAccessTime: Timestamp?) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.isCurrentDevice = isCurrentDevice
|
||||
self.lastAccessTime = lastAccessTime
|
||||
}
|
||||
}
|
||||
115
mobile/ios/Shared/RollingFileLogger.swift
Normal file
115
mobile/ios/Shared/RollingFileLogger.swift
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import XCGLogger
|
||||
|
||||
//// A rolling file logger that saves to a different log file based on given timestamp.
|
||||
open class RollingFileLogger: XCGLogger {
|
||||
|
||||
fileprivate static let TwoMBsInBytes: Int64 = 2 * 100000
|
||||
fileprivate let sizeLimit: Int64
|
||||
fileprivate let logDirectoryPath: String?
|
||||
|
||||
let fileLogIdentifierPrefix = "com.mozilla.firefox.filelogger."
|
||||
|
||||
fileprivate static let DateFormatter: DateFormatter = {
|
||||
let formatter = Foundation.DateFormatter()
|
||||
formatter.dateFormat = "yyyyMMdd'T'HHmmssZ"
|
||||
return formatter
|
||||
}()
|
||||
|
||||
let root: String
|
||||
|
||||
public init(filenameRoot: String, logDirectoryPath: String?, sizeLimit: Int64 = TwoMBsInBytes) {
|
||||
root = filenameRoot
|
||||
self.sizeLimit = sizeLimit
|
||||
self.logDirectoryPath = logDirectoryPath
|
||||
super.init()
|
||||
}
|
||||
|
||||
/**
|
||||
Create a new log file with the given timestamp to log events into
|
||||
|
||||
:param: date Date for with to start and mark the new log file
|
||||
*/
|
||||
open func newLogWithDate(_ date: Date) {
|
||||
// Don't start a log if we don't have a valid log directory path
|
||||
if logDirectoryPath == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if let filename = filenameWithRoot(root, withDate: date) {
|
||||
remove(destinationWithIdentifier: fileLogIdentifierWithRoot(root))
|
||||
add(destination: FileDestination(owner: self, writeToFile: filename, identifier: fileLogIdentifierWithRoot(root)))
|
||||
info("Created file destination for logger with root: \(self.root) and timestamp: \(date)")
|
||||
} else {
|
||||
error("Failed to create a new log with root name: \(self.root) and timestamp: \(date)")
|
||||
}
|
||||
}
|
||||
|
||||
open func deleteOldLogsDownToSizeLimit() {
|
||||
// Check to see we haven't hit our size limit and if we did, clear out some logs to make room.
|
||||
while sizeOfAllLogFilesWithPrefix(self.root, exceedsSizeInBytes: sizeLimit) {
|
||||
deleteOldestLogWithPrefix(self.root)
|
||||
}
|
||||
}
|
||||
|
||||
open func logFilenamesAndURLs() throws -> [(String, URL)] {
|
||||
guard let logPath = logDirectoryPath else {
|
||||
return []
|
||||
}
|
||||
|
||||
let files = try FileManager.default.contentsOfDirectoryAtPath(logPath, withFilenamePrefix: root)
|
||||
return files.flatMap { filename in
|
||||
if let url = URL(string: "\(logPath)/\(filename)") {
|
||||
return (filename, url)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func deleteOldestLogWithPrefix(_ prefix: String) {
|
||||
if logDirectoryPath == nil {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let logFiles = try FileManager.default.contentsOfDirectoryAtPath(logDirectoryPath!, withFilenamePrefix: prefix)
|
||||
if let oldestLogFilename = logFiles.first {
|
||||
try FileManager.default.removeItem(atPath: "\(logDirectoryPath!)/\(oldestLogFilename)")
|
||||
}
|
||||
} catch _ as NSError {
|
||||
error("Shouldn't get here")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func sizeOfAllLogFilesWithPrefix(_ prefix: String, exceedsSizeInBytes threshold: Int64) -> Bool {
|
||||
guard let path = logDirectoryPath else {
|
||||
return false
|
||||
}
|
||||
|
||||
let logDirURL = URL(fileURLWithPath: path)
|
||||
do {
|
||||
return try FileManager.default.allocatedSizeOfDirectoryAtURL(logDirURL, forFilesPrefixedWith: prefix, isLargerThanBytes: threshold)
|
||||
} catch let errorValue as NSError {
|
||||
error("Error determining log directory size: \(errorValue)")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fileprivate func filenameWithRoot(_ root: String, withDate date: Date) -> String? {
|
||||
if let dir = logDirectoryPath {
|
||||
return "\(dir)/\(root).\(RollingFileLogger.DateFormatter.string(from: date)).log"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func fileLogIdentifierWithRoot(_ root: String) -> String {
|
||||
return "\(fileLogIdentifierPrefix).\(root)"
|
||||
}
|
||||
}
|
||||
163
mobile/ios/Shared/SentryIntegration.swift
Normal file
163
mobile/ios/Shared/SentryIntegration.swift
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import Sentry
|
||||
|
||||
public enum SentryTag: String {
|
||||
case swiftData = "SwiftData"
|
||||
case browserDB = "BrowserDB"
|
||||
case notificationService = "NotificationService"
|
||||
case unifiedTelemetry = "UnifiedTelemetry"
|
||||
case general = "General"
|
||||
case tabManager = "TabManager"
|
||||
case bookmarks = "Bookmarks"
|
||||
}
|
||||
|
||||
public class Sentry {
|
||||
public static let shared = Sentry()
|
||||
|
||||
public static var crashedLastLaunch: Bool {
|
||||
return Client.shared?.crashedLastLaunch() ?? false
|
||||
}
|
||||
|
||||
private let SentryDSNKey = "SentryDSN"
|
||||
private let SentryDeviceAppHashKey = "SentryDeviceAppHash"
|
||||
private let DefaultDeviceAppHash = "0000000000000000000000000000000000000000"
|
||||
private let DeviceAppHashLength = UInt(20)
|
||||
|
||||
private var enabled = false
|
||||
|
||||
private var attributes: [String: Any] = [:]
|
||||
|
||||
public func setup(sendUsageData: Bool) {
|
||||
assert(!enabled, "Sentry.setup() should only be called once")
|
||||
|
||||
if DeviceInfo.isSimulator() {
|
||||
Logger.browserLogger.debug("Not enabling Sentry; Running in Simulator")
|
||||
return
|
||||
}
|
||||
|
||||
if !sendUsageData {
|
||||
Logger.browserLogger.debug("Not enabling Sentry; Not enabled by user choice")
|
||||
return
|
||||
}
|
||||
|
||||
guard let dsn = Bundle.main.object(forInfoDictionaryKey: SentryDSNKey) as? String, !dsn.isEmpty else {
|
||||
Logger.browserLogger.debug("Not enabling Sentry; Not configured in Info.plist")
|
||||
return
|
||||
}
|
||||
|
||||
Logger.browserLogger.debug("Enabling Sentry crash handler")
|
||||
|
||||
do {
|
||||
Client.shared = try Client(dsn: dsn)
|
||||
try Client.shared?.startCrashHandler()
|
||||
enabled = true
|
||||
|
||||
// If we have not already for this install, generate a completely random identifier
|
||||
// for this device. It is stored in the app group so that the same value will
|
||||
// be used for both the main application and the app extensions.
|
||||
if let defaults = UserDefaults(suiteName: AppInfo.sharedContainerIdentifier), defaults.string(forKey: SentryDeviceAppHashKey) == nil {
|
||||
defaults.set(Bytes.generateRandomBytes(DeviceAppHashLength).hexEncodedString, forKey: SentryDeviceAppHashKey)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
// For all outgoing reports, override the default device identifier with our own random
|
||||
// version. Default to a blank (zero) identifier in case of errors.
|
||||
Client.shared?.beforeSerializeEvent = { event in
|
||||
let deviceAppHash = UserDefaults(suiteName: AppInfo.sharedContainerIdentifier)?.string(forKey: self.SentryDeviceAppHashKey)
|
||||
event.context?.appContext?["device_app_hash"] = deviceAppHash ?? self.DefaultDeviceAppHash
|
||||
|
||||
var attributes = event.extra ?? [:]
|
||||
attributes.merge(with: self.attributes)
|
||||
event.extra = attributes
|
||||
}
|
||||
} catch let error {
|
||||
Logger.browserLogger.error("Failed to initialize Sentry: \(error)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func crash() {
|
||||
Client.shared?.crash()
|
||||
}
|
||||
|
||||
/*
|
||||
This is the behaviour we want for Sentry logging
|
||||
.info .error .severe
|
||||
Debug y y y
|
||||
Beta y y y
|
||||
Relase n n y
|
||||
*/
|
||||
private func shouldNotSendEventFor(_ severity: SentrySeverity) -> Bool {
|
||||
return !enabled || (AppConstants.BuildChannel == .release && severity != .fatal)
|
||||
}
|
||||
|
||||
private func makeEvent(message: String, tag: String, severity: SentrySeverity, extra: [String: Any]?) -> Event {
|
||||
let event = Event(level: severity)
|
||||
event.message = message
|
||||
event.tags = ["tag": tag]
|
||||
if let extra = extra {
|
||||
event.extra = extra
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
public func send(message: String, tag: SentryTag = .general, severity: SentrySeverity = .info, extra: [String: Any]? = nil, description: String? = nil, completion: SentryRequestFinished? = nil) {
|
||||
// Build the dictionary
|
||||
var extraEvents: [String: Any] = [:]
|
||||
if let paramEvents = extra {
|
||||
extraEvents.merge(with: paramEvents)
|
||||
}
|
||||
if let extraString = description {
|
||||
extraEvents.merge(with: ["errorDescription": extraString])
|
||||
}
|
||||
printMessage(message: message, extra: extraEvents)
|
||||
|
||||
// Only report fatal errors on release
|
||||
if shouldNotSendEventFor(severity) {
|
||||
completion?(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let event = makeEvent(message: message, tag: tag.rawValue, severity: severity, extra: extraEvents)
|
||||
Client.shared?.send(event: event, completion: completion)
|
||||
}
|
||||
|
||||
public func sendWithStacktrace(message: String, tag: SentryTag = .general, severity: SentrySeverity = .info, extra: [String: Any]? = nil, description: String? = nil, completion: SentryRequestFinished? = nil) {
|
||||
var extraEvents: [String: Any] = [:]
|
||||
if let paramEvents = extra {
|
||||
extraEvents.merge(with: paramEvents)
|
||||
}
|
||||
if let extraString = description {
|
||||
extraEvents.merge(with: ["errorDescription": extraString])
|
||||
}
|
||||
printMessage(message: message, extra: extraEvents)
|
||||
|
||||
// Do not send messages to Sentry if disabled OR if we are not on beta and the severity isnt severe
|
||||
if shouldNotSendEventFor(severity) {
|
||||
completion?(nil)
|
||||
return
|
||||
}
|
||||
Client.shared?.snapshotStacktrace {
|
||||
let event = self.makeEvent(message: message, tag: tag.rawValue, severity: severity, extra: extraEvents)
|
||||
Client.shared?.appendStacktrace(to: event)
|
||||
event.debugMeta = nil
|
||||
Client.shared?.send(event: event, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
public func addAttributes(_ attributes: [String: Any]) {
|
||||
self.attributes.merge(with: attributes)
|
||||
}
|
||||
|
||||
private func printMessage(message: String, extra: [String: Any]? = nil) {
|
||||
let string = extra?.reduce("") { (result: String, arg1) in
|
||||
let (key, value) = arg1
|
||||
return "\(result), \(key): \(value)"
|
||||
}
|
||||
Logger.browserLogger.debug("Sentry: \(message) \(string ??? "")")
|
||||
}
|
||||
}
|
||||
9
mobile/ios/Shared/Shared-Bridging-Header.h
Normal file
9
mobile/ios/Shared/Shared-Bridging-Header.h
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#ifndef Client_Shared_Bridging_Header_h
|
||||
#define Client_Shared_Bridging_Header_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CommonCrypto/CommonCrypto.h>
|
||||
#import "FSUtils.h"
|
||||
#import "CrashSimulator.h"
|
||||
|
||||
#endif
|
||||
21
mobile/ios/Shared/SupportUtils.swift
Normal file
21
mobile/ios/Shared/SupportUtils.swift
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Utility functions related to SUMO.
|
||||
public struct SupportUtils {
|
||||
/// Construct a NSURL pointing to a specific topic on SUMO. The topic should be a non-escaped string. It will
|
||||
/// be properly escaped by this function.
|
||||
///
|
||||
/// The resulting NSURL will include the app version, operating system and locale code. For example, a topic
|
||||
/// "cheese" will be turned into a link that looks like https://support.mozilla.org/1/mobile/2.0/iOS/en-US/cheese
|
||||
public static func URLForTopic(_ topic: String) -> URL? {
|
||||
guard let escapedTopic = topic.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed),
|
||||
let languageIdentifier = Locale.preferredLanguages.first else {
|
||||
return nil
|
||||
}
|
||||
return URL(string: "https://support.mozilla.org/1/mobile/\(AppInfo.appVersion)/iOS/\(languageIdentifier)/\(escapedTopic)")
|
||||
}
|
||||
}
|
||||
26
mobile/ios/Shared/Supporting Files/Info.plist
Normal file
26
mobile/ios/Shared/Supporting Files/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>
|
||||
69
mobile/ios/Shared/SystemUtils.swift
Normal file
69
mobile/ios/Shared/SystemUtils.swift
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* System helper methods written in Swift.
|
||||
*/
|
||||
public struct SystemUtils {
|
||||
|
||||
/**
|
||||
Returns an accurate version of the system uptime even while the device is asleep.
|
||||
http://stackoverflow.com/questions/12488481/getting-ios-system-uptime-that-doesnt-pause-when-asleep
|
||||
|
||||
- returns: Time interval since last reboot.
|
||||
*/
|
||||
public static func systemUptime() -> TimeInterval {
|
||||
var boottime = timeval()
|
||||
var mib = [CTL_KERN, KERN_BOOTTIME]
|
||||
var size = MemoryLayout<timeval>.stride
|
||||
var now = time_t()
|
||||
time(&now)
|
||||
|
||||
sysctl(&mib, u_int(mib.count), &boottime, &size, nil, 0)
|
||||
let tv_sec: time_t = withUnsafePointer(to: &boottime.tv_sec) { $0.pointee }
|
||||
return TimeInterval(now - tv_sec)
|
||||
}
|
||||
}
|
||||
|
||||
extension SystemUtils {
|
||||
// This should be run on first run of the application.
|
||||
// It shouldn't be run from an extension.
|
||||
// Its function is to write a lock file that is only accessible from the application,
|
||||
// and not accessible from extension when the device is locked. Thus, we can tell if an extension is being run
|
||||
// when the device is locked.
|
||||
public static func onFirstRun() {
|
||||
guard let lockFileURL = lockedDeviceURL else {
|
||||
return
|
||||
}
|
||||
|
||||
let lockFile = lockFileURL.path
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: lockFile) {
|
||||
return
|
||||
}
|
||||
let contents = "Device is unlocked".data(using: String.Encoding.utf8)
|
||||
fm.createFile(atPath: lockFile, contents: contents, attributes: [FileAttributeKey.protectionKey.rawValue: FileProtectionType.complete])
|
||||
}
|
||||
|
||||
private static var lockedDeviceURL: URL? {
|
||||
let directoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppInfo.sharedContainerIdentifier)
|
||||
return directoryURL?.appendingPathComponent("security.dummy")
|
||||
}
|
||||
|
||||
public static func isDeviceLocked() -> Bool {
|
||||
guard let lockFileURL = lockedDeviceURL else {
|
||||
return true
|
||||
}
|
||||
do {
|
||||
_ = try Data(contentsOf: lockFileURL, options: .mappedIfSafe)
|
||||
return false
|
||||
} catch let err as NSError {
|
||||
return err.code == 257
|
||||
} catch _ {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
172
mobile/ios/Shared/TimeConstants.swift
Normal file
172
mobile/ios/Shared/TimeConstants.swift
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public typealias Timestamp = UInt64
|
||||
public typealias MicrosecondTimestamp = UInt64
|
||||
|
||||
public let ThreeWeeksInSeconds = 3 * 7 * 24 * 60 * 60
|
||||
|
||||
public let OneYearInMilliseconds = 12 * OneMonthInMilliseconds
|
||||
public let OneMonthInMilliseconds = 30 * OneDayInMilliseconds
|
||||
public let OneWeekInMilliseconds = 7 * OneDayInMilliseconds
|
||||
public let OneDayInMilliseconds = 24 * OneHourInMilliseconds
|
||||
public let OneHourInMilliseconds = 60 * OneMinuteInMilliseconds
|
||||
public let OneMinuteInMilliseconds = 60 * OneSecondInMilliseconds
|
||||
public let OneSecondInMilliseconds: UInt64 = 1000
|
||||
|
||||
fileprivate let rfc822DateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
|
||||
dateFormatter.dateFormat = "EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'"
|
||||
dateFormatter.locale = Locale(identifier: "en_US")
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
extension Timestamp {
|
||||
public static func uptimeInMilliseconds() -> Timestamp {
|
||||
return Timestamp(DispatchTime.now().uptimeNanoseconds) / 1000000
|
||||
}
|
||||
}
|
||||
|
||||
extension Date {
|
||||
public static func now() -> Timestamp {
|
||||
return UInt64(1000 * Date().timeIntervalSince1970)
|
||||
}
|
||||
|
||||
public static func nowNumber() -> NSNumber {
|
||||
return NSNumber(value: now() as UInt64)
|
||||
}
|
||||
|
||||
public static func nowMicroseconds() -> MicrosecondTimestamp {
|
||||
return UInt64(1000000 * Date().timeIntervalSince1970)
|
||||
}
|
||||
|
||||
public static func fromTimestamp(_ timestamp: Timestamp) -> Date {
|
||||
return Date(timeIntervalSince1970: Double(timestamp) / 1000)
|
||||
}
|
||||
|
||||
public static func fromMicrosecondTimestamp(_ microsecondTimestamp: MicrosecondTimestamp) -> Date {
|
||||
return Date(timeIntervalSince1970: Double(microsecondTimestamp) / 1000000)
|
||||
}
|
||||
|
||||
public func toRelativeTimeString() -> String {
|
||||
let now = Date()
|
||||
|
||||
let units: NSCalendar.Unit = [NSCalendar.Unit.second, NSCalendar.Unit.minute, NSCalendar.Unit.day, NSCalendar.Unit.weekOfYear, NSCalendar.Unit.month, NSCalendar.Unit.year, NSCalendar.Unit.hour]
|
||||
|
||||
let components = (Calendar.current as NSCalendar).components(units,
|
||||
from: self,
|
||||
to: now,
|
||||
options: [])
|
||||
|
||||
if components.year! > 0 {
|
||||
return String(format: DateFormatter.localizedString(from: self, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short))
|
||||
}
|
||||
|
||||
if components.month == 1 {
|
||||
return String(format: NSLocalizedString("more than a month ago", comment: "Relative date for dates older than a month and less than two months."))
|
||||
}
|
||||
|
||||
if components.month! > 1 {
|
||||
return String(format: DateFormatter.localizedString(from: self, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short))
|
||||
}
|
||||
|
||||
if components.weekOfYear! > 0 {
|
||||
return String(format: NSLocalizedString("more than a week ago", comment: "Description for a date more than a week ago, but less than a month ago."))
|
||||
}
|
||||
|
||||
if components.day == 1 {
|
||||
return String(format: NSLocalizedString("yesterday", comment: "Relative date for yesterday."))
|
||||
}
|
||||
|
||||
if components.day! > 1 {
|
||||
return String(format: NSLocalizedString("this week", comment: "Relative date for date in past week."), String(describing: components.day))
|
||||
}
|
||||
|
||||
if components.hour! > 0 || components.minute! > 0 {
|
||||
let absoluteTime = DateFormatter.localizedString(from: self, dateStyle: DateFormatter.Style.none, timeStyle: DateFormatter.Style.short)
|
||||
let format = NSLocalizedString("today at %@", comment: "Relative date for date older than a minute.")
|
||||
return String(format: format, absoluteTime)
|
||||
}
|
||||
|
||||
return String(format: NSLocalizedString("just now", comment: "Relative time for a tab that was visited within the last few moments."))
|
||||
}
|
||||
|
||||
public func toRFC822String() -> String {
|
||||
return rfc822DateFormatter.string(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
let MaxTimestampAsDouble: Double = Double(UInt64.max)
|
||||
|
||||
/** This is just like decimalSecondsStringToTimestamp, but it looks for values that seem to be
|
||||
* milliseconds and fixes them. That's necessary because Firefox for iOS <= 7.3 uploaded millis
|
||||
* when seconds were expected.
|
||||
*/
|
||||
public func someKindOfTimestampStringToTimestamp(_ input: String) -> Timestamp? {
|
||||
var double = 0.0
|
||||
if Scanner(string: input).scanDouble(&double) {
|
||||
// This should never happen. Hah!
|
||||
if double.isNaN || double.isInfinite {
|
||||
return nil
|
||||
}
|
||||
|
||||
// `double` will be either huge or negatively huge on overflow, and 0 on underflow.
|
||||
// We clamp to reasonable ranges.
|
||||
if double < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if double >= MaxTimestampAsDouble {
|
||||
// Definitely not representable as a timestamp if the seconds are this large!
|
||||
return nil
|
||||
}
|
||||
|
||||
if double > 1000000000000 {
|
||||
// Oh, this was in milliseconds.
|
||||
return Timestamp(double)
|
||||
}
|
||||
|
||||
let millis = double * 1000
|
||||
if millis >= MaxTimestampAsDouble {
|
||||
// Not representable as a timestamp.
|
||||
return nil
|
||||
}
|
||||
|
||||
return Timestamp(millis)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func decimalSecondsStringToTimestamp(_ input: String) -> Timestamp? {
|
||||
var double = 0.0
|
||||
if Scanner(string: input).scanDouble(&double) {
|
||||
// This should never happen. Hah!
|
||||
if double.isNaN || double.isInfinite {
|
||||
return nil
|
||||
}
|
||||
|
||||
// `double` will be either huge or negatively huge on overflow, and 0 on underflow.
|
||||
// We clamp to reasonable ranges.
|
||||
if double < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
let millis = double * 1000
|
||||
if millis >= MaxTimestampAsDouble {
|
||||
// Not representable as a timestamp.
|
||||
return nil
|
||||
}
|
||||
|
||||
return Timestamp(millis)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func millisecondsToDecimalSeconds(_ input: Timestamp) -> String {
|
||||
let val: Double = Double(input) / 1000
|
||||
return String(format: "%.2F", val)
|
||||
}
|
||||
131
mobile/ios/Shared/UserAgent.swift
Normal file
131
mobile/ios/Shared/UserAgent.swift
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
|
||||
open class UserAgent {
|
||||
private static var defaults = UserDefaults(suiteName: AppInfo.sharedContainerIdentifier)!
|
||||
|
||||
private static func clientUserAgent(prefix: String) -> String {
|
||||
return "\(prefix)/\(AppInfo.appVersion)b\(AppInfo.buildNumber) (\(DeviceInfo.deviceModel()); iPhone OS \(UIDevice.current.systemVersion)) (\(AppInfo.displayName))"
|
||||
}
|
||||
|
||||
open static var syncUserAgent: String {
|
||||
return clientUserAgent(prefix: "Firefox-iOS-Sync")
|
||||
}
|
||||
|
||||
open static var tokenServerClientUserAgent: String {
|
||||
return clientUserAgent(prefix: "Firefox-iOS-Token")
|
||||
}
|
||||
|
||||
open static var fxaUserAgent: String {
|
||||
return clientUserAgent(prefix: "Firefox-iOS-FxA")
|
||||
}
|
||||
|
||||
open static var defaultClientUserAgent: String {
|
||||
return clientUserAgent(prefix: "Firefox-iOS")
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this if you know that a value must have been computed before your
|
||||
* code runs, or you don't mind failure.
|
||||
*/
|
||||
open static func cachedUserAgent(checkiOSVersion: Bool = true,
|
||||
checkFirefoxVersion: Bool = true,
|
||||
checkFirefoxBuildNumber: Bool = true) -> String? {
|
||||
let currentiOSVersion = UIDevice.current.systemVersion
|
||||
let lastiOSVersion = defaults.string(forKey: "LastDeviceSystemVersionNumber")
|
||||
|
||||
let currentFirefoxBuildNumber = AppInfo.buildNumber
|
||||
let currentFirefoxVersion = AppInfo.appVersion
|
||||
let lastFirefoxVersion = defaults.string(forKey: "LastFirefoxVersionNumber")
|
||||
let lastFirefoxBuildNumber = defaults.string(forKey: "LastFirefoxBuildNumber")
|
||||
|
||||
if let firefoxUA = defaults.string(forKey: "UserAgent") {
|
||||
if (!checkiOSVersion || (lastiOSVersion == currentiOSVersion))
|
||||
&& (!checkFirefoxVersion || (lastFirefoxVersion == currentFirefoxVersion)
|
||||
&& (!checkFirefoxBuildNumber || (lastFirefoxBuildNumber == currentFirefoxBuildNumber))) {
|
||||
return firefoxUA
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
* This will typically return quickly, but can require creation of a UIWebView.
|
||||
* As a result, it must be called on the UI thread.
|
||||
*/
|
||||
open static func defaultUserAgent() -> String {
|
||||
assert(Thread.current.isMainThread, "This method must be called on the main thread.")
|
||||
|
||||
if let firefoxUA = UserAgent.cachedUserAgent(checkiOSVersion: true) {
|
||||
return firefoxUA
|
||||
}
|
||||
|
||||
let webView = UIWebView()
|
||||
|
||||
let appVersion = AppInfo.appVersion
|
||||
let buildNumber = AppInfo.buildNumber
|
||||
let currentiOSVersion = UIDevice.current.systemVersion
|
||||
defaults.set(currentiOSVersion, forKey: "LastDeviceSystemVersionNumber")
|
||||
defaults.set(appVersion, forKey: "LastFirefoxVersionNumber")
|
||||
defaults.set(buildNumber, forKey: "LastFirefoxBuildNumber")
|
||||
|
||||
let userAgent = webView.stringByEvaluatingJavaScript(from: "navigator.userAgent")!
|
||||
|
||||
// Extract the WebKit version and use it as the Safari version.
|
||||
let webKitVersionRegex = try! NSRegularExpression(pattern: "AppleWebKit/([^ ]+) ", options: [])
|
||||
|
||||
let match = webKitVersionRegex.firstMatch(in: userAgent, options: [],
|
||||
range: NSRange(location: 0, length: userAgent.characters.count))
|
||||
|
||||
if match == nil {
|
||||
print("Error: Unable to determine WebKit version in UA.")
|
||||
return userAgent // Fall back to Safari's.
|
||||
}
|
||||
|
||||
let webKitVersion = (userAgent as NSString).substring(with: match!.rangeAt(1))
|
||||
|
||||
// Insert "FxiOS/<version>" before the Mobile/ section.
|
||||
let mobileRange = (userAgent as NSString).range(of: "Mobile/")
|
||||
if mobileRange.location == NSNotFound {
|
||||
print("Error: Unable to find Mobile section in UA.")
|
||||
return userAgent // Fall back to Safari's.
|
||||
}
|
||||
|
||||
let mutableUA = NSMutableString(string: userAgent)
|
||||
mutableUA.insert("FxiOS/\(appVersion)b\(AppInfo.buildNumber) ", at: mobileRange.location)
|
||||
|
||||
let firefoxUA = "\(mutableUA) Safari/\(webKitVersion)"
|
||||
|
||||
defaults.set(firefoxUA, forKey: "UserAgent")
|
||||
|
||||
return firefoxUA
|
||||
}
|
||||
|
||||
open static func desktopUserAgent() -> String {
|
||||
let userAgent = NSMutableString(string: defaultUserAgent())
|
||||
|
||||
// Spoof platform section
|
||||
let platformRegex = try! NSRegularExpression(pattern: "\\([^\\)]+\\)", options: [])
|
||||
guard let platformMatch = platformRegex.firstMatch(in: userAgent as String, options: [], range: NSRange(location: 0, length: userAgent.length)) else {
|
||||
print("Error: Unable to determine platform in UA.")
|
||||
return String(userAgent)
|
||||
}
|
||||
userAgent.replaceCharacters(in: platformMatch.range, with: "(Macintosh; Intel Mac OS X 10_11_1)")
|
||||
|
||||
// Strip mobile section
|
||||
let mobileRegex = try! NSRegularExpression(pattern: " FxiOS/[^ ]+ Mobile/[^ ]+", options: [])
|
||||
|
||||
guard let mobileMatch = mobileRegex.firstMatch(in: userAgent as String, options: [], range: NSRange(location: 0, length: userAgent.length)) else {
|
||||
print("Error: Unable to find Mobile section in UA.")
|
||||
return String(userAgent)
|
||||
}
|
||||
userAgent.replaceCharacters(in: mobileMatch.range, with: "")
|
||||
|
||||
return String(userAgent)
|
||||
}
|
||||
}
|
||||
64
mobile/ios/Shared/WeakList.swift
Normal file
64
mobile/ios/Shared/WeakList.swift
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* A list that weakly holds references to its items.
|
||||
* Note that while the references themselves are cleared, their wrapper objects
|
||||
* are not (though they are reused). Also note that since slots are reused,
|
||||
* order is not preserved.
|
||||
*
|
||||
* This class crashes at runtime with EXC_BAD_ACCESS if a protocol is given as
|
||||
* the type T. Make sure to use a class type.
|
||||
*/
|
||||
open class WeakList<T: AnyObject>: Sequence {
|
||||
private var items = [WeakRef<T>]()
|
||||
|
||||
public init() {}
|
||||
|
||||
/**
|
||||
* Adds an item to the list.
|
||||
* Note that every insertion iterates through the list to find any "holes" (items that have
|
||||
* been deallocated) to reuse them, so this class may not be appropriate in situations where
|
||||
* insertion is frequent.
|
||||
*/
|
||||
open func insert(_ item: T) {
|
||||
// Reuse any existing slots that have been deallocated.
|
||||
for wrapper in items where wrapper.value == nil {
|
||||
wrapper.value = item
|
||||
return
|
||||
}
|
||||
|
||||
items.append(WeakRef(item))
|
||||
}
|
||||
|
||||
open func makeIterator() -> AnyIterator<T> {
|
||||
var index = 0
|
||||
|
||||
return AnyIterator() {
|
||||
if index >= self.items.count {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i in index..<self.items.count {
|
||||
if let value = self.items[i].value {
|
||||
index = i + 1
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
index = self.items.count
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class WeakRef<T: AnyObject> {
|
||||
public weak var value: T?
|
||||
|
||||
public init(_ value: T) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
10309
mobile/ios/Shared/effective_tld_names.dat
Normal file
10309
mobile/ios/Shared/effective_tld_names.dat
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue