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,195 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import Shared
import AdjustSdk
private let AdjustIntegrationErrorDomain = "org.mozilla.ios.Firefox.AdjustIntegrationErrorDomain"
private let AdjustAttributionFileName = "AdjustAttribution.json"
private let AdjustAppTokenKey = "AdjustAppToken"
private let AdjustEnvironmentKey = "AdjustEnvironment"
private let AdjustSandboxEnvironment = "sandbox"
private let AdjustProductionEnvironment = "production"
private enum AdjustEnvironment: String {
case Sandbox = "sandbox"
case Production = "production"
}
private struct AdjustSettings {
var appToken: String
var environment: AdjustEnvironment
}
/// Simple (singleton) object to contain all code related to Adjust. The idea is to capture all logic
/// here so that we have one single place where we can see what Adjust is doing. Ideally you only call
/// functions in this object from other parts of the application.
class AdjustIntegration: NSObject {
let profile: Profile
init(profile: Profile) {
self.profile = profile
}
/// Return an ADJConfig object if Adjust has been enabled. It is determined from the values in
/// the Info.plist file if Adjust should be enabled, and if so, what its application token and
/// environment are. If those keys are either missing or empty in the Info.plist then it is
/// assumed that Adjust is not enabled for this build.
fileprivate func getConfig() -> ADJConfig? {
guard let settings = getSettings() else {
return nil
}
let config = ADJConfig(appToken: settings.appToken, environment: settings.environment.rawValue)
if settings.environment == .Sandbox {
config?.logLevel = ADJLogLevelDebug
}
config?.delegate = self
return config
}
/// Returns the Adjust settings from our Info.plist. If the settings are missing or invalid, such as an unknown
/// environment, then it will return nil.
fileprivate func getSettings() -> AdjustSettings? {
let bundle = Bundle.main
guard let adjustAppToken = bundle.object(forInfoDictionaryKey: AdjustAppTokenKey) as? String,
let adjustEnvironment = bundle.object(forInfoDictionaryKey: AdjustEnvironmentKey) as? String else {
return nil
}
guard !adjustAppToken.isEmpty && !adjustEnvironment.isEmpty else {
return nil
}
guard let environment = AdjustEnvironment(rawValue: adjustEnvironment) else {
Logger.browserLogger.error("Adjust - Invalid environment provided: \(adjustEnvironment)")
return nil
}
return AdjustSettings(appToken: adjustAppToken, environment: environment)
}
/// Returns true if the attribution file is present.
fileprivate func hasAttribution() throws -> Bool {
return FileManager.default.fileExists(atPath: try getAttributionPath())
}
/// Save an `ADJAttribution` instance to a JSON file. Throws an error if the file could not be written. The file
/// written is a JSON file with a single dictionary in it. We add one extra item to it that contains the current
/// timestamp in seconds since the UNIX epoch.
fileprivate func saveAttribution(_ attribution: ADJAttribution) throws {
if let attributionDictionary = attribution.dictionary() {
let dictionary = NSMutableDictionary(dictionary: attributionDictionary)
dictionary["_timestamp"] = NSNumber(value: Int64(Date().timeIntervalSince1970) as Int64)
let data = try JSONSerialization.data(withJSONObject: dictionary, options: [JSONSerialization.WritingOptions.prettyPrinted])
try data.write(to: URL(fileURLWithPath: try getAttributionPath()), options: [])
}
}
/// Return the path to the `AdjustAttribution.json` file. Throws an `NSError` if we could not build the path.
fileprivate func getAttributionPath() throws -> String {
guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
throw NSError(domain: AdjustIntegrationErrorDomain, code: -1,
userInfo: [NSLocalizedDescriptionKey: "Could not build \(AdjustAttributionFileName) path"])
}
return url.appendingPathComponent(AdjustAttributionFileName).path
}
/// Return true if Adjust should be enabled. If the user has disabled the Send Anonymous Usage Data then we immediately
/// return false. Otherwise we only do one ping, which means we only enable it if we have not seen the attributiond
/// data yet.
fileprivate func shouldEnable() throws -> Bool {
if profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true {
return true
}
return try hasAttribution() == false
}
/// Return true if retention (session) tracking should be enabled. This follows the Send Anonymous Usage Data
/// setting.
fileprivate func shouldTrackRetention() -> Bool {
return profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true
}
}
extension AdjustIntegration: AdjustDelegate {
/// This is called as part of `UIApplication.didFinishLaunchingWithOptions()`. We always initialize the
/// Adjust SDK. We always let it send the initial attribution ping. Session tracking is only enabled if
/// the Send Anonymous Usage Data setting is turned on.
func triggerApplicationDidFinishLaunchingWithOptions(_ launchOptions: [AnyHashable: Any]?) {
do {
if let config = getConfig() {
// Always initialize Adjust - otherwise we cannot enable/disable it later. Their SDK must be
// initialized through appDidFinishLaunching otherwise it will be in a bad state.
Adjust.appDidLaunch(config)
// Disable it right now if we have the attribution and if the user has disabled session tracking. If
// we do not have attribution yet then we wait until it comes in and at that point make the decision
// to disable Adjust again.
if try hasAttribution() {
if !shouldTrackRetention() {
Logger.browserLogger.info("Adjust - Disabling because sending of usage data is not allowed")
Adjust.setEnabled(false)
}
}
} else {
Logger.browserLogger.info("Adjust - Skipping because no or invalid config found")
}
} catch let error {
Logger.browserLogger.error("Adjust - Disabling because we failed to configure: \(error)")
Adjust.setEnabled(false)
}
}
/// This is called when Adjust has figured out the attribution. It will call us with a summary
/// of all the things it knows. Like the campaign ID. We simply save this to a local file so
/// that we know we have done a single attribution ping to Adjust.
///
/// Here we also disable Adjust based on the Send Anonymous Usage Data setting.
func adjustAttributionChanged(_ attribution: ADJAttribution!) {
do {
Logger.browserLogger.info("Adjust - Saving attribution info to disk")
try saveAttribution(attribution)
} catch let error {
Logger.browserLogger.error("Adjust - Failed to save attribution: \(error)")
}
// Keep Adjust enabled only if the user has allowed this
if shouldTrackRetention() {
Logger.browserLogger.info("Adjust - Enabling because user allows anonymous usage data collection")
Adjust.setEnabled(true)
} else {
Logger.browserLogger.info("Adjust - Disabling because user does not allow anonymous usage data collection")
Adjust.setEnabled(false)
}
}
/// This is called from the Settings screen. The settings screen will remember the choice in the
/// profile and then use this method to disable or enable Adjust.
static func setEnabled(_ enabled: Bool) {
Adjust.setEnabled(enabled)
}
/// Store the deeplink url from Adjust SDK. Per Adjust documentation, any interstitial view launched could interfere
/// with launching the deeplink. We let the interstial view decide what to do with deeplink.
/// Ref: https://github.com/adjust/ios_sdk#deferred-deep-linking-scenario
func adjustDeeplinkResponse(_ deeplink: URL!) -> Bool {
profile.prefs.setString("\(deeplink)", forKey: "AdjustDeeplinkKey")
return true
}
}

View file

