Dactyloidae iOS initial commit

This commit is contained in:
wuggy 2026-06-26 21:04:09 -07:00
commit 7154a0497e
2123 changed files with 197052 additions and 0 deletions

View 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)
}
}

View 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
}()
}

View 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"
}
}

View 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)
}

View 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
}

View 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
}
}

View 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()
}
}

View 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 }
}

View 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

View 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

View 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
}

View 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
}
}
}

View 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
}
}

View 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
}
}

View 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) }
}
}

View 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)
}
}

View 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())
}
}

View 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: [])
}
}

View 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)")
}
}
}
}
}

View 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*-_.")
}
}

View 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)
}
}

View 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)
}
}

View file

@ -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)
}
}

View 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
}
}

View 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
}
}
}

View 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
}
}

View file

@ -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
}
}

View 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()
}

View 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
}
}

View 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)
// "ABEF"
: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
}
}

View 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))
}
}

View 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
}
}

View 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"
}
}

View 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

View 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

View 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)
}
}

View 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)
}
}

View 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
}
}

View 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)")
}
}
}

View 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"
}

View 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)
}
}
}

View 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))) }
}
}

View 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")

View 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)
}
}

View 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
}
}

View 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)"
}
}

View 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 ??? "")")
}
}

View 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

View 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)")
}
}

View 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>

View 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
}
}
}

View 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)
}

View 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)
}
}

View 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
}
}

File diff suppressed because it is too large Load diff