/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import Foundation import UIKit import Shared import Deferred import SwiftyJSON private let log = Logger.syncLogger public protocol SearchableBookmarks: class { func bookmarksByURL(_ url: URL) -> Deferred>> } public protocol SyncableBookmarks: class, ResettableSyncStorage, AccountRemovalDelegate { // TODO func isUnchanged() -> Deferred> func getLocalBookmarksModifications(limit: Int) -> Deferred> func getLocalDeletions() -> Deferred> func treesForEdges() -> Deferred> func treeForMirror() -> Deferred> func applyLocalOverrideCompletionOp(_ op: LocalOverrideCompletionOp, itemSources: ItemSources) -> Success func applyBufferUpdatedCompletionOp(_ op: BufferUpdatedCompletionOp) -> Success } public let NotificationBookmarkBufferValidated = Notification.Name("NotificationBookmarkBufferValidated") public protocol BookmarkBufferStorage: class { func isEmpty() -> Deferred> func applyRecords(_ records: [BookmarkMirrorItem]) -> Success func doneApplyingRecordsAfterDownload() -> Success func validate() -> Success func getBufferedDeletions() -> Deferred> func applyBufferCompletionOp(_ op: BufferCompletionOp, itemSources: ItemSources) -> Success // Only use for diagnostics. func synchronousBufferCount() -> Int? func getUpstreamRecordCount() -> Deferred } public protocol MirrorItemSource: class { func getMirrorItemWithGUID(_ guid: GUID) -> Deferred> func getMirrorItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID func prefetchMirrorItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID } public protocol BufferItemSource: class { func getBufferItemWithGUID(_ guid: GUID) -> Deferred> func getBufferItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID func getBufferChildrenGUIDsForParent(_ guid: GUID) -> Deferred> func prefetchBufferItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID } public protocol LocalItemSource: class { func getLocalItemWithGUID(_ guid: GUID) -> Deferred> func getLocalItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID func prefetchLocalItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID } open class ItemSources { open let local: LocalItemSource open let mirror: MirrorItemSource open let buffer: BufferItemSource public init(local: LocalItemSource, mirror: MirrorItemSource, buffer: BufferItemSource) { self.local = local self.mirror = mirror self.buffer = buffer } open func prefetchWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { return self.local.prefetchLocalItemsWithGUIDs(guids) >>> { self.mirror.prefetchMirrorItemsWithGUIDs(guids) } >>> { self.buffer.prefetchBufferItemsWithGUIDs(guids) } } } public struct BookmarkRoots { // These match Places on desktop. public static let RootGUID = "root________" public static let MobileFolderGUID = "mobile______" public static let MenuFolderGUID = "menu________" public static let ToolbarFolderGUID = "toolbar_____" public static let UnfiledFolderGUID = "unfiled_____" public static let FakeDesktopFolderGUID = "desktop_____" // Pseudo. Never mentioned in a real record. // This is the order we use. public static let RootChildren: [GUID] = [ BookmarkRoots.MenuFolderGUID, BookmarkRoots.ToolbarFolderGUID, BookmarkRoots.UnfiledFolderGUID, BookmarkRoots.MobileFolderGUID, ] public static let DesktopRoots: [GUID] = [ BookmarkRoots.MenuFolderGUID, BookmarkRoots.ToolbarFolderGUID, BookmarkRoots.UnfiledFolderGUID, ] public static let Real = Set([ BookmarkRoots.RootGUID, BookmarkRoots.MobileFolderGUID, BookmarkRoots.MenuFolderGUID, BookmarkRoots.ToolbarFolderGUID, BookmarkRoots.UnfiledFolderGUID, ]) public static let All = Set([ BookmarkRoots.RootGUID, BookmarkRoots.MobileFolderGUID, BookmarkRoots.MenuFolderGUID, BookmarkRoots.ToolbarFolderGUID, BookmarkRoots.UnfiledFolderGUID, BookmarkRoots.FakeDesktopFolderGUID, ]) /** * Sync records are a horrible mess of Places-native GUIDs and Sync-native IDs. * For example: * {"id":"places", * "type":"folder", * "title":"", * "description":null, * "children":["menu________","toolbar_____", * "tags________","unfiled_____", * "jKnyPDrBQSDg","T6XK5oJMU8ih"], * "parentid":"2hYxKgBwvkEH"}" * * We thus normalize on the extended Places IDs (with underscores) for * local storage, and translate to the Sync IDs when creating an outbound * record. * We translate the record's ID and also its parent. Evidence suggests that * we don't need to translate children IDs. * * TODO: We don't create outbound records yet, so that's why there's no * translation in that direction yet! */ public static func translateIncomingRootGUID(_ guid: GUID) -> GUID { return [ "places": RootGUID, "root": RootGUID, "mobile": MobileFolderGUID, "menu": MenuFolderGUID, "toolbar": ToolbarFolderGUID, "unfiled": UnfiledFolderGUID ][guid] ?? guid } public static func translateOutgoingRootGUID(_ guid: GUID) -> GUID { return [ RootGUID: "places", MobileFolderGUID: "mobile", MenuFolderGUID: "menu", ToolbarFolderGUID: "toolbar", UnfiledFolderGUID: "unfiled" ][guid] ?? guid } /* public static let TagsFolderGUID = "tags________" public static let PinnedFolderGUID = "pinned______" */ static let RootID = 0 static let MobileID = 1 static let MenuID = 2 static let ToolbarID = 3 static let UnfiledID = 4 } /** * This partly matches Places's nsINavBookmarksService, just for sanity. * * It is further extended to support the types that exist in Sync, so we can use * this to store mirrored rows. * * These are only used at the DB layer. */ public enum BookmarkNodeType: Int { case bookmark = 1 case folder = 2 case separator = 3 case dynamicContainer = 4 case livemark = 5 case query = 6 // No microsummary: those turn into bookmarks. } public func == (lhs: BookmarkMirrorItem, rhs: BookmarkMirrorItem) -> Bool { if lhs.type != rhs.type || lhs.guid != rhs.guid || lhs.dateAdded != rhs.dateAdded || lhs.serverModified != rhs.serverModified || lhs.isDeleted != rhs.isDeleted || lhs.hasDupe != rhs.hasDupe || lhs.pos != rhs.pos || lhs.faviconID != rhs.faviconID || lhs.localModified != rhs.localModified || lhs.parentID != rhs.parentID || lhs.parentName != rhs.parentName || lhs.feedURI != rhs.feedURI || lhs.siteURI != rhs.siteURI || lhs.title != rhs.title || lhs.description != rhs.description || lhs.bookmarkURI != rhs.bookmarkURI || lhs.tags != rhs.tags || lhs.keyword != rhs.keyword || lhs.folderName != rhs.folderName || lhs.queryID != rhs.queryID { return false } if let lhsChildren = lhs.children, let rhsChildren = rhs.children { return lhsChildren == rhsChildren } return lhs.children == nil && rhs.children == nil } public struct BookmarkMirrorItem: Equatable { public let guid: GUID public let type: BookmarkNodeType public let dateAdded: Timestamp? public var serverModified: Timestamp public let isDeleted: Bool public let hasDupe: Bool public let parentID: GUID? public let parentName: String? // Livemarks. public let feedURI: String? public let siteURI: String? // Separators. let pos: Int? // Folders, livemarks, bookmarks and queries. public let title: String? let description: String? // Bookmarks and queries. let bookmarkURI: String? let tags: String? let keyword: String? // Queries. let folderName: String? let queryID: String? // Folders. public let children: [GUID]? // Internal stuff. let faviconID: Int? public let localModified: Timestamp? let syncStatus: SyncStatus? public func copyWithDateAdded(_ dateAdded: Timestamp) -> BookmarkMirrorItem { return BookmarkMirrorItem( guid: self.guid, type: self.type, dateAdded: dateAdded, serverModified: self.serverModified, isDeleted: self.isDeleted, hasDupe: self.hasDupe, parentID: self.parentID, parentName: self.parentName, feedURI: self.feedURI, siteURI: self.siteURI, pos: self.pos, title: self.title, description: self.description, bookmarkURI: self.bookmarkURI, tags: self.tags, keyword: self.keyword, folderName: self.folderName, queryID: self.queryID, children: self.children, faviconID: self.faviconID, localModified: self.localModified, syncStatus: self.syncStatus) } public func copyWithParentID(_ parentID: GUID, parentName: String?) -> BookmarkMirrorItem { return BookmarkMirrorItem( guid: self.guid, type: self.type, dateAdded: self.dateAdded, serverModified: self.serverModified, isDeleted: self.isDeleted, hasDupe: self.hasDupe, parentID: parentID, parentName: parentName, feedURI: self.feedURI, siteURI: self.siteURI, pos: self.pos, title: self.title, description: self.description, bookmarkURI: self.bookmarkURI, tags: self.tags, keyword: self.keyword, folderName: self.folderName, queryID: self.queryID, children: self.children, faviconID: self.faviconID, localModified: self.localModified, syncStatus: self.syncStatus) } // Ignores internal metadata and GUID; a pure value comparison. // Does compare child GUIDs! public func sameAs(_ rhs: BookmarkMirrorItem) -> Bool { if self.type != rhs.type || self.dateAdded != rhs.dateAdded || self.isDeleted != rhs.isDeleted || self.pos != rhs.pos || self.parentID != rhs.parentID || self.parentName != rhs.parentName || self.feedURI != rhs.feedURI || self.siteURI != rhs.siteURI || self.title != rhs.title || (self.description ?? "") != (rhs.description ?? "") || self.bookmarkURI != rhs.bookmarkURI || self.tags != rhs.tags || self.keyword != rhs.keyword || self.folderName != rhs.folderName || self.queryID != rhs.queryID { return false } if let lhsChildren = self.children, let rhsChildren = rhs.children { return lhsChildren == rhsChildren } return self.children == nil && rhs.children == nil } public func asJSON() -> JSON { return self.asJSONWithChildren(self.children) } public func asJSONWithChildren(_ children: [GUID]?) -> JSON { var out: [String: Any] = [:] out["id"] = BookmarkRoots.translateOutgoingRootGUID(self.guid) func take(_ key: String, _ val: String?) { guard let val = val else { return } out[key] = val } if self.isDeleted { out["deleted"] = true return JSON(out) } out["dateAdded"] = self.dateAdded out["hasDupe"] = self.hasDupe // TODO: this should never be nil! if let parentID = self.parentID { out["parentid"] = BookmarkRoots.translateOutgoingRootGUID(parentID) take("parentName", titleForSpecialGUID(parentID) ?? self.parentName ?? "") } func takeBookmarkFields() { take("title", self.title) take("bmkUri", self.bookmarkURI) take("description", self.description) if let tags = self.tags { let tagsJSON = JSON(parseJSON: tags) if let tagsArray = tagsJSON.array, tagsArray.every({ $0.type == SwiftyJSON.Type.string }) { out["tags"] = tagsArray } else { out["tags"] = [] } } else { out["tags"] = [] } take("keyword", self.keyword) } func takeFolderFields() { take("title", titleForSpecialGUID(self.guid) ?? self.title) take("description", self.description) if let children = children { if BookmarkRoots.RootGUID == self.guid { // Only the root contains roots, and so only its children // need to be translated. out["children"] = children.map(BookmarkRoots.translateOutgoingRootGUID) } else { out["children"] = children } } } switch self.type { case .query: out["type"] = "query" take("folderName", self.folderName) take("queryId", self.queryID) takeBookmarkFields() case .bookmark: out["type"] = "bookmark" takeBookmarkFields() case .livemark: out["type"] = "livemark" take("siteUri", self.siteURI) take("feedUri", self.feedURI) takeFolderFields() case .folder: out["type"] = "folder" takeFolderFields() case .separator: out["type"] = "separator" if let pos = self.pos { out["pos"] = pos } case .dynamicContainer: // Sigh. preconditionFailure("DynamicContainer not supported.") } return JSON(out) } // The places root is a folder but has no parentName. public static func folder(_ guid: GUID, dateAdded: Timestamp?, modified: Timestamp, hasDupe: Bool, parentID: GUID, parentName: String?, title: String, description: String?, children: [GUID]) -> BookmarkMirrorItem { let id = BookmarkRoots.translateIncomingRootGUID(guid) let parent = BookmarkRoots.translateIncomingRootGUID(parentID) return BookmarkMirrorItem(guid: id, type: .folder, dateAdded: dateAdded, serverModified: modified, isDeleted: false, hasDupe: hasDupe, parentID: parent, parentName: parentName, feedURI: nil, siteURI: nil, pos: nil, title: title, description: description, bookmarkURI: nil, tags: nil, keyword: nil, folderName: nil, queryID: nil, children: children, faviconID: nil, localModified: nil, syncStatus: nil) } public static func livemark(_ guid: GUID, dateAdded: Timestamp?, modified: Timestamp, hasDupe: Bool, parentID: GUID, parentName: String?, title: String?, description: String?, feedURI: String, siteURI: String) -> BookmarkMirrorItem { let id = BookmarkRoots.translateIncomingRootGUID(guid) let parent = BookmarkRoots.translateIncomingRootGUID(parentID) return BookmarkMirrorItem(guid: id, type: .livemark, dateAdded: dateAdded, serverModified: modified, isDeleted: false, hasDupe: hasDupe, parentID: parent, parentName: parentName, feedURI: feedURI, siteURI: siteURI, pos: nil, title: title, description: description, bookmarkURI: nil, tags: nil, keyword: nil, folderName: nil, queryID: nil, children: nil, faviconID: nil, localModified: nil, syncStatus: nil) } public static func separator(_ guid: GUID, dateAdded: Timestamp?, modified: Timestamp, hasDupe: Bool, parentID: GUID, parentName: String?, pos: Int) -> BookmarkMirrorItem { let id = BookmarkRoots.translateIncomingRootGUID(guid) let parent = BookmarkRoots.translateIncomingRootGUID(parentID) return BookmarkMirrorItem(guid: id, type: .separator, dateAdded: dateAdded, serverModified: modified, isDeleted: false, hasDupe: hasDupe, parentID: parent, parentName: parentName, feedURI: nil, siteURI: nil, pos: pos, title: nil, description: nil, bookmarkURI: nil, tags: nil, keyword: nil, folderName: nil, queryID: nil, children: nil, faviconID: nil, localModified: nil, syncStatus: nil) } public static func bookmark(_ guid: GUID, dateAdded: Timestamp?, modified: Timestamp, hasDupe: Bool, parentID: GUID, parentName: String?, title: String, description: String?, URI: String, tags: String, keyword: String?) -> BookmarkMirrorItem { let id = BookmarkRoots.translateIncomingRootGUID(guid) let parent = BookmarkRoots.translateIncomingRootGUID(parentID) return BookmarkMirrorItem(guid: id, type: .bookmark, dateAdded: dateAdded, serverModified: modified, isDeleted: false, hasDupe: hasDupe, parentID: parent, parentName: parentName, feedURI: nil, siteURI: nil, pos: nil, title: title, description: description, bookmarkURI: URI, tags: tags, keyword: keyword, folderName: nil, queryID: nil, children: nil, faviconID: nil, localModified: nil, syncStatus: nil) } public static func query(_ guid: GUID, dateAdded: Timestamp?, modified: Timestamp, hasDupe: Bool, parentID: GUID, parentName: String?, title: String, description: String?, URI: String, tags: String, keyword: String?, folderName: String?, queryID: String?) -> BookmarkMirrorItem { let id = BookmarkRoots.translateIncomingRootGUID(guid) let parent = BookmarkRoots.translateIncomingRootGUID(parentID) return BookmarkMirrorItem(guid: id, type: .query, dateAdded: dateAdded, serverModified: modified, isDeleted: false, hasDupe: hasDupe, parentID: parent, parentName: parentName, feedURI: nil, siteURI: nil, pos: nil, title: title, description: description, bookmarkURI: URI, tags: tags, keyword: keyword, folderName: folderName, queryID: queryID, children: nil, faviconID: nil, localModified: nil, syncStatus: nil) } public static func deleted(_ type: BookmarkNodeType, guid: GUID, modified: Timestamp) -> BookmarkMirrorItem { let id = BookmarkRoots.translateIncomingRootGUID(guid) return BookmarkMirrorItem(guid: id, type: type, dateAdded: nil, serverModified: modified, isDeleted: true, hasDupe: false, parentID: nil, parentName: nil, feedURI: nil, siteURI: nil, pos: nil, title: nil, description: nil, bookmarkURI: nil, tags: nil, keyword: nil, folderName: nil, queryID: nil, children: nil, faviconID: nil, localModified: nil, syncStatus: nil) } }