@ -0,0 +1,884 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Shared
import Storage
import AVFoundation
import XCGLogger
import MessageUI
import SDWebImage
import SwiftKeychainWrapper
import LocalAuthentication
import SyncTelemetry
import SwiftRouter
import Sync
import CoreSpotlight
private let log = Logger.browserLogger
let LatestAppVersionProfileKey = "latestAppVersion"
let AllowThirdPartyKeyboardsKey = "settings.allowThirdPartyKeyboards"
private let InitialPingSentKey = "initialPingSent"
class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestoration {
public static func viewController(withRestorationIdentifierPath identifierComponents: [Any], coder: NSCoder) -> UIViewController? {
return nil
}
var window: UIWindow?
var browserViewController: BrowserViewController!
var rootViewController: UIViewController!
weak var profile: Profile?
var tabManager: TabManager!
var adjustIntegration: AdjustIntegration?
var applicationCleanlyBackgrounded = true
weak var application: UIApplication?
var launchOptions: [AnyHashable: Any]?
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
var openInFirefoxParams: LaunchParams?
var receivedURLs: [URL]?
var unifiedTelemetry: UnifiedTelemetry?
@discardableResult func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
//
// Determine if the application cleanly exited last time it was used. We default to true in
// case we have never done this before. Then check if the "ApplicationCleanlyBackgrounded" user
// default exists and whether was properly set to true on app exit.
//
// Then we always set the user default to false. It will be set to true when we the application
// is backgrounded.
//
self.applicationCleanlyBackgrounded = true
let defaults = UserDefaults()
if defaults.object(forKey: "ApplicationCleanlyBackgrounded") != nil {
self.applicationCleanlyBackgrounded = defaults.bool(forKey: "ApplicationCleanlyBackgrounded")
}
defaults.set(false, forKey: "ApplicationCleanlyBackgrounded")
defaults.synchronize()
// Hold references to willFinishLaunching parameters for delayed app launch
self.application = application
self.launchOptions = launchOptions
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window!.backgroundColor = UIColor.white
// Short circuit the app if we want to email logs from the debug menu
if DebugSettingsBundleOptions.launchIntoEmailComposer {
self.window?.rootViewController = UIViewController()
presentEmailComposerWithLogs()
return true
} else {
return startApplication(application, withLaunchOptions: launchOptions)
}
}
@discardableResult fileprivate func startApplication(_ application: UIApplication, withLaunchOptions launchOptions: [AnyHashable: Any]?) -> Bool {
log.info("startApplication begin")
// Need to get "settings.sendUsageData" this way so that Sentry can be initialized
// before getting the Profile.
let sendUsageData = NSUserDefaultsPrefs(prefix: "profile").boolForKey(AppConstants.PrefSendUsageData) ?? true
Sentry.shared.setup(sendUsageData: sendUsageData)
// Set the Firefox UA for browsing.
setUserAgent()
// Start the keyboard helper to monitor and cache keyboard state.
KeyboardHelper.defaultHelper.startObserving()
DynamicFontHelper.defaultHelper.startObserving()
MenuHelper.defaultHelper.setItems()
let logDate = Date()
// Create a new sync log file on cold app launch. Note that this doesn't roll old logs.
Logger.syncLogger.newLogWithDate(logDate)
Logger.browserLogger.newLogWithDate(logDate)
let profile = getProfile(application)
unifiedTelemetry = UnifiedTelemetry(profile: profile)
if !DebugSettingsBundleOptions.disableLocalWebServer {
// Set up a web server that serves us static content. Do this early so that it is ready when the UI is presented.
setUpWebServer(profile)
}
do {
// for aural progress bar: play even with silent switch on, and do not stop audio from other apps (like music)
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.mixWithOthers)
} catch _ {
print("Error: Failed to assign AVAudioSession category to allow playing with silent switch on for aural progress bar")
}
let imageStore = DiskImageStore(files: profile.files, namespace: "TabManagerScreenshots", quality: UIConstants.ScreenshotQuality)
// Temporary fix for Bug 1390871 - NSInvalidArgumentException: -[WKContentView menuHelperFindInPage]: unrecognized selector
if #available(iOS 11, *) {
if let clazz = NSClassFromString("WKCont" + "ent" + "View"), let swizzledMethod = class_getInstanceMethod(TabWebViewMenuHelper.self, #selector(TabWebViewMenuHelper.swizzledMenuHelperFindInPage)) {
class_addMethod(clazz, MenuHelper.SelectorFindInPage, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
}
}
self.tabManager = TabManager(prefs: profile.prefs, imageStore: imageStore)
self.tabManager.stateDelegate = self
// Add restoration class, the factory that will return the ViewController we
// will restore with.
browserViewController = BrowserViewController(profile: self.profile!, tabManager: self.tabManager)
browserViewController.edgesForExtendedLayout = []
browserViewController.restorationIdentifier = NSStringFromClass(BrowserViewController.self)
browserViewController.restorationClass = AppDelegate.self
let navigationController = UINavigationController(rootViewController: browserViewController)
navigationController.delegate = self
navigationController.isNavigationBarHidden = true
navigationController.edgesForExtendedLayout = UIRectEdge(rawValue: 0)
rootViewController = navigationController
self.window!.rootViewController = rootViewController
NotificationCenter.default.addObserver(forName: NSNotification.Name.FSReadingListAddReadingListItem, object: nil, queue: nil) { (notification) -> Void in
if let userInfo = notification.userInfo, let url = userInfo["URL"] as? URL {
let title = (userInfo["Title"] as? String) ?? ""
profile.readingList?.createRecordWithURL(url.absoluteString, title: title, addedBy: UIDevice.current.name)
}
}
NotificationCenter.default.addObserver(forName: NotificationFirefoxAccountDeviceRegistrationUpdated, object: nil, queue: nil) { _ in
profile.flushAccount()
}
// check to see if we started 'cos someone tapped on a notification.
if let localNotification = launchOptions?[UIApplicationLaunchOptionsKey.localNotification] as? UILocalNotification {
viewURLInNewTab(localNotification)
}
adjustIntegration = AdjustIntegration(profile: profile)
if LeanPlumClient.shouldEnable(profile: profile) {
LeanPlumClient.shared.setup(profile: profile)
LeanPlumClient.shared.set(enabled: true)
}
self.updateAuthenticationInfo()
SystemUtils.onFirstRun()
let fxaLoginHelper = FxALoginHelper.sharedInstance
fxaLoginHelper.application(application, didLoadProfile: profile)
setUpDeepLinks(application: application)
log.info("startApplication end")
return true
}
func setUpDeepLinks(application: UIApplication) {
let router = Router.shared
let rootNav = rootViewController as! UINavigationController
router.map("homepanel/:page", handler: { (params: [String: String]?) -> (Bool) in
guard let page = params?["page"] else {
return false
}
assert(Thread.isMainThread, "Opening homepanels requires being invoked on the main thread")
switch page {
case "bookmarks":
self.browserViewController.openURLInNewTab(HomePanelType.bookmarks.localhostURL, isPrivileged: true)
case "history":
self.browserViewController.openURLInNewTab(HomePanelType.history.localhostURL, isPrivileged: true)
case "new-private-tab":
self.browserViewController.openBlankNewTab(focusLocationField: false, isPrivate: true)
default:
break
}
return true
})
// Route to general settings page like this: "...settings/general"
router.map("settings/:page", handler: { (params: [String: String]?) -> (Bool) in
guard let page = params?["page"] else {
return false
}
assert(Thread.isMainThread, "Opening settings requires being invoked on the main thread")
let settingsTableViewController = AppSettingsTableViewController()
settingsTableViewController.profile = self.profile
settingsTableViewController.tabManager = self.tabManager
settingsTableViewController.settingsDelegate = self.browserViewController
let controller = SettingsNavigationController(rootViewController: settingsTableViewController)
controller.popoverDelegate = self.browserViewController
controller.modalPresentationStyle = UIModalPresentationStyle.formSheet
rootNav.present(controller, animated: true, completion: nil)
switch page {
case "newtab":
let viewController = NewTabChoiceViewController(prefs: self.getProfile(application).prefs)
controller.pushViewController(viewController, animated: true)
case "homepage":
let viewController = HomePageSettingsViewController()
viewController.profile = self.getProfile(application)
viewController.tabManager = self.tabManager
controller.pushViewController(viewController, animated: true)
case "mailto":
let viewController = OpenWithSettingsViewController(prefs: self.getProfile(application).prefs)
controller.pushViewController(viewController, animated: true)
case "search":
let viewController = SearchSettingsTableViewController()
viewController.model = self.getProfile(application).searchEngines
viewController.profile = self.getProfile(application)
controller.pushViewController(viewController, animated: true)
case "clear-private-data":
let viewController = ClearPrivateDataTableViewController()
viewController.profile = self.getProfile(application)
viewController.tabManager = self.tabManager
controller.pushViewController(viewController, animated: true)
case "fxa":
self.browserViewController.presentSignInViewController()
default:
break
}
return true
})
}
func applicationWillTerminate(_ application: UIApplication) {
// We have only five seconds here, so let's hope this doesn't take too long.
self.profile?.shutdown()
// Allow deinitializers to close our database connections.
self.profile = nil
self.tabManager = nil
self.browserViewController = nil
self.rootViewController = nil
}
/**
* We maintain a weak reference to the profile so that we can pause timed
* syncs when we're backgrounded.
*
* The long-lasting ref to the profile lives in BrowserViewController,
* which we set in application:willFinishLaunchingWithOptions:.
*
* If that ever disappears, we won't be able to grab the profile to stop
* syncing... but in that case the profile's deinit will take care of things.
*/
func getProfile(_ application: UIApplication) -> Profile {
if let profile = self.profile {
return profile
}
let p = BrowserProfile(localName: "profile", syncDelegate: application.syncDelegate)
self.profile = p
return p
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
var shouldPerformAdditionalDelegateHandling = true
adjustIntegration?.triggerApplicationDidFinishLaunchingWithOptions(launchOptions)
#if BUDDYBUILD
print("Setting up BuddyBuild SDK")
BuddyBuildSDK.setup()
#endif
window!.makeKeyAndVisible()
// Now roll logs.
DispatchQueue.global(qos: DispatchQoS.background.qosClass).async {
Logger.syncLogger.deleteOldLogsDownToSizeLimit()
Logger.browserLogger.deleteOldLogsDownToSizeLimit()
}
// If a shortcut was launched, display its information and take the appropriate action
if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem {
QuickActions.sharedInstance.launchedShortcutItem = shortcutItem
// This will block "performActionForShortcutItem:completionHandler" from being called.
shouldPerformAdditionalDelegateHandling = false
}
return shouldPerformAdditionalDelegateHandling
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return false
}
guard let urlTypes = Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [AnyObject],
let urlSchemes = urlTypes.first?["CFBundleURLSchemes"] as? [String] else {
// Something very strange has happened; org.mozilla.Client should be the zeroeth URL type.
log.error("Custom URL schemes not available for validating")
return false
}
guard let scheme = components.scheme, urlSchemes.contains(scheme) else {
log.warning("Cannot handle \(components.scheme ?? "nil") URL scheme")
return false
}
guard let host = url.host else {
log.warning("Cannot handle nil URL host")
return false
}
let query = url.getQuery()
switch host {
case "open-url":
let url = query["url"]?.unescape() ?? ""
let isPrivate = NSString(string: query["private"] ?? "false").boolValue
let params = LaunchParams(url: URL(string: url), isPrivate: isPrivate)
if application.applicationState == .active {
// If we are active then we can ask the BVC to open the new tab right away.
// Otherwise, we remember the URL and we open it in applicationDidBecomeActive.
launchFromURL(params)
} else {
openInFirefoxParams = params
}
return true
case "deep-link":
guard let url = query["url"], Bundle.main.bundleIdentifier == sourceApplication else {
break
}
Router.shared.routeURL(url)
return true
case "fxa-signin":
if AppConstants.MOZ_FXA_DEEP_LINK_FORM_FILL {
// FxA form filling requires a `signin` query param and host = fxa-signin
// Ex. firefox://fxa-signin?signin=<token>&someQuery=<data>...
guard query["signin"] != nil else {
break
}
let fxaParams: FxALaunchParams
fxaParams = FxALaunchParams(query: query)
launchFxAFromURL(fxaParams)
return true
}
break
default: ()
}
return false
}
func launchFxAFromURL(_ params: FxALaunchParams) {
self.browserViewController.presentSignInViewController(params)
}
func launchFromURL(_ params: LaunchParams) {
let isPrivate = params.isPrivate ?? false
if let newURL = params.url {
self.browserViewController.switchToTabForURLOrOpen(newURL, isPrivate: isPrivate, isPrivileged: false)
} else {
self.browserViewController.openBlankNewTab(focusLocationField: true, isPrivate: isPrivate)
}
LeanPlumClient.shared.track(event: .openedNewTab, withParameters: ["Source": "External App or Extension" as AnyObject])
}
// We sync in the foreground only, to avoid the possibility of runaway resource usage.
// Eventually we'll sync in response to notifications.
func applicationDidBecomeActive(_ application: UIApplication) {
guard !DebugSettingsBundleOptions.launchIntoEmailComposer else {
return
}
//
// We are back in the foreground, so set CleanlyBackgrounded to false so that we can detect that
// the application was cleanly backgrounded later.
//
let defaults = UserDefaults()
defaults.set(false, forKey: "ApplicationCleanlyBackgrounded")
defaults.synchronize()
if let profile = self.profile {
profile.reopen()
if profile.prefs.boolForKey(PendingAccountDisconnectedKey) ?? false {
FxALoginHelper.sharedInstance.applicationDidDisconnect(application)
}
profile.syncManager.applicationDidBecomeActive()
}
// We could load these here, but then we have to futz with the tab counter
// and making NSURLRequests.
self.browserViewController.loadQueuedTabs(receivedURLs: self.receivedURLs)
self.receivedURLs = nil
application.applicationIconBadgeNumber = 0
// handle quick actions is available
let quickActions = QuickActions.sharedInstance
if let shortcut = quickActions.launchedShortcutItem {
// dispatch asynchronously so that BVC is all set up for handling new tabs
// when we try and open them
quickActions.handleShortCutItem(shortcut, withBrowserViewController: browserViewController)
quickActions.launchedShortcutItem = nil
}
// Check if we have a URL from an external app or extension waiting to launch,
// then launch it on the main thread.
if let params = openInFirefoxParams {
openInFirefoxParams = nil
DispatchQueue.main.async {
self.launchFromURL(params)
}
}
UnifiedTelemetry.recordEvent(category: .action, method: .foreground, object: .app)
}
func applicationDidEnterBackground(_ application: UIApplication) {
//
// At this point we are happy to mark the app as CleanlyBackgrounded. If a crash happens in background
// sync then that crash will still be reported. But we won't bother the user with the Restore Tabs
// dialog. We don't have to because at this point we already saved the tab state properly.
//
let defaults = UserDefaults()
defaults.set(true, forKey: "ApplicationCleanlyBackgrounded")
defaults.synchronize()
syncOnDidEnterBackground(application: application)
UnifiedTelemetry.recordEvent(category: .action, method: .background, object: .app)
}
fileprivate func syncOnDidEnterBackground(application: UIApplication) {
guard let profile = self.profile else {
return
}
profile.syncManager.applicationDidEnterBackground()
var taskId: UIBackgroundTaskIdentifier = 0
taskId = application.beginBackgroundTask (expirationHandler: { _ in
print("Running out of background time, but we have a profile shutdown pending.")
self.shutdownProfileWhenNotActive(application)
application.endBackgroundTask(taskId)
})
if profile.hasSyncableAccount() {
profile.syncManager.syncEverything(why: .backgrounded).uponQueue(DispatchQueue.main) { _ in
self.shutdownProfileWhenNotActive(application)
application.endBackgroundTask(taskId)
}
} else {
profile.shutdown()
application.endBackgroundTask(taskId)
}
}
fileprivate func shutdownProfileWhenNotActive(_ application: UIApplication) {
// Only shutdown the profile if we are not in the foreground
guard application.applicationState != UIApplicationState.active else {
return
}
profile?.shutdown()
}
func applicationWillEnterForeground(_ application: UIApplication) {
// The reason we need to call this method here instead of `applicationDidBecomeActive`
// is that this method is only invoked whenever the application is entering the foreground where as
// `applicationDidBecomeActive` will get called whenever the Touch ID authentication overlay disappears.
self.updateAuthenticationInfo()
}
fileprivate func updateAuthenticationInfo() {
if let authInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo() {
if !LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
authInfo.useTouchID = false
KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authInfo)
}
}
}
fileprivate func setUpWebServer(_ profile: Profile) {
let server = WebServer.sharedInstance
ReaderModeHandlers.register(server, profile: profile)
ErrorPageHelper.register(server, certStore: profile.certStore)
AboutHomeHandler.register(server)
AboutLicenseHandler.register(server)
SessionRestoreHandler.register(server)
// Bug 1223009 was an issue whereby CGDWebserver crashed when moving to a background task
// catching and handling the error seemed to fix things, but we're not sure why.
// Either way, not implicitly unwrapping a try is not a great way of doing things
// so this is better anyway.
do {
try server.start()
} catch let err as NSError {
print("Error: Unable to start WebServer \(err)")
}
}
fileprivate func setUserAgent() {
let firefoxUA = UserAgent.defaultUserAgent()
// Set the UA for WKWebView (via defaults), the favicon fetcher, and the image loader.
// This only needs to be done once per runtime. Note that we use defaults here that are
// readable from extensions, so they can just use the cached identifier.
let defaults = UserDefaults(suiteName: AppInfo.sharedContainerIdentifier)!
defaults.register(defaults: ["UserAgent": firefoxUA])
SDWebImageDownloader.shared().setValue(firefoxUA, forHTTPHeaderField: "User-Agent")
// Record the user agent for use by search suggestion clients.
SearchViewController.userAgent = firefoxUA
// Some sites will only serve HTML that points to .ico files.
// The FaviconFetcher is explicitly for getting high-res icons, so use the desktop user agent.
FaviconFetcher.userAgent = UserAgent.desktopUserAgent()
}
func application(_ application: UIApplication, handleActionWithIdentifier identifier: String?, for notification: UILocalNotification, completionHandler: @escaping () -> Void) {
if let actionId = identifier {
if let action = SentTabAction(rawValue: actionId) {
viewURLInNewTab(notification)
switch action {
case .bookmark:
addBookmark(notification)
break
case .readingList:
addToReadingList(notification)
break
default:
break
}
} else {
print("ERROR: Unknown notification action received")
}
} else {
print("ERROR: Unknown notification received")
}
}
func application(_ application: UIApplication, didReceive notification: UILocalNotification) {
viewURLInNewTab(notification)
}
fileprivate func presentEmailComposerWithLogs() {
if let buildNumber = Bundle.main.object(forInfoDictionaryKey: String(kCFBundleVersionKey)) as? NSString {
let mailComposeViewController = MFMailComposeViewController()
mailComposeViewController.mailComposeDelegate = self
mailComposeViewController.setSubject("Debug Info for iOS client version v\(appVersion) (\(buildNumber))")
if DebugSettingsBundleOptions.attachLogsToDebugEmail {
do {
let logNamesAndData = try Logger.diskLogFilenamesAndData()
logNamesAndData.forEach { nameAndData in
if let data = nameAndData.1 {
mailComposeViewController.addAttachmentData(data, mimeType: "text/plain", fileName: nameAndData.0)
}
}
} catch _ {
print("Failed to retrieve logs from device")
}
}
if DebugSettingsBundleOptions.attachTabStateToDebugEmail {
if let tabStateDebugData = TabManager.tabRestorationDebugInfo().data(using: String.Encoding.utf8) {
mailComposeViewController.addAttachmentData(tabStateDebugData, mimeType: "text/plain", fileName: "tabState.txt")
}
if let tabStateData = TabManager.tabArchiveData() {
mailComposeViewController.addAttachmentData(tabStateData as Data, mimeType: "application/octet-stream", fileName: "tabsState.archive")
}
}
self.window?.rootViewController?.present(mailComposeViewController, animated: true, completion: nil)
}
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
// If the `NSUserActivity` has a `webpageURL`, it is either a deep link or an old history item
// reached via a "Spotlight" search before we began indexing visited pages via CoreSpotlight.
if let url = userActivity.webpageURL {
let query = url.getQuery()
// Check for fxa sign-in code and launch the login screen directly
if query["signin"] != nil {
browserViewController.launchFxAFromDeeplinkURL(url)
return true
}
// Per Adjust documenation, https://docs.adjust.com/en/universal-links/#running-campaigns-through-universal-links,
// it is recommended that links contain the `deep_link` query parameter. This link will also
// be url encoded.
if let deepLink = query["deep_link"]?.removingPercentEncoding, let url = URL(string: deepLink) {
browserViewController.switchToTabForURLOrOpen(url, isPrivileged: true)
return true
}
browserViewController.switchToTabForURLOrOpen(url, isPrivileged: true)
return true
}
// Otherwise, check if the `NSUserActivity` is a CoreSpotlight item and switch to its tab or
// open a new one.
if userActivity.activityType == CSSearchableItemActionType {
if let userInfo = userActivity.userInfo,
let urlString = userInfo[CSSearchableItemActivityIdentifier] as? String,
let url = URL(string: urlString) {
browserViewController.switchToTabForURLOrOpen(url, isPrivileged: true)
return true
}
}
return false
}
fileprivate func viewURLInNewTab(_ notification: UILocalNotification) {
if let alertURL = notification.userInfo?[TabSendURLKey] as? String {
if let urlToOpen = URL(string: alertURL) {
browserViewController.openURLInNewTab(urlToOpen, isPrivileged: true)
}
}
}
fileprivate func addBookmark(_ notification: UILocalNotification) {
if let alertURL = notification.userInfo?[TabSendURLKey] as? String,
let title = notification.userInfo?[TabSendTitleKey] as? String {
let tabState = TabState(isPrivate: false, desktopSite: false, isBookmarked: false, url: URL(string: alertURL), title: title, favicon: nil)
browserViewController.addBookmark(tabState)
let userData = [QuickActions.TabURLKey: alertURL,
QuickActions.TabTitleKey: title]
QuickActions.sharedInstance.addDynamicApplicationShortcutItemOfType(.openLastBookmark, withUserData: userData, toApplication: UIApplication.shared)
}
}
fileprivate func addToReadingList(_ notification: UILocalNotification) {
if let alertURL = notification.userInfo?[TabSendURLKey] as? String,
let title = notification.userInfo?[TabSendTitleKey] as? String {
if let urlToOpen = URL(string: alertURL) {
NotificationCenter.default.post(name: NSNotification.Name.FSReadingListAddReadingListItem, object: self, userInfo: ["URL": urlToOpen, "Title": title])
}
}
}
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
let handledShortCutItem = QuickActions.sharedInstance.handleShortCutItem(shortcutItem, withBrowserViewController: browserViewController)
completionHandler(handledShortCutItem)
}
}
// MARK: - Root View Controller Animations
extension AppDelegate: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if operation == UINavigationControllerOperation.push {
return BrowserToTrayAnimator()
} else if operation == UINavigationControllerOperation.pop {
return TrayToBrowserAnimator()
} else {
return nil
}
}
}
extension AppDelegate: TabManagerStateDelegate {
func tabManagerWillStoreTabs(_ tabs: [Tab]) {
// It is possible that not all tabs have loaded yet, so we filter out tabs with a nil URL.
let storedTabs: [RemoteTab] = tabs.flatMap( Tab.toTab )
// Don't insert into the DB immediately. We tend to contend with more important
// work like querying for top sites.
let queue = DispatchQueue.global(qos: DispatchQoS.background.qosClass)
queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(ProfileRemoteTabsSyncDelay * Double(NSEC_PER_MSEC))) / Double(NSEC_PER_SEC)) {
self.profile?.storeTabs(storedTabs)
}
}
}
extension AppDelegate: MFMailComposeViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
// Dismiss the view controller and start the app up
controller.dismiss(animated: true, completion: nil)
startApplication(application!, withLaunchOptions: self.launchOptions)
}
}
extension AppDelegate {
func application(_ application: UIApplication, didRegister notificationSettings: UIUserNotificationSettings) {
FxALoginHelper.sharedInstance.application(application, didRegisterUserNotificationSettings: notificationSettings)
}
}
extension AppDelegate {
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
FxALoginHelper.sharedInstance.apnsRegisterDidSucceed(deviceToken)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("failed to register. \(error)")
FxALoginHelper.sharedInstance.apnsRegisterDidFail()
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
if Logger.logPII && log.isEnabledFor(level: .info) {
NSLog("APNS NOTIFICATION \(userInfo)")
}
// At this point, we know that NotificationService has been run.
// We get to this point if the notification was received while the app was in the foreground
// OR the app was backgrounded and now the user has tapped on the notification.
// Either way, if this method is being run, then the app is foregrounded.
// Either way, we should zero the badge number.
application.applicationIconBadgeNumber = 0
guard let profile = self.profile else {
return completionHandler(.noData)
}
// NotificationService will have decrypted the push message, and done some syncing
// activity. If the `client` collection was synced, and there are `displayURI` commands (i.e. sent tabs)
// NotificationService will have collected them for us in the userInfo.
if let serializedTabs = userInfo["sentTabs"] as? [[String: String]] {
// Let's go ahead and open those.
let receivedURLs = serializedTabs.flatMap { item -> URL? in
guard let tabURL = item["url"] else {
return nil
}
return URL(string: tabURL)
}
if receivedURLs.count > 0 {
// Remember which URLs we received so we can filter them out later when
// loading the queued tabs.
self.receivedURLs = receivedURLs
// If we're in the foreground, load the queued tabs now.
if application.applicationState == UIApplicationState.active {
DispatchQueue.main.async {
self.browserViewController.loadQueuedTabs(receivedURLs: self.receivedURLs)
self.receivedURLs = nil
}
}
return completionHandler(.newData)
}
}
// By now, we've dealt with any sent tab notifications.
//
// The only thing left to do now is to perform actions that can only be performed
// while the app is foregrounded.
//
// Use the push message handler to re-parse the message,
// this time with a BrowserProfile and processing the return
// differently than in NotificationService.
let handler = FxAPushMessageHandler(with: profile)
handler.handle(userInfo: userInfo).upon { res in
if let message = res.successValue {
switch message {
case .accountVerified:
_ = handler.postVerification()
case .thisDeviceDisconnected:
FxALoginHelper.sharedInstance.applicationDidDisconnect(application)
default:
break
}
}
completionHandler(res.isSuccess ? .newData : .failed)
}
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
let completionHandler: (UIBackgroundFetchResult) -> Void = { _ in }
self.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler)
}
}
struct FxALaunchParams {
var query: [String: String]
}
struct LaunchParams {
let url: URL?
let isPrivate: Bool?
}
extension UIApplication {
var syncDelegate: SyncDelegate {
return AppSyncDelegate(app: self)
}
static var isInPrivateMode: Bool {
let appDelegate = UIApplication.shared.delegate as? AppDelegate
return appDelegate?.browserViewController.tabManager.selectedTab?.isPrivate ?? false
}
}
class AppSyncDelegate: SyncDelegate {
let app: UIApplication
init(app: UIApplication) {
self.app = app
}
open func displaySentTab(for url: URL, title: String, from deviceName: String?) {
DispatchQueue.main.sync {
if let appDelegate = app.delegate as? AppDelegate, app.applicationState == .active {
appDelegate.browserViewController.switchToTabForURLOrOpen(url, isPrivileged: false)
return
}
// check to see what the current notification settings are and only try and send a notification if
// the user has agreed to them
if let currentSettings = app.currentUserNotificationSettings {
if currentSettings.types.rawValue & UIUserNotificationType.alert.rawValue != 0 {
if Logger.logPII {
log.info("Displaying notification for URL \(url.absoluteString)")
}
let notification = UILocalNotification()
notification.fireDate = Date()
notification.timeZone = NSTimeZone.default
let title: String
if let deviceName = deviceName {
title = String(format: Strings.SentTab_TabArrivingNotification_WithDevice_title, deviceName)
} else {
title = Strings.SentTab_TabArrivingNotification_NoDevice_title
}
notification.alertTitle = title
notification.alertBody = url.absoluteDisplayExternalString
notification.userInfo = [TabSendURLKey: url.absoluteString, TabSendTitleKey: title]
notification.alertAction = nil
// Restore this when we fix Bug 1364420.
// notification.category = TabSendCategory
app.presentLocalNotificationNow(notification)
}
}
}
}
}

