Revision control

1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
* License, v. 2.0. If a copy of the MPL was not distributed with this
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
// IMPORTANT!: Please take into consideration when adding new imports to
6
// this file that it is utilized by external components besides the core
7
// application (i.e. App Extensions). Introducing new dependencies here
8
// may have unintended negative consequences for App Extensions such as
9
// increased startup times which may lead to termination by the OS.
10
import Account
11
import Shared
12
import Storage
13
import Sync
14
import XCGLogger
15
import SwiftKeychainWrapper
16
17
// Import these dependencies ONLY for the main `Client` application target.
18
#if MOZ_TARGET_CLIENT
19
import SwiftyJSON
20
import SyncTelemetry
21
#endif
22
23
private let log = Logger.syncLogger
24
25
public let ProfileRemoteTabsSyncDelay: TimeInterval = 0.1
26
27
public protocol SyncManager {
28
var isSyncing: Bool { get }
29
var lastSyncFinishTime: Timestamp? { get set }
30
var syncDisplayState: SyncDisplayState? { get }
31
32
func hasSyncedHistory() -> Deferred<Maybe<Bool>>
33
func hasSyncedLogins() -> Deferred<Maybe<Bool>>
34
35
func syncClients() -> SyncResult
36
func syncClientsThenTabs() -> SyncResult
37
func syncHistory() -> SyncResult
38
func syncLogins() -> SyncResult
39
func syncBookmarks() -> SyncResult
40
@discardableResult func syncEverything(why: SyncReason) -> Success
41
func syncNamedCollections(why: SyncReason, names: [String]) -> Success
42
43
// The simplest possible approach.
44
func beginTimedSyncs()
45
func endTimedSyncs()
46
func applicationDidEnterBackground()
47
func applicationDidBecomeActive()
48
49
func onNewProfile()
50
@discardableResult func onRemovedAccount(_ account: Account.FirefoxAccount?) -> Success
51
@discardableResult func onAddedAccount() -> Success
52
}
53
54
typealias SyncFunction = (SyncDelegate, Prefs, Ready, SyncReason) -> SyncResult
55
56
class ProfileFileAccessor: FileAccessor {
57
convenience init(profile: Profile) {
58
self.init(localName: profile.localName())
59
}
60
61
init(localName: String) {
62
let profileDirName = "profile.\(localName)"
63
64
// Bug 1147262: First option is for device, second is for simulator.
65
var rootPath: String
66
let sharedContainerIdentifier = AppInfo.sharedContainerIdentifier
67
if let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainerIdentifier) {
68
rootPath = url.path
69
} else {
70
log.error("Unable to find the shared container. Defaulting profile location to ~/Documents instead.")
71
rootPath = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
72
}
73
74
super.init(rootPath: URL(fileURLWithPath: rootPath).appendingPathComponent(profileDirName).path)
75
}
76
}
77
78
class CommandStoringSyncDelegate: SyncDelegate {
79
let profile: Profile
80
81
init(profile: Profile) {
82
self.profile = profile
83
}
84
85
public func displaySentTab(for url: URL, title: String, from deviceName: String?) {
86
let item = ShareItem(url: url.absoluteString, title: title, favicon: nil)
87
_ = self.profile.queue.addToQueue(item)
88
}
89
}
90
91
/**
92
* A Profile manages access to the user's data.
93
*/
94
protocol Profile: AnyObject {
95
var places: RustPlaces { get }
96
var prefs: Prefs { get }
97
var queue: TabQueue { get }
98
var searchEngines: SearchEngines { get }
99
var files: FileAccessor { get }
100
var history: BrowserHistory & SyncableHistory & ResettableSyncStorage { get }
101
var metadata: Metadata { get }
102
var recommendations: HistoryRecommendations { get }
103
var favicons: Favicons { get }
104
var logins: RustLogins { get }
105
var certStore: CertStore { get }
106
var recentlyClosedTabs: ClosedTabsStore { get }
107
var panelDataObservers: PanelDataObservers { get }
108
109
#if !MOZ_TARGET_NOTIFICATIONSERVICE
110
var readingList: ReadingList { get }
111
#endif
112
113
var isShutdown: Bool { get }
114
115
/// WARNING: Only to be called as part of the app lifecycle from the AppDelegate
116
/// or from App Extension code.
117
func _shutdown()
118
119
/// WARNING: Only to be called as part of the app lifecycle from the AppDelegate
120
/// or from App Extension code.
121
func _reopen()
122
123
// I got really weird EXC_BAD_ACCESS errors on a non-null reference when I made this a getter.
125
func localName() -> String
126
127
// URLs and account configuration.
128
var accountConfiguration: FirefoxAccountConfiguration { get }
129
130
// Do we have an account at all?
131
func hasAccount() -> Bool
132
133
// Do we have an account that (as far as we know) is in a syncable state?
134
func hasSyncableAccount() -> Bool
135
136
func getAccount() -> Account.FirefoxAccount?
137
func removeAccount()
138
func setAccount(_ account: Account.FirefoxAccount)
139
func flushAccount()
140
141
func getClients() -> Deferred<Maybe<[RemoteClient]>>
142
func getCachedClients()-> Deferred<Maybe<[RemoteClient]>>
143
func getClientsAndTabs() -> Deferred<Maybe<[ClientAndTabs]>>
144
func getCachedClientsAndTabs() -> Deferred<Maybe<[ClientAndTabs]>>
145
146
func cleanupHistoryIfNeeded()
147
148
@discardableResult func storeTabs(_ tabs: [RemoteTab]) -> Deferred<Maybe<Int>>
149
150
func sendItem(_ item: ShareItem, toDevices devices: [RemoteDevice]) -> Success
151
152
var syncManager: SyncManager! { get }
153
}
154
155
fileprivate let PrefKeyClientID = "PrefKeyClientID"
156
extension Profile {
157
var clientID: String {
158
let clientID: String
159
if let id = prefs.stringForKey(PrefKeyClientID) {
160
clientID = id
161
} else {
162
clientID = UUID().uuidString
163
prefs.setString(clientID, forKey: PrefKeyClientID)
164
}
165
return clientID
166
}
167
}
168
169
open class BrowserProfile: Profile {
170
fileprivate let name: String
171
fileprivate let keychain: KeychainWrapper
172
var isShutdown = false
173
174
internal let files: FileAccessor
175
176
let db: BrowserDB
177
let readingListDB: BrowserDB
178
var syncManager: SyncManager!
179
180
private static var loginsKey: String {
181
let key = "sqlcipher.key.logins.db"
182
let keychain = KeychainWrapper.sharedAppContainerKeychain
183
keychain.ensureStringItemAccessibility(.afterFirstUnlock, forKey: key)
184
if keychain.hasValue(forKey: key), let secret = keychain.string(forKey: key) {
185
return secret
186
}
187
188
let Length: UInt = 256
189
let secret = Bytes.generateRandomBytes(Length).base64EncodedString
190
keychain.set(secret, forKey: key, withAccessibility: .afterFirstUnlock)
191
return secret
192
}
193
194
var syncDelegate: SyncDelegate?
195
196
/**
197
* N.B., BrowserProfile is used from our extensions, often via a pattern like
198
*
199
* BrowserProfile(…).foo.saveSomething(…)
200
*
201
* This can break if BrowserProfile's initializer does async work that
202
* subsequently — and asynchronously — expects the profile to stick around:
203
* see Bug 1218833. Be sure to only perform synchronous actions here.
204
*
205
* A SyncDelegate can be provided in this initializer, or once the profile is initialized.
206
* However, if we provide it here, it's assumed that we're initializing it from the application.
207
*/
208
init(localName: String, syncDelegate: SyncDelegate? = nil, clear: Bool = false) {
209
log.debug("Initing profile \(localName) on thread \(Thread.current).")
210
self.name = localName
211
self.files = ProfileFileAccessor(localName: localName)
212
self.keychain = KeychainWrapper.sharedAppContainerKeychain
213
self.syncDelegate = syncDelegate
214
215
if clear {
216
do {
217
// Remove the contents of the directory…
218
try self.files.removeFilesInDirectory()
219
// …then remove the directory itself.
220
try self.files.remove("")
221
} catch {
222
log.info("Cannot clear profile: \(error)")
223
}
224
}
225
226
// If the profile dir doesn't exist yet, this is first run (for this profile). The check is made here
227
// since the DB handles will create new DBs under the new profile folder.
228
let isNewProfile = !files.exists("")
229
230
// Set up our database handles.
231
self.db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files)
232
self.readingListDB = BrowserDB(filename: "ReadingList.db", schema: ReadingListSchema(), files: files)
233
234
if isNewProfile {
235
log.info("New profile. Removing old Keychain/Prefs data.")
236
KeychainWrapper.wipeKeychain()
237
prefs.clearAll()
238
}
239
240
// Migrate bookmarks from old browser.db to new Rust places.db only
241
// if this user is NOT signed into Sync (only migrates once if needed).
242
if !self.hasAccount() {
243
self.places.migrateBookmarksIfNeeded(fromBrowserDB: self.db)
244
}
245
246
// Log SQLite compile_options.
247
// db.sqliteCompileOptions() >>== { compileOptions in
248
// log.debug("SQLite compile_options:\n\(compileOptions.joined(separator: "\n"))")
249
// }
250
251
// Set up logging from Rust.
252
if !RustLog.shared.tryEnable({ (level, tag, message) -> Bool in
253
let logString = "[RUST][\(tag ?? "no-tag")] \(message)"
254
255
switch level {
256
case .trace:
257
if Logger.logPII {
258
log.verbose(logString)
259
}
260
case .debug:
261
log.debug(logString)
262
case .info:
263
log.info(logString)
264
case .warn:
265
log.warning(logString)
266
case .error:
267
Sentry.shared.sendWithStacktrace(message: logString, tag: .rustLog, severity: .error)
268
log.error(logString)
269
}
270
271
return true
272
}) {
273
log.error("ERROR: Unable to enable logging from Rust")
274
}
275
276
// By default, filter logging from Rust below `.info` level.
277
try? RustLog.shared.setLevelFilter(filter: .info)
278
279
// This has to happen prior to the databases being opened, because opening them can trigger
280
// events to which the SyncManager listens.
281
self.syncManager = BrowserSyncManager(profile: self)
282
283
let notificationCenter = NotificationCenter.default
284
285
notificationCenter.addObserver(self, selector: #selector(onLocationChange), name: .OnLocationChange, object: nil)
286
notificationCenter.addObserver(self, selector: #selector(onPageMetadataFetched), name: .OnPageMetadataFetched, object: nil)
287
288
// Always start by needing invalidation.
289
// This is the same as self.history.setTopSitesNeedsInvalidation, but without the
290
// side-effect of instantiating SQLiteHistory (and thus BrowserDB) on the main thread.
291
prefs.setBool(false, forKey: PrefsKeys.KeyTopSitesCacheIsValid)
292
293
if BrowserProfile.isChinaEdition {
294
295
// Set the default homepage.
296
prefs.setString(PrefsDefaults.ChineseHomePageURL, forKey: PrefsKeys.KeyDefaultHomePageURL)
297
298
if prefs.stringForKey(PrefsKeys.KeyNewTab) == nil {
299
prefs.setString(PrefsDefaults.ChineseHomePageURL, forKey: PrefsKeys.NewTabCustomUrlPrefKey)
300
prefs.setString(PrefsDefaults.ChineseNewTabDefault, forKey: PrefsKeys.KeyNewTab)
301
}
302
303
if prefs.stringForKey(PrefsKeys.HomePageTab) == nil {
304
prefs.setString(PrefsDefaults.ChineseHomePageURL, forKey: PrefsKeys.HomeButtonHomePageURL)
305
prefs.setString(PrefsDefaults.ChineseNewTabDefault, forKey: PrefsKeys.HomePageTab)
306
}
307
} else {
308
// Remove the default homepage. This does not change the user's preference,
309
// just the behaviour when there is no homepage.
310
prefs.removeObjectForKey(PrefsKeys.KeyDefaultHomePageURL)
311
}
312
313
// Hide the "__leanplum.sqlite" file in the documents directory.
314
if var leanplumFile = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("__leanplum.sqlite"), FileManager.default.fileExists(atPath: leanplumFile.path) {
315
let isHidden = (try? leanplumFile.resourceValues(forKeys: [.isHiddenKey]))?.isHidden ?? false
316
if !isHidden {
317
var resourceValues = URLResourceValues()
318
resourceValues.isHidden = true
319
try? leanplumFile.setResourceValues(resourceValues)
320
}
321
}
322
323
// Create the "Downloads" folder in the documents directory.
324
if let downloadsPath = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Downloads").path {
325
try? FileManager.default.createDirectory(atPath: downloadsPath, withIntermediateDirectories: true, attributes: nil)
326
}
327
}
328
329
func _reopen() {
330
log.debug("Reopening profile.")
331
isShutdown = false
332
333
db.reopenIfClosed()
334
_ = logins.reopenIfClosed()
335
_ = places.reopenIfClosed()
336
}
337
338
func _shutdown() {
339
log.debug("Shutting down profile.")
340
isShutdown = true
341
342
db.forceClose()
343
_ = logins.forceClose()
344
_ = places.forceClose()
345
}
346
347
@objc
348
func onLocationChange(notification: NSNotification) {
349
if let v = notification.userInfo!["visitType"] as? Int,
350
let visitType = VisitType(rawValue: v),
351
let url = notification.userInfo!["url"] as? URL, !isIgnoredURL(url),
352
let title = notification.userInfo!["title"] as? NSString {
353
// Only record local vists if the change notification originated from a non-private tab
354
if !(notification.userInfo!["isPrivate"] as? Bool ?? false) {
355
// We don't record a visit if no type was specified -- that means "ignore me".
356
let site = Site(url: url.absoluteString, title: title as String)
357
let visit = SiteVisit(site: site, date: Date.nowMicroseconds(), type: visitType)
358
history.addLocalVisit(visit)
359
}
360
361
history.setTopSitesNeedsInvalidation()
362
} else {
363
log.debug("Ignoring navigation.")
364
}
365
}
366
367
@objc
368
func onPageMetadataFetched(notification: NSNotification) {
369
let isPrivate = notification.userInfo?["isPrivate"] as? Bool ?? true
370
guard !isPrivate else {
371
log.debug("Private mode - Ignoring page metadata.")
372
return
373
}
374
guard let pageURL = notification.userInfo?["tabURL"] as? URL,
375
let pageMetadata = notification.userInfo?["pageMetadata"] as? PageMetadata else {
376
log.debug("Metadata notification doesn't contain any metadata!")
377
return
378
}
379
let defaultMetadataTTL: UInt64 = 3 * 24 * 60 * 60 * 1000 // 3 days for the metadata to live
380
self.metadata.storeMetadata(pageMetadata, forPageURL: pageURL, expireAt: defaultMetadataTTL + Date.now())
381
}
382
383
deinit {
384
log.debug("Deiniting profile \(self.localName()).")
385
self.syncManager.endTimedSyncs()
386
}
387
388
func localName() -> String {
389
return name
390
}
391
392
lazy var queue: TabQueue = {
393
withExtendedLifetime(self.history) {
394
return SQLiteQueue(db: self.db)
395
}
396
}()
397
398
/**
399
* Favicons, history, and tabs are all stored in one intermeshed
400
* collection of tables.
401
*
402
* Any other class that needs to access any one of these should ensure
403
* that this is initialized first.
404
*/
405
fileprivate lazy var legacyPlaces: BrowserHistory & Favicons & SyncableHistory & ResettableSyncStorage & HistoryRecommendations = {
406
return SQLiteHistory(db: self.db, prefs: self.prefs)
407
}()
408
409
var favicons: Favicons {
410
return self.legacyPlaces
411
}
412
413
var history: BrowserHistory & SyncableHistory & ResettableSyncStorage {
414
return self.legacyPlaces
415
}
416
417
lazy var panelDataObservers: PanelDataObservers = {
418
return PanelDataObservers(profile: self)
419
}()
420
421
lazy var metadata: Metadata = {
422
return SQLiteMetadata(db: self.db)
423
}()
424
425
var recommendations: HistoryRecommendations {
426
return self.legacyPlaces
427
}
428
429
lazy var places: RustPlaces = {
430
let databasePath = URL(fileURLWithPath: (try! files.getAndEnsureDirectory()), isDirectory: true).appendingPathComponent("places.db").path
431
return RustPlaces(databasePath: databasePath)
432
}()
433
434
lazy var searchEngines: SearchEngines = {
435
return SearchEngines(prefs: self.prefs, files: self.files)
436
}()
437
438
func makePrefs() -> Prefs {
439
return NSUserDefaultsPrefs(prefix: self.localName())
440
}
441
442
lazy var prefs: Prefs = {
443
return self.makePrefs()
444
}()
445
446
lazy var readingList: ReadingList = {
447
return SQLiteReadingList(db: self.readingListDB)
448
}()
449
450
lazy var remoteClientsAndTabs: RemoteClientsAndTabs & ResettableSyncStorage & AccountRemovalDelegate & RemoteDevices = {
451
return SQLiteRemoteClientsAndTabs(db: self.db)
452
}()
453
454
lazy var certStore: CertStore = {
455
return CertStore()
456
}()
457
458
lazy var recentlyClosedTabs: ClosedTabsStore = {
459
return ClosedTabsStore(prefs: self.prefs)
460
}()
461
462
open func getSyncDelegate() -> SyncDelegate {
463
return syncDelegate ?? CommandStoringSyncDelegate(profile: self)
464
}
465
466
public func getClients() -> Deferred<Maybe<[RemoteClient]>> {
467
return self.syncManager.syncClients()
468
>>> { self.remoteClientsAndTabs.getClients() }
469
}
470
471
public func getCachedClients()-> Deferred<Maybe<[RemoteClient]>> {
472
return self.remoteClientsAndTabs.getClients()
473
}
474
475
public func getClientsAndTabs() -> Deferred<Maybe<[ClientAndTabs]>> {
476
return self.syncManager.syncClientsThenTabs()
477
>>> { self.remoteClientsAndTabs.getClientsAndTabs() }
478
}
479
480
public func getCachedClientsAndTabs() -> Deferred<Maybe<[ClientAndTabs]>> {
481
return self.remoteClientsAndTabs.getClientsAndTabs()
482
}
483
484
public func cleanupHistoryIfNeeded() {
485
recommendations.cleanupHistoryIfNeeded()
486
}
487
488
func storeTabs(_ tabs: [RemoteTab]) -> Deferred<Maybe<Int>> {
489
return self.remoteClientsAndTabs.insertOrUpdateTabs(tabs)
490
}
491
492
public func sendItem(_ item: ShareItem, toDevices devices: [RemoteDevice]) -> Success {
493
guard let account = self.getAccount() else {
494
return deferMaybe(NoAccountError())
495
}
496
497
let scratchpadPrefs = self.prefs.branch("sync.scratchpad")
498
let id = scratchpadPrefs.stringForKey("clientGUID") ?? ""
499
let command = SyncCommand.displayURIFromShareItem(item, asClient: id)
500
let fxaDeviceIds = devices.compactMap { $0.id }
501
502
let result = Success()
503
504
self.remoteClientsAndTabs.getClients() >>== { clients in
505
let newRemoteDevices = devices.filter { account.commandsClient.sendTab.isDeviceCompatible($0) }
506
var oldRemoteClients = devices.filter { !account.commandsClient.sendTab.isDeviceCompatible($0) }.compactMap { remoteDevice in
507
clients.find { $0.fxaDeviceId == remoteDevice.id }
508
}
509
510
func sendViaSyncFallback() {
511
if oldRemoteClients.isEmpty {
512
result.fill(Maybe(success: ()))
513
} else {
514
self.remoteClientsAndTabs.insertCommands([command], forClients: oldRemoteClients) >>> {
515
self.syncManager.syncClients() >>> {
516
account.notify(deviceIDs: fxaDeviceIds, collectionsChanged: ["clients"], reason: "sendtab")
517
result.fill(Maybe(success: ()))
518
}
519
}
520
}
521
}
522
523
if !newRemoteDevices.isEmpty {
524
account.commandsClient.sendTab.send(to: newRemoteDevices, url: item.url, title: item.title ?? "") >>== { report in
525
for failedRemoteDevice in report.failed {
526
log.debug("Failed to send a tab with FxA commands for \(failedRemoteDevice.name). Falling back on the Sync back-end")
527
if let oldRemoteClient = clients.find({ $0.fxaDeviceId == failedRemoteDevice.id }) {
528
oldRemoteClients.append(oldRemoteClient)
529
}
530
}
531
532
sendViaSyncFallback()
533
}
534
} else {
535
sendViaSyncFallback()
536
}
537
}
538
539
return result
540
}
541
542
lazy var logins: RustLogins = {
543
let databasePath = URL(fileURLWithPath: (try! files.getAndEnsureDirectory()), isDirectory: true).appendingPathComponent("logins.db").path
544
return RustLogins(databasePath: databasePath, encryptionKey: BrowserProfile.loginsKey)
545
}()
546
547
static var isChinaEdition: Bool = {
548
return Locale.current.identifier == "zh_CN"
549
}()
550
551
var accountConfiguration: FirefoxAccountConfiguration {
552
if prefs.boolForKey("useCustomSyncService") ?? false {
553
return CustomFirefoxAccountConfiguration(prefs: self.prefs)
554
}
555
if prefs.boolForKey("useChinaSyncService") ?? BrowserProfile.isChinaEdition {
556
return ChinaEditionFirefoxAccountConfiguration(prefs: self.prefs)
557
}
558
if prefs.boolForKey("useStageSyncService") ?? false {
559
return StageFirefoxAccountConfiguration(prefs: self.prefs)
560
}
561
return ProductionFirefoxAccountConfiguration(prefs: self.prefs)
562
}
563
564
fileprivate lazy var account: Account.FirefoxAccount? = {
565
let key = name + ".account"
566
keychain.ensureObjectItemAccessibility(.afterFirstUnlock, forKey: key)
567
if let dictionary = keychain.object(forKey: key) as? [String: AnyObject] {
568
let account = Account.FirefoxAccount.fromDictionary(dictionary, withPrefs: prefs)
569
570
// Check to see if the account configuration set is a custom service
571
// and update it to use the custom servers.
572
if let configuration = account?.configuration as? CustomFirefoxAccountConfiguration {
573
account?.configuration = CustomFirefoxAccountConfiguration(prefs: prefs)
574
}
575
account?.updateProfile()
576
577
return account
578
}
579
return nil
580
}()
581
582
func hasAccount() -> Bool {
583
return account != nil
584
}
585
586
func hasSyncableAccount() -> Bool {
587
return account?.actionNeeded == FxAActionNeeded.none
588
}
589
590
func getAccount() -> Account.FirefoxAccount? {
591
return account
592
}
593
594
func removeAccountMetadata() {
595
self.prefs.removeObjectForKey(PrefsKeys.KeyLastRemoteTabSyncTime)
596
self.keychain.removeObject(forKey: self.name + ".account")
597
}
598
599
func removeExistingAuthenticationInfo() {
600
self.keychain.setAuthenticationInfo(nil)
601
}
602
603
func removeAccount() {
604
let old = self.account
605
removeAccountMetadata()
606
self.account = nil
607
608
// Tell any observers that our account has changed.
609
NotificationCenter.default.post(name: .FirefoxAccountChanged, object: nil)
610
611
// Trigger cleanup. Pass in the account in case we want to try to remove
612
// client-specific data from the server.
613
self.syncManager.onRemovedAccount(old)
614
}
615
616
func setAccount(_ account: Account.FirefoxAccount) {
617
self.account = account
618
619
flushAccount()
620
621
// tell any observers that our account has changed
622
DispatchQueue.main.async {
623
// Many of the observers for this notifications are on the main thread,
624
// so we should post the notification there, just in case we're not already
625
// on the main thread.
626
let userInfo = [Notification.Name.UserInfoKeyHasSyncableAccount: self.hasSyncableAccount()]
627
NotificationCenter.default.post(name: .FirefoxAccountChanged, object: nil, userInfo: userInfo)
628
}
629
630
self.syncManager.onAddedAccount()
631
}
632
633
func flushAccount() {
634
if let account = account {
635
self.keychain.set(account.dictionary() as NSCoding, forKey: name + ".account", withAccessibility: .afterFirstUnlock)
636
}
637
}
638
639
class NoAccountError: MaybeErrorType {
640
var description = "No account."
641
}
642
643
// Extends NSObject so we can use timers.
644
public class BrowserSyncManager: NSObject, SyncManager, CollectionChangedNotifier {
645
// We shouldn't live beyond our containing BrowserProfile, either in the main app or in
646
// an extension.
647
// But it's possible that we'll finish a side-effect sync after we've ditched the profile
648
// as a whole, so we hold on to our Prefs, potentially for a little while longer. This is
649
// safe as a strong reference, because there's no cycle.
650
unowned fileprivate let profile: BrowserProfile
651
fileprivate let prefs: Prefs
652
653
let FifteenMinutes = TimeInterval(60 * 15)
654
let OneMinute = TimeInterval(60)
655
656
fileprivate var syncTimer: Timer?
657
658
fileprivate var backgrounded: Bool = true
659
public func applicationDidEnterBackground() {
660
self.backgrounded = true
661
self.endTimedSyncs()
662
}
663
664
public func applicationDidBecomeActive() {
665
self.backgrounded = false
666
667
guard self.profile.hasSyncableAccount() else {
668
return
669
}
670
671
self.beginTimedSyncs()
672
673
// Sync now if it's been more than our threshold.
674
let now = Date.now()
675
let then = self.lastSyncFinishTime ?? 0
676
guard now >= then else {
677
log.debug("Time was modified since last sync.")
678
self.syncEverythingSoon()
679
return
680
}
681
let since = now - then
682
log.debug("\(since)msec since last sync.")
683
if since > SyncConstants.SyncOnForegroundMinimumDelayMillis {
684
self.syncEverythingSoon()
685
}
686
}
687
688
/**
689
* Locking is managed by syncSeveral. Make sure you take and release these
690
* whenever you do anything Sync-ey.
691
*/
692
fileprivate let syncLock = NSRecursiveLock()
693
694
public var isSyncing: Bool {
695
syncLock.lock()
696
defer { syncLock.unlock() }
697
return syncDisplayState != nil && syncDisplayState! == .inProgress
698
}
699
700
public var syncDisplayState: SyncDisplayState?
701
702
// The dispatch queue for coordinating syncing and resetting the database.
703
fileprivate let syncQueue = DispatchQueue(label: "com.mozilla.firefox.sync")
704
705
fileprivate typealias EngineResults = [(EngineIdentifier, SyncStatus)]
706
fileprivate typealias EngineTasks = [(EngineIdentifier, SyncFunction)]
707
708
// Used as a task queue for syncing.
709
fileprivate var syncReducer: AsyncReducer<EngineResults, EngineTasks>?
710
711
fileprivate func beginSyncing() {
712
notifySyncing(notification: .ProfileDidStartSyncing)
713
}
714
715
fileprivate func endSyncing(_ result: SyncOperationResult) {
716
// loop through statuses and fill sync state
717
syncLock.lock()
718
defer { syncLock.unlock() }
719
log.info("Ending all queued syncs.")
720
721
syncDisplayState = SyncStatusResolver(engineResults: result.engineResults).resolveResults()
722
723
#if MOZ_TARGET_CLIENT
724
if let account = profile.account, canSendUsageData() {
725
SyncPing.from(result: result,
726
account: account,
727
remoteClientsAndTabs: profile.remoteClientsAndTabs,
728
prefs: prefs,
729
why: .schedule) >>== { SyncTelemetry.send(ping: $0, docType: .sync) }
730
} else {
731
log.debug("Profile isn't sending usage data. Not sending sync status event.")
732
}
733
#endif
734
735
// Dont notify if we are performing a sync in the background. This prevents more db access from happening
736
if !self.backgrounded {
737
notifySyncing(notification: .ProfileDidFinishSyncing)
738
}
739
syncReducer = nil
740
}
741
742
func canSendUsageData() -> Bool {
743
return profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true
744
}
745
746
private func notifySyncing(notification: Notification.Name) {
747
NotificationCenter.default.post(name: notification, object: syncDisplayState?.asObject())
748
}
749
750
init(profile: BrowserProfile) {
751
self.profile = profile
752
self.prefs = profile.prefs
753
754
super.init()
755
756
let center = NotificationCenter.default
757
758
center.addObserver(self, selector: #selector(onDatabaseWasRecreated), name: .DatabaseWasRecreated, object: nil)
759
center.addObserver(self, selector: #selector(onLoginDidChange), name: .DataLoginDidChange, object: nil)
760
center.addObserver(self, selector: #selector(onStartSyncing), name: .ProfileDidStartSyncing, object: nil)
761
center.addObserver(self, selector: #selector(onFinishSyncing), name: .ProfileDidFinishSyncing, object: nil)
762
}
763
764
// TODO: Do we still need this/do we need to do this for our new DB too?
765
private func handleRecreationOfDatabaseNamed(name: String?) -> Success {
766
let browserCollections = ["history", "tabs"]
767
let dbName = name ?? "<all>"
768
switch dbName {
769
case "<all>", "browser.db":
770
return self.locallyResetCollections(browserCollections)
771
default:
772
log.debug("Unknown database \(dbName).")
773
return succeed()
774
}
775
}
776
777
func doInBackgroundAfter(_ millis: Int64, _ block: @escaping () -> Void) {
778
let queue = DispatchQueue.global(qos: DispatchQoS.background.qosClass)
779
//Pretty ambiguous here. I'm thinking .now was DispatchTime.now() and not Date.now()
780
queue.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(millis)), execute: block)
781
}
782
783
@objc
784
func onDatabaseWasRecreated(notification: NSNotification) {
785
log.debug("Database was recreated.")
786
let name = notification.object as? String
787
log.debug("Database was \(name ?? "nil").")
788
789
// We run this in the background after a few hundred milliseconds;
790
// it doesn't really matter when it runs, so long as it doesn't
791
// happen in the middle of a sync.
792
793
let resetDatabase = {
794
return self.handleRecreationOfDatabaseNamed(name: name) >>== {
795
log.debug("Reset of \(name ?? "nil") done")
796
}
797
}
798
799
self.doInBackgroundAfter(300) {
800
self.syncLock.lock()
801
defer { self.syncLock.unlock() }
802
// If we're syncing already, then wait for sync to end,
803
// then reset the database on the same serial queue.
804
if let reducer = self.syncReducer, !reducer.isFilled {
805
reducer.terminal.upon { _ in
806
self.syncQueue.async(execute: resetDatabase)
807
}
808
} else {
809
// Otherwise, reset the database on the sync queue now
810
// Sync can't start while this is still going on.
811
self.syncQueue.async(execute: resetDatabase)
812
}
813
}
814
}
815
816
// Simple in-memory rate limiting.
817
var lastTriggeredLoginSync: Timestamp = 0
818
@objc func onLoginDidChange(_ notification: NSNotification) {
819
log.debug("Login did change.")
820
if (Date.now() - lastTriggeredLoginSync) > OneMinuteInMilliseconds {
821
lastTriggeredLoginSync = Date.now()
822
823
// Give it a few seconds.
824
// Trigger on the main queue. The bulk of the sync work runs in the background.
825
let greenLight = self.greenLight()
826
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(SyncConstants.SyncDelayTriggered)) {
827
if greenLight() {
828
self.syncLogins()
829
}
830
}
831
}
832
}
833
834
public var lastSyncFinishTime: Timestamp? {
835
get {
836
return self.prefs.timestampForKey(PrefsKeys.KeyLastSyncFinishTime)
837
}
838
839
set(value) {
840
if let value = value {
841
self.prefs.setTimestamp(value, forKey: PrefsKeys.KeyLastSyncFinishTime)
842
} else {
843
self.prefs.removeObjectForKey(PrefsKeys.KeyLastSyncFinishTime)
844
}
845
}
846
}
847
848
@objc func onStartSyncing(_ notification: NSNotification) {
849
syncLock.lock()
850
defer { syncLock.unlock() }
851
syncDisplayState = .inProgress
852
}
853
854
@objc func onFinishSyncing(_ notification: NSNotification) {
855
syncLock.lock()
856
defer { syncLock.unlock() }
857
if let syncState = syncDisplayState, syncState == .good {
858
self.lastSyncFinishTime = Date.now()
859
}
860
}
861
862
var prefsForSync: Prefs {
863
return self.prefs.branch("sync")
864
}
865
866
public func onAddedAccount() -> Success {
867
// Only sync if we're green lit. This makes sure that we don't sync unverified accounts.
868
guard self.profile.hasSyncableAccount() else { return succeed() }
869
870
self.beginTimedSyncs()
871
return self.syncEverything(why: .didLogin)
872
}
873
874
func locallyResetCollections(_ collections: [String]) -> Success {
875
return walk(collections, f: self.locallyResetCollection)
876
}
877
878
func locallyResetCollection(_ collection: String) -> Success {
879
switch collection {
880
case "bookmarks":
881
return self.profile.places.resetBookmarksMetadata()
882
case "clients":
883
fallthrough
884
case "tabs":
885
// Because clients and tabs share storage, and thus we wipe data for both if we reset either,
886
// we reset the prefs for both at the same time.
887
return TabsSynchronizer.resetClientsAndTabsWithStorage(self.profile.remoteClientsAndTabs, basePrefs: self.prefsForSync)
888
889
case "history":
890
return HistorySynchronizer.resetSynchronizerWithStorage(self.profile.history, basePrefs: self.prefsForSync, collection: "history")
891
case "passwords":
892
return self.profile.logins.reset()
893
case "forms":
894
log.debug("Requested reset for forms, but this client doesn't sync them yet.")
895
return succeed()
896
case "addons":
897
log.debug("Requested reset for addons, but this client doesn't sync them.")
898
return succeed()
899
case "prefs":
900
log.debug("Requested reset for prefs, but this client doesn't sync them.")
901
return succeed()
902
default:
903
log.warning("Asked to reset collection \(collection), which we don't know about.")
904
return succeed()
905
}
906
}
907
908
public func onNewProfile() {
909
SyncStateMachine.clearStateFromPrefs(self.prefsForSync)
910
}
911
912
public func onRemovedAccount(_ account: Account.FirefoxAccount?) -> Success {
913
let profile = self.profile
914
915
// Run these in order, because they might write to the same DB!
916
let remove = [
917
profile.history.onRemovedAccount,
918
profile.remoteClientsAndTabs.onRemovedAccount,
919
profile.logins.reset,
920
profile.places.resetBookmarksMetadata,
921
]
922
923
let clearPrefs: () -> Success = {
924
withExtendedLifetime(self) {
925
// Clear prefs after we're done clearing everything else -- just in case
926
// one of them needs the prefs and we race. Clear regardless of success
927
// or failure.
928
929
// This will remove keys from the Keychain if they exist, as well
930
// as wiping the Sync prefs.
931
SyncStateMachine.clearStateFromPrefs(self.prefsForSync)
932
}
933
return succeed()
934
}
935
936
return accumulate(remove) >>> clearPrefs
937
}
938
939
fileprivate func repeatingTimerAtInterval(_ interval: TimeInterval, selector: Selector) -> Timer {
940
return Timer.scheduledTimer(timeInterval: interval, target: self, selector: selector, userInfo: nil, repeats: true)
941
}
942
943
public func beginTimedSyncs() {
944
if self.syncTimer != nil {
945
log.debug("Already running sync timer.")
946
return
947
}
948
949
let interval = FifteenMinutes
950
let selector = #selector(syncOnTimer)
951
log.debug("Starting sync timer.")
952
self.syncTimer = repeatingTimerAtInterval(interval, selector: selector)
953
}
954
955
/**
956
* The caller is responsible for calling this on the same thread on which it called
957
* beginTimedSyncs.
958
*/
959
public func endTimedSyncs() {
960
if let t = self.syncTimer {
961
log.debug("Stopping sync timer.")
962
self.syncTimer = nil
963
t.invalidate()
964
}
965
}
966
967
fileprivate func syncClientsWithDelegate(_ delegate: SyncDelegate, prefs: Prefs, ready: Ready, why: SyncReason) -> SyncResult {
968
log.debug("Syncing clients to storage.")
969
970
let clientSynchronizer = ready.synchronizer(ClientsSynchronizer.self, delegate: delegate, prefs: prefs, why: why)
971
return clientSynchronizer.synchronizeLocalClients(self.profile.remoteClientsAndTabs, withServer: ready.client, info: ready.info, notifier: self) >>== { result in
972
guard case .completed = result else {
973
return deferMaybe(result)
974
}
975
guard let account = self.profile.account else {
976
return deferMaybe(result)
977
}
978
log.debug("Updating FxA devices list.")
979
return account.updateFxADevices(remoteDevices: self.profile.remoteClientsAndTabs).bind { _ in
980
return deferMaybe(result)
981
}
982
}
983
}
984
985
fileprivate func syncTabsWithDelegate(_ delegate: SyncDelegate, prefs: Prefs, ready: Ready, why: SyncReason) -> SyncResult {
986
let storage = self.profile.remoteClientsAndTabs
987
let tabSynchronizer = ready.synchronizer(TabsSynchronizer.self, delegate: delegate, prefs: prefs, why: why)
988
return tabSynchronizer.synchronizeLocalTabs(storage, withServer: ready.client, info: ready.info)
989
}
990
991
fileprivate func syncHistoryWithDelegate(_ delegate: SyncDelegate, prefs: Prefs, ready: Ready, why: SyncReason) -> SyncResult {
992
log.debug("Syncing history to storage.")
993
let historySynchronizer = ready.synchronizer(HistorySynchronizer.self, delegate: delegate, prefs: prefs, why: why)
994
return historySynchronizer.synchronizeLocalHistory(self.profile.history, withServer: ready.client, info: ready.info, greenLight: self.greenLight())
995
}
996
997
fileprivate func syncLoginsWithDelegate(_ delegate: SyncDelegate, prefs: Prefs, ready: Ready, why: SyncReason) -> SyncResult {
998
guard let account = profile.account else {
999
return deferMaybe(SyncStatus.notStarted(.noAccount))
1000
}
1001
1002
log.debug("Syncing logins to storage.")
1003
return account.syncUnlockInfo().bind({ result in
1004
guard let syncUnlockInfo = result.successValue else {
1005
return deferMaybe(SyncStatus.notStarted(.unknown))
1006
}
1007
1008
return self.profile.logins.sync(unlockInfo: syncUnlockInfo).bind({ result in
1009
guard result.isSuccess else {
1010
return deferMaybe(SyncStatus.notStarted(.unknown))
1011
}
1012
1013
let syncEngineStatsSession = SyncEngineStatsSession(collection: "logins")
1014
return deferMaybe(SyncStatus.completed(syncEngineStatsSession))
1015
})
1016
})
1017
}
1018
1019
fileprivate func syncBookmarksWithDelegate(_ delegate: SyncDelegate, prefs: Prefs, ready: Ready, why: SyncReason) -> SyncResult {
1020
guard let account = profile.account else {
1021
return deferMaybe(SyncStatus.notStarted(.noAccount))
1022
}
1023
1024
log.debug("Syncing bookmarks to storage.")
1025
return account.syncUnlockInfo().bind({ result in
1026
guard let syncUnlockInfo = result.successValue else {
1027
return deferMaybe(SyncStatus.notStarted(.unknown))
1028
}
1029
1030
return self.profile.places.syncBookmarks(unlockInfo: syncUnlockInfo).bind({ result in
1031
guard result.isSuccess else {
1032
return deferMaybe(SyncStatus.notStarted(.unknown))
1033
}
1034
1035
let syncEngineStatsSession = SyncEngineStatsSession(collection: "bookmarks")
1036
return deferMaybe(SyncStatus.completed(syncEngineStatsSession))
1037
})
1038
})
1039
}
1040
1041
func takeActionsOnEngineStateChanges<T: EngineStateChanges>(_ changes: T) -> Deferred<Maybe<T>> {
1042
var needReset = Set<String>(changes.collectionsThatNeedLocalReset())
1043
needReset.formUnion(changes.enginesDisabled())
1044
needReset.formUnion(changes.enginesEnabled())
1045
if needReset.isEmpty {
1046
log.debug("No collections need reset. Moving on.")
1047
return deferMaybe(changes)
1048
}
1049
1050
// needReset needs at most one of clients and tabs, because we reset them
1051
// both if either needs reset. This is strictly an optimization to avoid
1052
// doing duplicate work.
1053
if needReset.contains("clients") {
1054
if needReset.remove("tabs") != nil {
1055
log.debug("Already resetting clients (and tabs); not bothering to also reset tabs again.")
1056
}
1057
}
1058
1059
return walk(Array(needReset), f: self.locallyResetCollection)
1060
>>> effect(changes.clearLocalCommands)
1061
>>> always(changes)
1062
}
1063
1064
/**
1065
* Runs the single provided synchronization function and returns its status.
1066
*/
1067
fileprivate func sync(_ label: EngineIdentifier, function: @escaping SyncFunction) -> SyncResult {
1068
return syncSeveral(why: .user, synchronizers: [(label, function)]) >>== { statuses in
1069
let status = statuses.find { label == $0.0 }?.1
1070
return deferMaybe(status ?? .notStarted(.unknown))
1071
}
1072
}
1073
1074
/**
1075
* Convenience method for syncSeveral([(EngineIdentifier, SyncFunction)])
1076
*/
1077
private func syncSeveral(why: SyncReason, synchronizers: (EngineIdentifier, SyncFunction)...) -> Deferred<Maybe<[(EngineIdentifier, SyncStatus)]>> {
1078
return syncSeveral(why: why, synchronizers: synchronizers)
1079
}
1080
1081
/**
1082
* Runs each of the provided synchronization functions with the same inputs.
1083
* Returns an array of IDs and SyncStatuses at least length as the input.
1084
* The statuses returned will be a superset of the ones that are requested here.
1085
* While a sync is ongoing, each engine from successive calls to this method will only be called once.
1086
*/
1087
fileprivate func syncSeveral(why: SyncReason, synchronizers: [(EngineIdentifier, SyncFunction)]) -> Deferred<Maybe<[(EngineIdentifier, SyncStatus)]>> {
1088
syncLock.lock()
1089
defer { syncLock.unlock() }
1090
1091
guard let account = self.profile.account else {
1092
log.info("No account to sync with.")
1093
let statuses = synchronizers.map {
1094
($0.0, SyncStatus.notStarted(.noAccount))
1095
}
1096
return deferMaybe(statuses)
1097
}
1098
1099
// TODO: Invoke `account.commandsClient.fetchMissedRemoteCommands()` to
1100
// catch any missed FxA commands at time of Sync?
1101
1102
if !isSyncing {
1103
// A sync isn't already going on, so start another one.
1104
let statsSession = SyncOperationStatsSession(why: why, uid: account.uid, deviceID: account.deviceRegistration?.id)
1105
let reducer = AsyncReducer<EngineResults, EngineTasks>(initialValue: [], queue: syncQueue) { (statuses, synchronizers) in
1106
let done = Set(statuses.map { $0.0 })
1107
let remaining = synchronizers.filter { !done.contains($0.0) }
1108
if remaining.isEmpty {
1109
log.info("Nothing left to sync")
1110
return deferMaybe(statuses)
1111
}
1112
1113
return self.syncWith(synchronizers: remaining, account: account, statsSession: statsSession, why: why) >>== { deferMaybe(statuses + $0) }
1114
}
1115
1116
reducer.terminal.upon { results in
1117
let result = SyncOperationResult(
1118
engineResults: results,
1119
stats: statsSession.hasStarted() ? statsSession.end() : nil
1120
)
1121
self.endSyncing(result)
1122
}
1123
1124
// The actual work of synchronizing doesn't start until we append
1125
// the synchronizers to the reducer below.
1126
self.syncReducer = reducer
1127
self.beginSyncing()
1128
}
1129
1130
do {
1131
return try syncReducer!.append(synchronizers)
1132
} catch let error {
1133
log.error("Synchronizers appended after sync was finished. This is a bug. \(error)")
1134
let statuses = synchronizers.map {
1135
($0.0, SyncStatus.notStarted(.unknown))
1136
}
1137
return deferMaybe(statuses)
1138
}
1139
}
1140
1141
func engineEnablementChangesForAccount(account: Account.FirefoxAccount, profile: Profile) -> [String: Bool]? {
1142
var enginesEnablements: [String: Bool] = [:]
1143
// We just created the account, the user went through the Choose What to Sync screen on FxA.
1144
if let declined = account.declinedEngines {
1145
declined.forEach { enginesEnablements[$0] = false }
1146
account.declinedEngines = nil
1147
// Persist account changes so we don't try to decline engines on the next sync.
1148
profile.flushAccount()
1149
} else {
1150
// Bundle in authState the engines the user activated/disabled since the last sync.
1151
TogglableEngines.forEach { engine in
1152
let stateChangedPref = "engine.\(engine).enabledStateChanged"
1153
if let _ = self.prefsForSync.boolForKey(stateChangedPref),
1154
let enabled = self.prefsForSync.boolForKey("engine.\(engine).enabled") {
1155
enginesEnablements[engine] = enabled
1156
self.prefsForSync.setObject(nil, forKey: stateChangedPref)
1157
}
1158
}
1159
}
1160
return enginesEnablements
1161
}
1162
1163
// This SHOULD NOT be called directly: use syncSeveral instead.
1164
fileprivate func syncWith(synchronizers: [(EngineIdentifier, SyncFunction)],
1165
account: Account.FirefoxAccount,
1166
statsSession: SyncOperationStatsSession, why: SyncReason) -> Deferred<Maybe<[(EngineIdentifier, SyncStatus)]>> {
1167
log.info("Syncing \(synchronizers.map { $0.0 })")
1168
var authState = account.syncAuthState
1169
let delegate = self.profile.getSyncDelegate()
1170
if let enginesEnablements = self.engineEnablementChangesForAccount(account: account, profile: profile),
1171
!enginesEnablements.isEmpty {
1172
authState?.enginesEnablements = enginesEnablements
1173
log.debug("engines to enable: \(enginesEnablements.compactMap { $0.value ? $0.key : nil })")
1174
log.debug("engines to disable: \(enginesEnablements.compactMap { !$0.value ? $0.key : nil })")
1175
}
1176
1177
authState?.clientName = account.deviceName
1178
1179
let readyDeferred = SyncStateMachine(prefs: self.prefsForSync).toReady(authState!)
1180
1181
let function: (SyncDelegate, Prefs, Ready) -> Deferred<Maybe<[EngineStatus]>> = { delegate, syncPrefs, ready in
1182
let thunks = synchronizers.map { (i, f) in
1183
return { () -> Deferred<Maybe<EngineStatus>> in
1184
log.debug("Syncing \(i)…")
1185
return f(delegate, syncPrefs, ready, why) >>== { deferMaybe((i, $0)) }
1186
}
1187
}
1188
return accumulate(thunks)
1189
}
1190
1191
return readyDeferred.bind { readyResult in
1192
guard let success = readyResult.successValue else {
1193
if let tokenServerError = readyResult.failureValue as? TokenServerError,
1194
case let TokenServerError.remote(code, _, _) = tokenServerError,
1195
code == 401 {
1196
self.profile.getAccount()?.makeSeparated()
1197
}
1198
return deferMaybe(readyResult.failureValue!)
1199
}
1200
return self.takeActionsOnEngineStateChanges(success) >>== { ready in
1201
let updateEnginePref: ((String, Bool) -> Void) = { engine, enabled in
1202
self.prefsForSync.setBool(enabled, forKey: "engine.\(engine).enabled")
1203
}
1204
ready.engineConfiguration?.enabled.forEach { updateEnginePref($0, true) }
1205
ready.engineConfiguration?.declined.forEach { updateEnginePref($0, false) }
1206
1207
statsSession.start()
1208
return function(delegate, self.prefsForSync, ready)
1209
}
1210
}
1211
}
1212
1213
@discardableResult public func syncEverything(why: SyncReason) -> Success {
1214
let synchronizers = [
1215
("clients", self.syncClientsWithDelegate),
1216
("tabs", self.syncTabsWithDelegate),
1217
("bookmarks", self.syncBookmarksWithDelegate),
1218
("history", self.syncHistoryWithDelegate),
1219
("logins", self.syncLoginsWithDelegate)
1220
]
1221
1222
return self.syncSeveral(why: why, synchronizers: synchronizers) >>> succeed
1223
}
1224
1225
func syncEverythingSoon() {
1226
self.doInBackgroundAfter(SyncConstants.SyncOnForegroundAfterMillis) {
1227
log.debug("Running delayed startup sync.")
1228
self.syncEverything(why: .startup)
1229
}
1230
}
1231
1232
/**
1233
* Allows selective sync of different collections, for use by external APIs.
1234
* Some help is given to callers who use different namespaces (specifically: `passwords` is mapped to `logins`)
1235
* and to preserve some ordering rules.
1236
*/
1237
public func syncNamedCollections(why: SyncReason, names: [String]) -> Success {
1238
// Massage the list of names into engine identifiers.
1239
let engineIdentifiers = names.map { name -> [EngineIdentifier] in
1240
switch name {
1241
case "passwords":
1242
return ["logins"]
1243
case "tabs":
1244
return ["clients", "tabs"]
1245
default:
1246
return [name]
1247
}
1248
}.flatMap { $0 }
1249
1250
// By this time, `engineIdentifiers` may have duplicates in. We won't try and dedupe here
1251
// because `syncSeveral` will do that for us.
1252
1253
let synchronizers: [(EngineIdentifier, SyncFunction)] = engineIdentifiers.compactMap {
1254
switch $0 {
1255
case "clients": return ("clients", self.syncClientsWithDelegate)
1256
case "tabs": return ("tabs", self.syncTabsWithDelegate)
1257
case "logins": return ("logins", self.syncLoginsWithDelegate)
1258
case "bookmarks": return ("bookmarks", self.syncBookmarksWithDelegate)
1259
case "history": return ("history", self.syncHistoryWithDelegate)
1260
default: return nil
1261
}
1262
}
1263
return self.syncSeveral(why: why, synchronizers: synchronizers) >>> succeed
1264
}
1265
1266
@objc func syncOnTimer() {
1267
self.syncEverything(why: .scheduled)
1268
}
1269
1270
public func hasSyncedHistory() -> Deferred<Maybe<Bool>> {
1271
return self.profile.history.hasSyncedHistory()
1272
}
1273
1274
public func hasSyncedLogins() -> Deferred<Maybe<Bool>> {
1275
return self.profile.logins.hasSyncedLogins()
1276
}
1277
1278
public func syncClients() -> SyncResult {
1279
// TODO: recognize .NotStarted.
1280
return self.sync("clients", function: syncClientsWithDelegate)
1281
}
1282
1283
public func syncClientsThenTabs() -> SyncResult {
1284
return self.syncSeveral(
1285
why: .user,
1286
synchronizers:
1287
("clients", self.syncClientsWithDelegate),
1288
("tabs", self.syncTabsWithDelegate)) >>== { statuses in
1289
let status = statuses.find { "tabs" == $0.0 }
1290
return deferMaybe(status!.1)
1291
}
1292
}
1293
1294
@discardableResult public func syncBookmarks() -> SyncResult {
1295
return self.sync("bookmarks", function: syncBookmarksWithDelegate)
1296
}
1297
1298
@discardableResult public func syncLogins() -> SyncResult {
1299
return self.sync("logins", function: syncLoginsWithDelegate)
1300
}
1301
1302
public func syncHistory() -> SyncResult {
1303
// TODO: recognize .NotStarted.
1304
return self.sync("history", function: syncHistoryWithDelegate)
1305
}
1306
1307
/**
1308
* Return a thunk that continues to return true so long as an ongoing sync
1309
* should continue.
1310
*/
1311
func greenLight() -> () -> Bool {
1312
let start = Date.now()
1313
1314
// Give it two minutes to run before we stop.
1315
let stopBy = start + (2 * OneMinuteInMilliseconds)
1316
log.debug("Checking green light. Backgrounded: \(self.backgrounded).")
1317
return {
1318
Date.now() < stopBy &&
1319
self.profile.hasSyncableAccount()
1320
}
1321
}
1322
1323
public func notify(deviceIDs: [GUID], collectionsChanged collections: [String], reason: String) -> Success {
1324
guard let account = self.profile.account else {
1325
return deferMaybe(NoAccountError())
1326
}
1327
return account.notify(deviceIDs: deviceIDs, collectionsChanged: collections, reason: reason)
1328
}
1329
1330
public func notifyAll(collectionsChanged collections: [String], reason: String) -> Success {
1331
guard let account = self.profile.account else {
1332
return deferMaybe(NoAccountError())
1333
}
1334
return account.notifyAll(collectionsChanged: collections, reason: reason)
1335
}
1336
}
1337
}