View file

@ -0,0 +1,13 @@
//
// Crasher.h
// Client
//
// Created by Steph Leroux on 2015-10-08.
// Copyright © 2015 Mozilla. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface Crasher : NSObject
@end

View file

@ -0,0 +1,13 @@
//
// Crasher.m
// Client
//
// Created by Steph Leroux on 2015-10-08.
// Copyright © 2015 Mozilla. All rights reserved.
//
#import "Crasher.h"
@implementation Crasher
@end

View file

@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import MessageUI
struct DebugSettingsBundleOptions {
/// Don't restore tabs on app launch
static var skipSessionRestore: Bool {
return UserDefaults.standard.bool(forKey: "SettingsBundleSkipSessionRestore")
}
/// Disable the local web server we use for restoration, error pages, etc
static var disableLocalWebServer: Bool {
return UserDefaults.standard.bool(forKey: "SettingsBundleDisableLocalWebServer")
}
/// When enabled, the app launch will be replaced with the mail compose view appearing with the device
/// logs pre-attached. When the mail is sent, the app continues launching normally.
static var launchIntoEmailComposer: Bool {
return ((attachTabStateToDebugEmail || attachLogsToDebugEmail) && MFMailComposeViewController.canSendMail())
}
/// When enabled, the email composer will have the tab state attached.
static var attachTabStateToDebugEmail: Bool {
return UserDefaults.standard.bool(forKey: "SettingsBundleEmailTabState")
}
/// When enabled, the email composer will have the application logs attached.
static var attachLogsToDebugEmail: Bool {
return UserDefaults.standard.bool(forKey: "SettingsBundleEmailLogsOnLaunch")
}
}

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="7706" systemVersion="14E26a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="splash" translatesAutoresizingMaskIntoConstraints="NO" id="8jg-1f-0sB">
<rect key="frame" x="175" y="175" width="130" height="130"/>
<constraints>
<constraint firstAttribute="width" constant="130" id="V8U-lj-86g"/>
<constraint firstAttribute="height" constant="130" id="eV3-E9-QZk"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="8jg-1f-0sB" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="ITK-hU-DwR"/>
<constraint firstItem="8jg-1f-0sB" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="grm-jt-xY2"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="404" y="445"/>
</view>
</objects>
<resources>
<image name="splash" width="400" height="390"/>
</resources>
</document>

View file

@ -0,0 +1,320 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import AdSupport
import Shared
import Leanplum
private let LPAppIdKey = "LeanplumAppId"
private let LPProductionKeyKey = "LeanplumProductionKey"
private let LPDevelopmentKeyKey = "LeanplumDevelopmentKey"
private let AppRequestedUserNotificationsPrefKey = "applicationDidRequestUserNotificationPermissionPrefKey"
// FxA Custom Leanplum message template for A/B testing push notifications.
private struct LPMessage {
static let FxAPrePush = "FxA Prepush v1"
static let ArgAcceptAction = "Accept action"
static let ArgCancelAction = "Cancel action"
static let ArgTitleText = "Title.Text"
static let ArgTitleColor = "Title.Color"
static let ArgMessageText = "Message.Text"
static let ArgMessageColor = "Message.Color"
static let ArgAcceptButtonText = "Accept button.Text"
static let ArgCancelButtonText = "Cancel button.Text"
static let ArgCancelButtonTextColor = "Cancel button.Text color"
// These defaults are overridden though Leanplum webUI
static let DefaultAskToAskTitle = NSLocalizedString("Firefox Sync Requires Push", comment: "Default push to ask title")
static let DefaultAskToAskMessage = NSLocalizedString("Firefox will stay in sync faster with Push Notifications enabled.", comment: "Default push to ask message")
static let DefaultOkButtonText = NSLocalizedString("Enable Push", comment: "Default push alert ok button text")
static let DefaultLaterButtonText = NSLocalizedString("Don't Enable", comment: "Default push alert cancel button text")
}
private let log = Logger.browserLogger
enum LPEvent: String {
case firstRun = "E_First_Run"
case secondRun = "E_Second_Run"
case openedApp = "E_Opened_App"
case dismissedOnboarding = "E_Dismissed_Onboarding"
case openedLogins = "Opened Login Manager"
case openedBookmark = "E_Opened_Bookmark"
case openedNewTab = "E_Opened_New_Tab"
case openedPocketStory = "E_Opened_Pocket_Story"
case interactWithURLBar = "E_Interact_With_Search_URL_Area"
case savedBookmark = "E_Saved_Bookmark"
case openedTelephoneLink = "Opened Telephone Link"
case openedMailtoLink = "E_Opened_Mailto_Link"
case saveImage = "E_Download_Media_Saved_Image"
case savedLoginAndPassword = "E_Saved_Login_And_Password"
case clearPrivateData = "E_Cleared_Private_Data"
case downloadedFocus = "E_User_Downloaded_Focus"
case downloadedPocket = "E_User_Downloaded_Pocket"
case userSharedWebpage = "E_User_Tapped_Share_Button"
case signsInFxa = "E_User_Signed_In_To_FxA"
case useReaderView = "E_User_Used_Reader_View"
case trackingProtectionSettings = "E_Tracking_Protection_Settings_Changed"
}
struct LPAttributeKey {
static let focusInstalled = "Focus Installed"
static let klarInstalled = "Klar Installed"
static let signedInSync = "Signed In Sync"
static let mailtoIsDefault = "Mailto Is Default"
static let pocketInstalled = "Pocket Installed"
static let telemetryOptIn = "Telemetry Opt In"
}
struct MozillaAppSchemes {
static let focus = "firefox-focus"
static let focusDE = "firefox-klar"
static let pocket = "pocket"
}
private let supportedLocales = ["en_US", "de_DE", "en_GB", "en_CA", "en_AU", "zh_TW", "en_HK", "en_SG",
"fr_FR", "it_IT", "id_ID", "id_ID", "pt_BR", "pl_PL", "ru_RU", "es_ES", "es_MX"]
private struct LPSettings {
var appId: String
var developmentKey: String
var productionKey: String
}
class LeanPlumClient {
static let shared = LeanPlumClient()
// Setup
private weak var profile: Profile?
private var prefs: Prefs? { return profile?.prefs }
private var enabled: Bool = true
// This defines an external Leanplum varible to enable/disable FxA prepush dialogs.
// The primary result is having a feature flag controlled by Leanplum, and falling back
// to prompting with native push permissions.
private var useFxAPrePush: LPVar = LPVar.define("useFxAPrePush", with: false)
private func isPrivateMode() -> Bool {
// Need to be run on main thread since isInPrivateMode requires to be on the main thread.
assert(Thread.isMainThread)
return UIApplication.isInPrivateMode
}
func isLPEnabled() -> Bool {
return enabled && Leanplum.hasStarted()
}
static func shouldEnable(profile: Profile) -> Bool {
return AppConstants.MOZ_ENABLE_LEANPLUM && (profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true)
}
func setup(profile: Profile) {
self.profile = profile
}
fileprivate func start() {
guard let settings = getSettings(), supportedLocales.contains(Locale.current.identifier), !Leanplum.hasStarted() else {
enabled = false
log.error("LeanplumIntegration - Could not be started")
return
}
if UIDevice.current.name.contains("MozMMADev") {
log.info("LeanplumIntegration - Setting up for Development")
Leanplum.setDeviceId(UIDevice.current.identifierForVendor?.uuidString)
Leanplum.setAppId(settings.appId, withDevelopmentKey: settings.developmentKey)
} else {
log.info("LeanplumIntegration - Setting up for Production")
Leanplum.setAppId(settings.appId, withProductionKey: settings.productionKey)
}
Leanplum.syncResourcesAsync(true)
let attributes: [AnyHashable: Any] = [
LPAttributeKey.mailtoIsDefault: mailtoIsDefault(),
LPAttributeKey.focusInstalled: focusInstalled(),
LPAttributeKey.klarInstalled: klarInstalled(),
LPAttributeKey.pocketInstalled: pocketInstalled(),
LPAttributeKey.signedInSync: profile?.hasAccount() ?? false
]
self.setupCustomTemplates()
Leanplum.start(withUserId: nil, userAttributes: attributes, responseHandler: { _ in
self.track(event: .openedApp)
// We need to check if the app is a clean install to use for
// preventing the What's New URL from appearing.
if self.prefs?.intForKey(IntroViewControllerSeenProfileKey) == nil {
self.prefs?.setString(AppInfo.appVersion, forKey: LatestAppVersionProfileKey)
self.track(event: .firstRun)
} else if self.prefs?.boolForKey("SecondRun") == nil {
self.prefs?.setBool(true, forKey: "SecondRun")
self.track(event: .secondRun)
}
self.checkIfAppWasInstalled(key: PrefsKeys.HasFocusInstalled, isAppInstalled: self.focusInstalled(), lpEvent: .downloadedFocus)
self.checkIfAppWasInstalled(key: PrefsKeys.HasPocketInstalled, isAppInstalled: self.pocketInstalled(), lpEvent: .downloadedPocket)
})
}
// Events
func track(event: LPEvent, withParameters parameters: [String: AnyObject]? = nil) {
guard isLPEnabled() else {
return
}
ensureMainThread {
guard !self.isPrivateMode() else {
return
}
if let params = parameters {
Leanplum.track(event.rawValue, withParameters: params)
} else {
Leanplum.track(event.rawValue)
}
}
}
func set(attributes: [AnyHashable: Any]) {
guard isLPEnabled() else {
return
}
ensureMainThread {
if !self.isPrivateMode() {
Leanplum.setUserAttributes(attributes)
}
}
}
func set(enabled: Bool) {
// Setting up Test Mode stops sending things to server.
if enabled { start() }
self.enabled = enabled
Leanplum.setTestModeEnabled(!enabled)
}
func isFxAPrePushEnabled() -> Bool {
return AppConstants.MOZ_FXA_LEANPLUM_AB_PUSH_TEST && useFxAPrePush.boolValue()
}
/*
This is used to determine if an app was installed after firefox was installed
*/
private func checkIfAppWasInstalled(key: String, isAppInstalled: Bool, lpEvent: LPEvent) {
// if no key is present. create one and set it.
// if the app is already installed then the flag will set true and the second block will never run
if self.prefs?.boolForKey(key) == nil {
self.prefs?.setBool(isAppInstalled, forKey: key)
}
// on a subsquent launch if the app is installed and the key is false then switch the flag to true
if !(self.prefs?.boolForKey(key) ?? false), isAppInstalled {
self.prefs?.setBool(isAppInstalled, forKey: key)
self.track(event: lpEvent)
}
}
private func canOpenApp(scheme: String) -> Bool {
return URL(string: "\(scheme)://").flatMap { UIApplication.shared.canOpenURL($0) } ?? false
}
private func focusInstalled() -> Bool {
return canOpenApp(scheme: MozillaAppSchemes.focus)
}
private func klarInstalled() -> Bool {
return canOpenApp(scheme: MozillaAppSchemes.focusDE)
}
private func pocketInstalled() -> Bool {
return canOpenApp(scheme: MozillaAppSchemes.pocket)
}
private func mailtoIsDefault() -> Bool {
return (prefs?.stringForKey(PrefsKeys.KeyMailToOption) ?? "mailto:") == "mailto:"
}
private func getSettings() -> LPSettings? {
let bundle = Bundle.main
guard let appId = bundle.object(forInfoDictionaryKey: LPAppIdKey) as? String,
let productionKey = bundle.object(forInfoDictionaryKey: LPProductionKeyKey) as? String,
let developmentKey = bundle.object(forInfoDictionaryKey: LPDevelopmentKeyKey) as? String else {
return nil
}
return LPSettings(appId: appId, developmentKey: developmentKey, productionKey: productionKey)
}
// This must be called before `Leanplum.start` in order to correctly setup
// custom message templates.
private func setupCustomTemplates() {
// These properties are exposed through the Leanplum web interface.
// Ref: https://github.com/Leanplum/Leanplum-iOS-Samples/blob/master/iOS_customMessageTemplates/iOS_customMessageTemplates/LPMessageTemplates.m
let args: [LPActionArg] = [
LPActionArg(named: LPMessage.ArgTitleText, with: LPMessage.DefaultAskToAskTitle),
LPActionArg(named: LPMessage.ArgTitleColor, with: UIColor.black),
LPActionArg(named: LPMessage.ArgMessageText, with: LPMessage.DefaultAskToAskMessage),
LPActionArg(named: LPMessage.ArgMessageColor, with: UIColor.black),
LPActionArg(named: LPMessage.ArgAcceptButtonText, with: LPMessage.DefaultOkButtonText),
LPActionArg(named: LPMessage.ArgCancelAction, withAction: nil),
LPActionArg(named: LPMessage.ArgCancelButtonText, with: LPMessage.DefaultLaterButtonText),
LPActionArg(named: LPMessage.ArgCancelButtonTextColor, with: UIColor.gray)
]
let responder: LeanplumActionBlock = { (context) -> Bool in
// Before proceeding, double check that Leanplum FxA prepush config value has been enabled.
if !self.isFxAPrePushEnabled() {
return false
}
guard let context = context else {
return false
}
// Don't display permission screen if they have already allowed/disabled push permissions
if self.prefs?.boolForKey(AppRequestedUserNotificationsPrefKey) ?? false {
FxALoginHelper.sharedInstance.readyForSyncing()
return false
}
// Present Alert View onto the current top view controller
let rootViewController = UIApplication.topViewController()
let alert = UIAlertController(title: context.stringNamed(LPMessage.ArgTitleText), message: context.stringNamed(LPMessage.ArgMessageText), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: context.stringNamed(LPMessage.ArgCancelButtonText), style: .cancel, handler: { (action) -> Void in
// Log cancel event and call ready for syncing
context.runTrackedActionNamed(LPMessage.ArgCancelAction)
FxALoginHelper.sharedInstance.readyForSyncing()
}))
alert.addAction(UIAlertAction(title: context.stringNamed(LPMessage.ArgAcceptButtonText), style: .default, handler: { (action) -> Void in
// Log accept event and present push permission modal
context.runTrackedActionNamed(LPMessage.ArgAcceptAction)
FxALoginHelper.sharedInstance.requestUserNotifications(UIApplication.shared)
self.prefs?.setBool(true, forKey: AppRequestedUserNotificationsPrefKey)
}))
rootViewController?.present(alert, animated: true, completion: nil)
return true
}
// Register or update the custom Leanplum message
Leanplum.defineAction(LPMessage.FxAPrePush, of: kLeanplumActionKindMessage, withArguments: args, withOptions: [:], withResponder: responder)
}
}
extension UIApplication {
// Extension to get the current top most view controller
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}

View file

@ -0,0 +1,142 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import Storage
import Shared
import XCGLogger
enum ShortcutType: String {
case newTab = "NewTab"
case newPrivateTab = "NewPrivateTab"
case openLastBookmark = "OpenLastBookmark"
case qrCode = "QRCode"
init?(fullType: String) {
guard let last = fullType.components(separatedBy: ".").last else { return nil }
self.init(rawValue: last)
}
var type: String {
return Bundle.main.bundleIdentifier! + ".\(self.rawValue)"
}
}
protocol QuickActionHandlerDelegate {
func handleShortCutItemType(_ type: ShortcutType, userData: [String: NSSecureCoding]?)
}
class QuickActions: NSObject {
fileprivate let log = Logger.browserLogger
static let QuickActionsVersion = "1.0"
static let QuickActionsVersionKey = "dynamicQuickActionsVersion"
static let TabURLKey = "url"
static let TabTitleKey = "title"
fileprivate let lastBookmarkTitle = NSLocalizedString("Open Last Bookmark", tableName: "3DTouchActions", comment: "String describing the action of opening the last added bookmark from the home screen Quick Actions via 3D Touch")
fileprivate let _lastTabTitle = NSLocalizedString("Open Last Tab", tableName: "3DTouchActions", comment: "String describing the action of opening the last tab sent to Firefox from the home screen Quick Actions via 3D Touch")
static var sharedInstance = QuickActions()
var launchedShortcutItem: UIApplicationShortcutItem?
// MARK: Administering Quick Actions
func addDynamicApplicationShortcutItemOfType(_ type: ShortcutType, fromShareItem shareItem: ShareItem, toApplication application: UIApplication) {
var userData = [QuickActions.TabURLKey: shareItem.url]
if let title = shareItem.title {
userData[QuickActions.TabTitleKey] = title
}
QuickActions.sharedInstance.addDynamicApplicationShortcutItemOfType(type, withUserData: userData, toApplication: application)
}
@discardableResult func addDynamicApplicationShortcutItemOfType(_ type: ShortcutType, withUserData userData: [AnyHashable: Any] = [AnyHashable: Any](), toApplication application: UIApplication) -> Bool {
// add the quick actions version so that it is always in the user info
var userData: [AnyHashable: Any] = userData
userData[QuickActions.QuickActionsVersionKey] = QuickActions.QuickActionsVersion
var dynamicShortcutItems = application.shortcutItems ?? [UIApplicationShortcutItem]()
switch type {
case .openLastBookmark:
let openLastBookmarkShortcut = UIMutableApplicationShortcutItem(type: ShortcutType.openLastBookmark.type,
localizedTitle: lastBookmarkTitle,
localizedSubtitle: userData[QuickActions.TabTitleKey] as? String,
icon: UIApplicationShortcutIcon(templateImageName: "quick_action_last_bookmark"),
userInfo: userData
)
if let index = (dynamicShortcutItems.index { $0.type == ShortcutType.openLastBookmark.type }) {
dynamicShortcutItems[index] = openLastBookmarkShortcut
} else {
dynamicShortcutItems.append(openLastBookmarkShortcut)
}
default:
log.warning("Cannot add static shortcut item of type \(type)")
return false
}
application.shortcutItems = dynamicShortcutItems
return true
}
func removeDynamicApplicationShortcutItemOfType(_ type: ShortcutType, fromApplication application: UIApplication) {
guard var dynamicShortcutItems = application.shortcutItems,
let index = (dynamicShortcutItems.index { $0.type == type.type }) else { return }
dynamicShortcutItems.remove(at: index)
application.shortcutItems = dynamicShortcutItems
}
// MARK: Handling Quick Actions
@discardableResult func handleShortCutItem(_ shortcutItem: UIApplicationShortcutItem, withBrowserViewController bvc: BrowserViewController ) -> Bool {
// Verify that the provided `shortcutItem`'s `type` is one handled by the application.
guard let shortCutType = ShortcutType(fullType: shortcutItem.type) else { return false }
DispatchQueue.main.async {
self.handleShortCutItemOfType(shortCutType, userData: shortcutItem.userInfo, browserViewController: bvc)
}
return true
}
fileprivate func handleShortCutItemOfType(_ type: ShortcutType, userData: [String: NSSecureCoding]?, browserViewController: BrowserViewController) {
switch type {
case .newTab:
handleOpenNewTab(withBrowserViewController: browserViewController, isPrivate: false)
case .newPrivateTab:
handleOpenNewTab(withBrowserViewController: browserViewController, isPrivate: true)
// even though we're removing OpenLastTab, it's possible that someone will use an existing last tab quick action to open the app
// the first time after upgrading, so we should still handle it
case .openLastBookmark:
if let urlToOpen = (userData?[QuickActions.TabURLKey] as? String)?.asURL {
handleOpenURL(withBrowserViewController: browserViewController, urlToOpen: urlToOpen)
}
case .qrCode:
handleQRCode(with: browserViewController)
}
}
fileprivate func handleOpenNewTab(withBrowserViewController bvc: BrowserViewController, isPrivate: Bool) {
bvc.openBlankNewTab(focusLocationField: true, isPrivate: isPrivate)
}
fileprivate func handleOpenURL(withBrowserViewController bvc: BrowserViewController, urlToOpen: URL) {
// open bookmark in a non-private browsing tab
bvc.switchToPrivacyMode(isPrivate: false)
// find out if bookmarked URL is currently open
// if so, open to that tab,
// otherwise, create a new tab with the bookmarked URL
bvc.switchToTabForURLOrOpen(urlToOpen, isPrivileged: true)
}
fileprivate func handleQRCode(with vc: QRCodeViewControllerDelegate & UIViewController) {
let qrCodeViewController = QRCodeViewController()
qrCodeViewController.qrCodeDelegate = vc
let controller = UINavigationController(rootViewController: qrCodeViewController)
vc.present(controller, animated: true, completion: nil)
}
}

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>StringsTable</key>
<string>Root</string>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>Title</key>
<string>Debug Settings</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Email Saved Tab State</string>
<key>Key</key>
<string>SettingsBundleEmailTabState</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Email Logs On Launch</string>
<key>Key</key>
<string>SettingsBundleEmailLogsOnLaunch</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Disable Local Web Server</string>
<key>Key</key>
<string>SettingsBundleDisableLocalWebServer</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Skip Session Restore</string>
<key>Key</key>
<string>SettingsBundleSkipSessionRestore</string>
<key>DefaultValue</key>
<false/>
</dict>
</array>
</dict>
</plist>

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 Shared
import SDWebImage
import XCGLogger
private let log = Logger.browserLogger
class TestAppDelegate: AppDelegate {
override func getProfile(_ application: UIApplication) -> Profile {
if let profile = self.profile {
return profile
}
var profile: BrowserProfile
let launchArguments = ProcessInfo.processInfo.arguments
if launchArguments.contains(LaunchArguments.ClearProfile) {
// Use a clean profile for each test session.
log.debug("Deleting all files in 'Documents' directory to clear the profile")
profile = BrowserProfile(localName: "testProfile", syncDelegate: application.syncDelegate, clear: true)
} else {
profile = BrowserProfile(localName: "testProfile", syncDelegate: application.syncDelegate)
}
// Don't show the What's New page.
if launchArguments.contains(LaunchArguments.SkipWhatsNew) {
profile.prefs.setString(AppInfo.appVersion, forKey: LatestAppVersionProfileKey)
}
// Skip the intro when requested by for example tests or automation
if launchArguments.contains(LaunchArguments.SkipIntro) {
profile.prefs.setInt(1, forKey: IntroViewControllerSeenProfileKey)
}
self.profile = profile
return profile
}
override func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// If the app is running from a XCUITest reset all settings in the app
if ProcessInfo.processInfo.arguments.contains(LaunchArguments.ClearProfile) {
resetApplication()
}
return super.application(application, willFinishLaunchingWithOptions: launchOptions)
}
/**
Use this to reset the application between tests.
**/
func resetApplication() {
log.debug("Wiping everything for a clean start.")
// Clear image cache
SDImageCache.shared().clearDisk()
SDImageCache.shared().clearMemory()
// Clear the cookie/url cache
URLCache.shared.removeAllCachedResponses()
let storage = HTTPCookieStorage.shared
if let cookies = storage.cookies {
for cookie in cookies {
storage.deleteCookie(cookie)
}
}
// Clear the documents directory
var rootPath: String = ""
let sharedContainerIdentifier = AppInfo.sharedContainerIdentifier
if let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainerIdentifier) {
rootPath = url.path
} else {
rootPath = (NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0])
}
let manager = FileManager.default
let documents = URL(fileURLWithPath: rootPath)
let docContents = try! manager.contentsOfDirectory(atPath: rootPath)
for content in docContents {
do {
try manager.removeItem(at: documents.appendingPathComponent(content))
} catch {
log.debug("Couldn't delete some document contents.")
}
}
}
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Speed up the animations to 100 times as fast.
defer { application.keyWindow?.layer.speed = 100.0 }
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View file

@ -0,0 +1,88 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import GCDWebServers
import Shared
class WebServer {
private let log = Logger.browserLogger
static let WebServerSharedInstance = WebServer()
class var sharedInstance: WebServer {
return WebServerSharedInstance
}
let server: GCDWebServer = GCDWebServer()
var base: String {
return "http://localhost:\(server.port)"
}
/// The private credentials for accessing resources on this Web server.
let credentials: URLCredential
/// A random, transient token used for authenticating requests.
/// Other apps are able to make requests to our local Web server,
/// so this prevents them from accessing any resources.
fileprivate let sessionToken = UUID().uuidString
init() {
credentials = URLCredential(user: sessionToken, password: "", persistence: .forSession)
}
@discardableResult func start() throws -> Bool {
if !server.isRunning {
try server.start(options: [
GCDWebServerOption_Port: 6571,
GCDWebServerOption_BindToLocalhost: true,
GCDWebServerOption_AutomaticallySuspendInBackground: true,
GCDWebServerOption_AuthenticationMethod: GCDWebServerAuthenticationMethod_Basic,
GCDWebServerOption_AuthenticationAccounts: [sessionToken: ""]
])
}
return server.isRunning
}
/// Convenience method to register a dynamic handler. Will be mounted at $base/$module/$resource
func registerHandlerForMethod(_ method: String, module: String, resource: String, handler: @escaping (_ request: GCDWebServerRequest?) -> GCDWebServerResponse!) {
// Prevent serving content if the requested host isn't a whitelisted local host.
let wrappedHandler = {(request: GCDWebServerRequest?) -> GCDWebServerResponse? in
guard let request = request, request.url.isLocal else {
return GCDWebServerResponse(statusCode: 403)
}
return handler(request)
}
server.addHandler(forMethod: method, path: "/\(module)/\(resource)", request: GCDWebServerRequest.self, processBlock: wrappedHandler)
}
/// Convenience method to register a resource in the main bundle. Will be mounted at $base/$module/$resource
func registerMainBundleResource(_ resource: String, module: String) {
if let path = Bundle.main.path(forResource: resource, ofType: nil) {
server.addGETHandler(forPath: "/\(module)/\(resource)", filePath: path, isAttachment: false, cacheAge: UInt.max, allowRangeRequests: true)
}
}
/// Convenience method to register all resources in the main bundle of a specific type. Will be mounted at $base/$module/$resource
func registerMainBundleResourcesOfType(_ type: String, module: String) {
for path: String in Bundle.paths(forResourcesOfType: type, inDirectory: Bundle.main.bundlePath) {
if let resource = NSURL(string: path)?.lastPathComponent {
server.addGETHandler(forPath: "/\(module)/\(resource)", filePath: path as String, isAttachment: false, cacheAge: UInt.max, allowRangeRequests: true)
} else {
log.warning("Unable to locate resource at path: '\(path)'")
}
}
}
/// Return a full url, as a string, for a resource in a module. No check is done to find out if the resource actually exist.
func URLForResource(_ resource: String, module: String) -> String {
return "\(base)/\(module)/\(resource)"
}
func baseReaderModeURL() -> String {
return WebServer.sharedInstance.URLForResource("page", module: "reader-mode")
}
}

View file

@ -0,0 +1,16 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Shared
private var appDelegate: AppDelegate.Type
if AppConstants.IsRunningTest {
appDelegate = TestAppDelegate.self
} else {
appDelegate = AppDelegate.self
}
private let pointer = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc))
UIApplicationMain(CommandLine.argc, pointer, NSStringFromClass(UIApplication.self), NSStringFromClass(appDelegate))

View file

@ -0,0 +1,674 @@
<html>
<head>
<meta name=viewport content="width=device-width, initial-scale=1">
<style type="text/css">
@font-face {
font-family: sans-serif;
src: url('/reader-mode/fonts/FiraSans-Regular.ttf');
}
body,p,h1,h2,h3 {
font-family: sans-serif;
}
h2 {
padding: 0;
margin: 0;
text-transform: uppercase;
}
.link {
margin: 0;
text-size: 55%;
}
</style>
</head>
<h1>Licenses</h1>
<h2>Firefox for iOS</h2>
<p class="link"><a href="https://github.com/mozilla/firefox-ios">github.com/mozilla/firefox-ios</a></p>
<div class="text">
<h3 id="mozilla-public-license-version-2.0">Mozilla Public License<br>Version 2.0</h3>
<h4 id="definitions">1. Definitions</h4>
<dl>
<dt>1.1. “Contributor”</dt>
<dd><p>means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software.</p>
</dd>
<dt>1.2. “Contributor Version”</dt>
<dd><p>means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributors Contribution.</p>
</dd>
<dt>1.3. “Contribution”</dt>
<dd><p>means Covered Software of a particular Contributor.</p>
</dd>
<dt>1.4. “Covered Software”</dt>
<dd><p>means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof.</p>
</dd>
<dt>1.5. “Incompatible With Secondary Licenses”</dt>
<dd><p>means</p>
<ol type="a">
<li><p>that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or</p></li>
<li><p>that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License.</p></li>
</ol>
</dd>
<dt>1.6. “Executable Form”</dt>
<dd><p>means any form of the work other than Source Code Form.</p>
</dd>
<dt>1.7. “Larger Work”</dt>
<dd><p>means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software.</p>
</dd>
<dt>1.8. “License”</dt>
<dd><p>means this document.</p>
</dd>
<dt>1.9. “Licensable”</dt>
<dd><p>means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License.</p>
</dd>
<dt>1.10. “Modifications”</dt>
<dd><p>means any of the following:</p>
<ol type="a">
<li><p>any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or</p></li>
<li><p>any new file in Source Code Form that contains any Covered Software.</p></li>
</ol>
</dd>
<dt>1.11. “Patent Claims” of a Contributor</dt>
<dd><p>means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version.</p>
</dd>
<dt>1.12. “Secondary License”</dt>
<dd><p>means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses.</p>
</dd>
<dt>1.13. “Source Code Form”</dt>
<dd><p>means the form of the work preferred for making modifications.</p>
</dd>
<dt>1.14. “You” (or “Your”)</dt>
<dd><p>means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.</p>
</dd>
</dl>
<h4 id="license-grants-and-conditions">2. License Grants and Conditions</h4>
<h3 id="grants">2.1. Grants</h3>
<p>Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:</p>
<ol type="a">
<li><p>under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and</p></li>
<li><p>under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version.</p></li>
</ol>
<h3 id="effective-date">2.2. Effective Date</h3>
<p>The licenses granted in Section&nbsp;2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution.</p>
<h3 id="limitations-on-grant-scope">2.3. Limitations on Grant Scope</h3>
<p>The licenses granted in this Section&nbsp;2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section&nbsp;2.1(b) above, no patent license is granted by a Contributor:</p>
<ol type="a">
<li><p>for any code that a Contributor has removed from Covered Software; or</p></li>
<li><p>for infringements caused by: (i) Your and any other third partys modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or</p></li>
<li><p>under Patent Claims infringed by Covered Software in the absence of its Contributions.</p></li>
</ol>
<p>This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section&nbsp;3.4).</p>
<h3 id="subsequent-licenses">2.4. Subsequent Licenses</h3>
<p>No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section&nbsp;10.2) or under the terms of a Secondary License (if permitted under the terms of Section&nbsp;3.3).</p>
<h3 id="representation">2.5. Representation</h3>
<p>Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License.</p>
<h3 id="fair-use">2.6. Fair Use</h3>
<p>This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents.</p>
<h3 id="conditions">2.7. Conditions</h3>
<p>Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section&nbsp;2.1.</p>
<h4 id="responsibilities">3. Responsibilities</h4>
<h3 id="distribution-of-source-form">3.1. Distribution of Source Form</h3>
<p>All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients rights in the Source Code Form.</p>
<h3 id="distribution-of-executable-form">3.2. Distribution of Executable Form</h3>
<p>If You distribute Covered Software in Executable Form then:</p>
<ol type="a">
<li><p>such Covered Software must also be made available in Source Code Form, as described in Section&nbsp;3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and</p></li>
<li><p>You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients rights in the Source Code Form under this License.</p></li>
</ol>
<h3 id="distribution-of-a-larger-work">3.3. Distribution of a Larger Work</h3>
<p>You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s).</p>
<h3 id="notices">3.4. Notices</h3>
<p>You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies.</p>
<h3 id="application-of-additional-terms">3.5. Application of Additional Terms</h3>
<p>You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction.</p>
<h4 id="inability-to-comply-due-to-statute-or-regulation">4. Inability to Comply Due to Statute or Regulation</h4>
<p>If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it.</p>
<h4 id="termination">5. Termination</h4>
<p>5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice.</p>
<p>5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section&nbsp;2.1 of this License shall terminate.</p>
<p>5.3. In the event of termination under Sections&nbsp;5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination.</p>
<h4 id="disclaimer-of-warranty">6. Disclaimer of Warranty</h4>
<p><em>Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer.</em></p>
<h4 id="limitation-of-liability">7. Limitation of Liability</h4>
<p><em>Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such partys negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You.</em></p>
<h4 id="litigation">8. Litigation</h4>
<p>Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a partys ability to bring cross-claims or counter-claims.</p>
<h4 id="miscellaneous">9. Miscellaneous</h4>
<p>This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor.</p>
<h4 id="versions-of-the-license">10. Versions of the License</h4>
<h3 id="new-versions">10.1. New Versions</h3>
<p>Mozilla Foundation is the license steward. Except as provided in Section&nbsp;10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number.</p>
<h3 id="effect-of-new-versions">10.2. Effect of New Versions</h3>
<p>You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward.</p>
<h3 id="modified-versions">10.3. Modified Versions</h3>
<p>If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License).</p>
<h3 id="distributing-source-code-form-that-is-incompatible-with-secondary-licenses">10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses</h3>
<p>If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached.</p>
<h4 id="exhibit-a---source-code-form-license-notice">Exhibit A - Source Code Form License Notice</h4>
<blockquote>
<p>This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.</p>
</blockquote>
<p>If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice.</p>
<p>You may add additional accurate notices of copyright ownership.</p>
<h4 id="exhibit-b---incompatible-with-secondary-licenses-notice">Exhibit B - “Incompatible With Secondary Licenses” Notice</h4>
<blockquote>
<p>This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0.</p>
</blockquote>
</div>
<center><p>-</p></center>
<!-- -->
<h2>Alamofire</h2>
<p class="link"><a href="http://alamofire.org">alamofire.org</a></p>
<div class="text">
<p>Copyright (c) 2014 Alamofire (http://alamofire.org/)</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.</p>
</div>
<center><p>-</p></center>
<!-- -->
<h2>Base32</h2>
<p class="link"><a href="https://github.com/norio-nomura/Base32">github.com/norio-nomura/Base32</a></p>
<div class="text">
<p>The MIT License (MIT)</p>
<p>Copyright (c) 2015 Norio Nomura</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.</p>
</div>
<center><p>-</p></center>
<h2>Box</h2>
<p class="link"><a href="https://github.com/robrix/Box">github.com/robrix/Box</a></p>
<div class="text">
<p>The MIT License (MIT)</p>
<p>Copyright (c) 2014 Rob Rix</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE</p>
</div>
<center><p>-</p></center>
<h2>Deferred</h2>
<p class="link"><a href="https://github.com/bignerdranch/Deferred">github.com/bignerdranch/Deferred</a></p>
<div class="text">
<p>Copyright (c) 2014 John Gallagher &lt;jgallagher@bignerdranch.com&gt;
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.</p>
</div>
<h2>FilledPageControl</h2>
<p>Copyright (c) 2016 Kyle Zaragoza <popwarsweet@gmail.com></p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.</p>
<h2>GCDWebServer</h2>
<p class="link"><a href="https://github.com/swisspol/GCDWebServer">github.com/swisspol/GCDWebServer</a></p>
<div class="text">
<p>Copyright (c) 2012-2014, Pierre-Olivier Latour</p>
<p>All rights reserved.</p>
<p>Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:</p>
<ul>
<li>Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.</li>
<li>Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.</li>
<li>The name of Pierre-Olivier Latour may not be used to endorse
or promote products derived from this software without specific
prior written permission.</li>
</ul>
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>
</div>
<center><p>-</p></center>
<h2>KIF</h2>
<p class="link"><a href="https://github.com/kif-framework/KIF">https://github.com/kif-framework/KIF</a></p>
<div class="text">
<p>Copyright 2011 Square, Inc.</p>
<p>A full list of contributors is available at https://github.com/square/KIF/contributors</p>
<p>Licensed under the Apache License, Version 2.0 (the "License");</p>
you may not use this file except in compliance with the License.
You may obtain a copy of the License at</p>
<p>http://www.apache.org/licenses/LICENSE-2.0</p>
<p>Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.</p>
</div>
<h2>Result</h2>
<p class="link"><a href="https://github.com/bignerdranch/Result">github.com/bignerdranch/Result</a></p>
<div class="text">
<p>Copyright (c) 2014 John Gallagher &lt;jgallagher@bignerdranch.com&gt;</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.</p>
</div>
<h2>SDWebImage</h2>
<p class="link"><a href="https://github.com/rs/SDWebImage">github.com/rs/SDWebImage</a></p>
<div class="text">
<p>Copyright (c) 2009 Olivier Poitrey &lt;rs@dailymotion.com&gt;</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.</p>
</div>
<center><p>-</p></center>
<h2>SQLite.swift</h2>
<p class="link"><a href="https://github.com/stephencelis/SQLite.swift">github.com/stephencelis/SQLite.swift</a></p>
<div class="text">
<p>(The MIT License)</p>
<p>Copyright (c) 2014-2015 Stephen Celis (&lt;stephen@stephencelis.com&gt;)</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.</p>
</div>
<center><p>-</p></center>
<h2>Fuzi</h2>
<p class="link"><a href="https://github.com/cezheng/Fuzi">github.com/cezheng/Fuzi</a></p>
<div class="text">
<p>Copyright (c) 2015 Ce Zheng</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.</p>
</div>
<center><p>-</p></center>
<h2>Snap</h2>
<p class="link"><a href="https://github.com/SnapKit">github.com/SnapKit</a></p>
<div class="text">
<p>Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.</p>
</div>
<center><p>-</p></center>
<h2>XCGLogger</h2>
<p class="link"><a href="https://github.com/DaveWoodCom/XCGLogger">github.com/DaveWoodCom/XCGLogger</a></p>
<div class="text">
<p>The MIT License (MIT)</p>
<p>Copyright (c) 2014 Dave Wood, Cerebral Gardens http://www.cerebralgardens.com/</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.</p>
</div>
<center><p>-</p></center>
<h2>Readability</h2>
<p class="link"><a href="https://github.com/mozilla/readability">github.com/mozilla/readability</a></p>
<div class="text">
<p>Copyright (c) 2010 Arc90 Inc</p>
<p>Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at</p>
<p>http://www.apache.org/licenses/LICENSE-2.0</p>
<p>Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.</p>
</div>
<center><p>-</p></center>
<h2>sqlchipher</h2>
<p class="link"><a href="https://github.com/sqlcipher/sqlcipher">github.com/sqlcipher/sqlcipher</a></p>
<div class="text">
<p>Copyright (c) 2008, ZETETIC LLC</p>
<p>All rights reserved.</p>
<p>Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:</p>
<ul>
<li>Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.</li>
<li>Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.</li>
<li>Neither the name of the ZETETIC LLC nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.</li>
</ul>
<p>THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>
</div>
<center><p>-</p></center>
<h2>SwiftKeychainWrapper</h2>
<p class="link"><a href="https://github.com/jrendel/SwiftKeychainWrapper">github.com/jrendel/SwiftKeychainWrapper</a></p>
<div class="text">
<p>The MIT License (MIT)</p>
<p>Copyright (c) 2014 Jason</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.</p>
</div>
<center><p>-</p></center>
<h2>Swift-JSON</h2>
<p class="link"><a href="https://github.com/dankogai/swift-json">https://github.com/dankogai/swift-json</a></p>
<div class="text">
<p>The MIT License (MIT)</p>
<p>Copyright (c) 2014 Dan Kogai</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.</p>
</div>
<div>
<h2>UIImageColors</h2>
<p class="link"><a href="https://github.com/jathu/UIImageColors">github.com/jathu/UIImageColors</a></p>
<p>Created by Jathu Satkunarajah (@jathu) on 2015-06-11 - Toronto</p>
<p>Original Cocoa version by Panic Inc. - Portland</p>
<p>MIT License</p>
<p>Copyright (c) 2015 Jathu Satkunarajah</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.</p>
</div>
<h2>UIImageViewAligned</h2>
<p class="link"><a href="https://github.com/reydanro/UIImageViewAligned">github.com/reydanro/UIImageViewAligned</a></p>
<div class="text">
<p>The MIT License (MIT)</p>
<p>Copyright (c) 2013 Andrei Stanescu</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
</div>
</html>

View file

@ -0,0 +1,76 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
body {
background-color: white;
padding: 0 65px;
color: #333;
font-size: 13px;
font-family: "Helvetica Neue Medium" sans-serif;
}
h1 {
color: #333;
font-size: 16px;
font-family: "Helvetica Neue Medium" sans-serif;
font-weight: 300;
margin-bottom: 25px;
}
/* Add a set of stripes at the top of pages */
#decoration {
background-image: repeating-linear-gradient(-65deg, #ed2, #ed2 10px,
#fe3 10px, #fe3 20px,
#ed2 20px);
position: fixed;
top: 0;
left: 0;
height: 32px;
width: 100%;
}
a {
color: rgb(76, 158, 255);
text-decoration: none;
}
button {
width: 100%;
border: none;
padding: 1rem;
font-family: "Helvetica Neue Medium" sans-serif;
background-color: rgb(76, 158, 255);
color: white;
font-size: 16px;
font-weight: 300;
border-radius: 5px;
}
#errorPageContainer {
max-width: 300px;
margin: 0 auto;
-webkit-transform: translateY(70px);
padding-bottom: 10px;
}
#certErrorCode {
color: #999;
}
#advancedButton {
background-color: white;
color: #777;
border: 1px solid #aaa;
}
#advancedContent {
margin-bottom: 20px;
}

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/. -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>%error_title%</title>
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="CertError.css" type="text/css" media="all" />
<script type="application/javascript">
// If the user swipes back and forth to this tab, we want to make sure we make an attempt to reload the original page.
var fresh = true;
window.addEventListener('popstate', function () {
if (fresh)
fresh = false;
else
webkit.messageHandlers.localRequestHelper.postMessage({ type: "reload" });
});
</script>
</head>
<body>
<div id="errorPageContainer">
<h1 id="errorTitleText">%error_title%</h1>
<p id="actions">%actions%</p>
<p>%long_description%</p>
<button id="advancedButton">%advanced_button%</button>
<div id="advancedContent">
<p id="certErrorCode">%cert_error%</p>
<p>%warning_advanced1%</p>
<p>%warning_advanced2%</p>
<div>%warning_actions%</div>
</div>
</div>
<div id="decoration"></div>
<script>
var advancedContent = document.getElementById("advancedContent");
advancedContent.style.display = "none";
document.getElementById("advancedButton").onclick = function () {
this.style.display = "none";
advancedContent.style.display = "block";
};
</script>
</body>
</html>

View file

@ -0,0 +1,28 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
window.addEventListener('touchstart', function(evt) {
var target = evt.target;
var targetLink = target.closest('a');
var targetImage = target.closest('img');
if (!targetLink && !targetImage) {
return;
}
var data = {};
if (targetLink) {
data.link = targetLink.href;
}
if (targetImage) {
data.image = targetImage.src;
}
if (data.link || data.image) {
webkit.messageHandlers.contextMenuMessageHandler.postMessage(data);
}
}, true);

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/. */
"use strict";
if (!window.__firefox__) {
Object.defineProperty(window, '__firefox__', {
enumerable: false,
configurable: false,
writable: false,
value: {}
});
}
Object.defineProperty(window.__firefox__, 'searchQueryForField', {
enumerable: false,
configurable: false,
writable: false,
value: function() {
var input = document.activeElement;
if (input.tagName.toLowerCase() !== 'input') return null;
var form = input.form;
if (!form || form.method.toLowerCase() != 'get') return null;
var inputs = form.getElementsByTagName('input');
inputs = Array.prototype.slice.call(inputs, 0);
var params = inputs.map(function(element) {
if (element.name == input.name) return [element.name, '{searchTerms}'].join('=');
return [element.name, element.value].map(encodeURIComponent).join('=');
});
var selectFields = form.getElementsByTagName('select');
selectFields = Array.prototype.slice.call(selectFields, 0);
var selectParams = selectFields.map(function(e){
return [e.name, e.options[e.selectedIndex].value].map(encodeURIComponent).join('=');
});
params = params.concat(selectParams);
if (!form.action) return null; //an invalid form.
var url = [form.action, params.join('&')].join('?');
return url;
}
});

View file

@ -0,0 +1,60 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
if (!window.__firefox__) {
Object.defineProperty(window, '__firefox__', {
enumerable: false,
configurable: false,
writable: false,
value: {}
});
}
Object.defineProperty(window.__firefox__, 'favicons', {
enumerable: false,
configurable: false,
writable: false,
value: (function() {
// These integers should be kept in sync with the IconType raw-values
var ICON = 0;
var APPLE = 1;
var APPLE_PRECOMPOSED = 2;
var GUESS = 3;
var selectors = {
"link[rel~='icon']": ICON,
"link[rel='apple-touch-icon']": APPLE,
"link[rel='apple-touch-icon-precomposed']": APPLE_PRECOMPOSED
};
function getAll() {
var favicons = {};
for (var selector in selectors) {
var icons = document.querySelectorAll(selector);
for (var i = 0; i < icons.length; i++) {
var href = icons[i].href;
favicons[href] = selectors[selector];
}
}
// If we didn't find anything in the page, look to see if a favicon.ico file exists for the domain
if (Object.keys(favicons).length === 0) {
var href = document.location.origin + "/favicon.ico";
favicons[href] = GUESS;
}
return favicons;
}
function getFavicons() {
var favicons = getAll();
webkit.messageHandlers.faviconsMessageHandler.postMessage(favicons);
}
return {
getFavicons: getFavicons
};
})()
});

View file

@ -0,0 +1,292 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
(function() {
"use strict";
var DEBUG_ENABLED = false;
var MATCH_HIGHLIGHT_ACTIVE = "#f19750";
var MATCH_HIGHLIGHT_INACTIVE = "#ffde49";
var SCROLL_INTERVAL_INCREMENT = 5;
var SCROLL_INTERVAL_DURATION = 400;
var SCROLL_OFFSET = 60;
var activeHighlightSpan = null;
var lastSearch;
var scrollInterval;
var activeIndex = 0;
var highlightSpans = [];
function debug(str) {
if (DEBUG_ENABLED) {
console.log("FindInPage: " + str);
}
}
function isElementVisible(elem) {
return getComputedStyle(elem).visibility !== "hidden";
}
function isRectInViewport(rect) {
var left = rect.left + document.body.scrollLeft;
var right = rect.right + document.body.scrollLeft;
var top = rect.top + document.body.scrollTop;
var bottom = rect.bottom + document.body.scrollTop;
return rect.width > 0 &&
rect.height > 0 &&
right >= 0 &&
bottom >= 0 &&
left <= document.body.scrollWidth &&
top <= document.body.scrollHeight;
}
function findMatches(text) {
// For case-insensitive matching.
var lowerText = text.toLocaleLowerCase();
var upperText = text.toLocaleUpperCase();
var matches = [];
var range = document.createRange();
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
var textLength = text.length;
var node;
while (node = walker.nextNode()) {
var textContent = node.textContent;
findString: for (var i = 0; i < textContent.length - textLength + 1; ++i) {
for (var j = 0; j < textLength; ++j) {
var nextChar = textContent[i + j];
if (lowerText[j] !== nextChar && upperText[j] !== nextChar) {
continue findString;
}
}
// This node is a TextNode, not an Element. Its parent is the nearest Element.
var element = node.parentNode;
// Find the rect of just the text for this match.
range.setStart(node, i);
range.setEnd(node, i + textLength);
var textRect = range.getBoundingClientRect();
// We have a match, but we need to make sure it's visible. The condition
// below checks the following cases:
// * If this element or any of its parents has style visibility hidden.
// The visibility style is inherited, so we need to check only this
// element and not all of its ancestors.
// * If the highlight will be outside of the page's bounds. We determine
// this by comparing the bounds of the text rect.
// * If the element style display is set to none. display:none collapses
// the element's space, so this will again be detected by looking at
// the text's rect: if the element is collapsed, the width and height
// will be zero.
if (isElementVisible(element) && isRectInViewport(textRect)) {
matches.push({ node: node, index: i });
// Resume searching after this match to prevent overlapping results.
i += textLength- 1;
}
}
}
return matches;
}
function flattenNode(node) {
var parent = node.parentNode;
if (!parent) {
return;
}
while (node.firstChild) {
parent.insertBefore(node.firstChild, node);
}
node.remove();
parent.normalize();
}
function clearHighlights() {
if (highlightSpans.length > 0) {
for (var span of highlightSpans) {
flattenNode(span);
}
highlightSpans = [];
}
activeHighlightSpan = null;
}
function highlightAllMatches(text) {
debug("Searching: " + text);
clearHighlights();
if (!text.trim()) {
webkit.messageHandlers.findInPageHandler.postMessage({ totalResults: 0 });
return;
}
var range = document.createRange();
var matches = findMatches(text);
var highlightTemplate = document.createElement("span");
highlightTemplate.style.backgroundColor = MATCH_HIGHLIGHT_INACTIVE;
// If there are multiple matches in the same node, inserting a highlight span before other matches
// in that node will invalidate other matches since the node itself changes. By iterating through
// results in reverse, we highlight matches last in the node first so earlier matches are unaffected.
for (var i = matches.length - 1; i >= 0; --i) {
var match = matches[i];
var highlight = highlightTemplate.cloneNode();
range.setStart(match.node, match.index);
range.setEnd(match.node, match.index + text.length);
range.surroundContents(highlight);
highlightSpans.unshift(highlight);
}
debug(matches.length + " highlighted rects created!");
webkit.messageHandlers.findInPageHandler.postMessage({ totalResults: matches.length });
}
function getIDForRect(rect) {
return rect.top + "," + rect.bottom + "," + rect.left + "," + rect.right;
}
function updateActiveHighlight() {
// Reset the color of the previous highlight.
if (activeHighlightSpan) {
activeHighlightSpan.style.backgroundColor = MATCH_HIGHLIGHT_INACTIVE;
}
if (!highlightSpans.length) {
return;
}
activeHighlightSpan = highlightSpans[activeIndex];
activeHighlightSpan.style.backgroundColor = MATCH_HIGHLIGHT_ACTIVE;
// Find the position of the element centered on the screen, then scroll to it.
var rect = activeHighlightSpan.getBoundingClientRect();
var top = SCROLL_OFFSET + rect.top + scrollY - window.innerHeight / 2;
var left = rect.left + scrollX - window.innerWidth / 2;
left = clamp(left, 0, document.body.scrollWidth);
top = clamp(top, 0, document.body.scrollHeight);
scrollToSelection(left, top, SCROLL_INTERVAL_DURATION);
debug("Scrolled to: " + left + ", " + top);
}
function scrollToSelection(left, top, duration) {
var time = 0;
var startX = scrollX;
var startY = scrollY;
clearInterval(scrollInterval);
scrollInterval = setInterval(function() {
var xStep = easeOutCubic(time, startX, left - startX, duration);
var yStep = easeOutCubic(time, startY, top - startY, duration);
window.scrollTo(xStep, yStep);
time += SCROLL_INTERVAL_INCREMENT;
if (time >= duration) {
clearInterval(scrollInterval);
}
}, SCROLL_INTERVAL_INCREMENT);
}
function easeOutCubic(currentTime, startValue, changeInValue, duration) {
return changeInValue * (Math.pow(currentTime / duration - 1, 3) + 1) + startValue;
}
function clamp(number, min, max) {
return Math.max(min, Math.min(number, max));
}
function updateSearch(text) {
if (lastSearch == text) {
// The text is the same, so we're either finding either the next or previous result.
var totalResults = highlightSpans.length;
activeIndex = (activeIndex + totalResults) % totalResults;
} else {
// Store the current active rect to decide which new match should be active.
var activeHighlightRect = null;
if (activeHighlightSpan) {
activeHighlightRect = activeHighlightSpan.getBoundingClientRect();
}
// The search text changed, so scan the page for new results.
highlightAllMatches(text);
// If we found a match at or after the last match, use that position
// instead of starting again from the top.
activeIndex = 0;
if (activeHighlightRect) {
for (var i = 0; i < highlightSpans.length; i++) {
var highlight = highlightSpans[i];
var highlightRect = highlight.getBoundingClientRect();
if ((highlightRect.top == activeHighlightRect.top && highlightRect.left >= activeHighlightRect.left) ||
(highlightRect.top > activeHighlightRect.top)) {
activeIndex = i;
break;
}
}
}
lastSearch = text;
}
// Update the UI with the current match index.
var currentResult = highlightSpans.length ? activeIndex + 1 : 0;
webkit.messageHandlers.findInPageHandler.postMessage({ currentResult: currentResult });
updateActiveHighlight();
}
if (!window.__firefox__) {
Object.defineProperty(window, '__firefox__', {
enumerable: false,
configurable: false,
writable: false,
value: {}
});
}
Object.defineProperty(window.__firefox__, 'find', {
enumerable: false,
configurable: false,
writable: false,
value: function(text) {
updateSearch(text);
}
});
Object.defineProperty(window.__firefox__, 'findNext', {
enumerable: false,
configurable: false,
writable: false,
value: function(text) {
activeIndex++;
updateSearch(text);
}
});
Object.defineProperty(window.__firefox__, 'findPrevious', {
enumerable: false,
configurable: false,
writable: false,
value: function(text) {
activeIndex--;
updateSearch(text);
}
});
Object.defineProperty(window.__firefox__, 'findDone', {
enumerable: false,
configurable: false,
writable: false,
value: function() {
clearHighlights();
lastSearch = null;
}
});
})();

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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/. */
/**
* Transport postMessage events from the fxa-content-server to the embedding
* webview.
*/
"use strict";
function handleAccountsCommand(evt) {
webkit.messageHandlers.accountsCommandHandler.postMessage({ type: evt.type, detail: evt.detail });
};
window.addEventListener("FirefoxAccountsCommand", handleAccountsCommand);

View file

@ -0,0 +1,36 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
(function() {
var nativeHistoryPushState = window.history.pushState;
var nativeHistoryReplaceState = window.history.replaceState;
// We need to catch calls to `history.pushState()` in order to
// notify the BrowserViewController so that history can be
// recorded in single-page web applications that do all rendering
// on the client side.
window.history.pushState = function(state, title, url) {
nativeHistoryPushState.apply(this, arguments);
webkit.messageHandlers.historyStateHelper.postMessage({
pushState: true,
state: state,
title: title,
url: url
});
};
// We need to catch calls to `history.replaceState()` in order to
// notify the BrowserViewController so that history can be
// recorded in single-page web applications that do all rendering
// on the client side.
window.history.replaceState = function(state, title, url) {
nativeHistoryReplaceState.apply(this, arguments);
webkit.messageHandlers.historyStateHelper.postMessage({
replaceState: true,
state: state,
title: title,
url: url
});
};
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "AddSearch.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "AddSearch@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "AddSearch@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,150 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "icon-40.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "icon-60.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "icon-29.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "icon-29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "icon-29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "icon-40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "icon-40@3x.png",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "57x57",
"scale" : "1x"
},
{
"idiom" : "iphone",
"size" : "57x57",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "icon-60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "icon-60@3x.png",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "icon-29.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "icon-29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "icon-40.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "icon-40@2x.png",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "50x50",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "50x50",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "72x72",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "72x72",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "icon-76.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "icon-76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "icon-83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "icon-512@2x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,151 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "icon-40.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "icon-60.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "icon-29.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "icon-29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "icon-29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "icon-40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "icon-40@3x.png",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "57x57",
"scale" : "1x"
},
{
"idiom" : "iphone",
"size" : "57x57",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "icon-60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "icon-60@3x.png",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "icon-40.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "icon-29.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "icon-29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "icon-40.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "icon-40@2x.png",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "50x50",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "50x50",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "72x72",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "72x72",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "icon-76.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "icon-76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "icon-83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "icon-512@2x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,149 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-60@2x.png",
"scale" : "3x"
},
{
"size" : "57x57",
"idiom" : "iphone",
"filename" : "Icon.png",
"scale" : "1x"
},
{
"size" : "57x57",
"idiom" : "iphone",
"filename" : "Icon@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-60@3x.png",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-Small.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-Small@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-Small-40.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-Small-40@2x.png",
"scale" : "2x"
},
{
"size" : "50x50",
"idiom" : "ipad",
"filename" : "Icon-Small-50.png",
"scale" : "1x"
},
{
"size" : "50x50",
"idiom" : "ipad",
"filename" : "Icon-Small-50@2x.png",
"scale" : "2x"
},
{
"size" : "72x72",
"idiom" : "ipad",
"filename" : "Icon-72.png",
"scale" : "1x"
},
{
"size" : "72x72",
"idiom" : "ipad",
"filename" : "Icon-72@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-76.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-iPadPro@2x.png",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "close.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "close@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "close@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "down-caret.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "down-caret@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "down-caret@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "up-caret.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "up-caret@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "up-caret@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Some files were not shown because too many files have changed in this diff Show more