This repository has been archived by the owner. It is now read-only.
Permalink
Cannot retrieve contributors at this time
executable file
571 lines (468 sloc)
16.3 KB
| /* 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 Storage | |
| import XCGLogger | |
| import SwiftyJSON | |
| private let log = Logger.syncLogger | |
| public protocol MirrorItemable { | |
| func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem | |
| } | |
| extension BookmarkMirrorItem { | |
| func asPayload() -> BookmarkBasePayload { | |
| return BookmarkType.somePayloadFromJSON(self.asJSON()) | |
| } | |
| func asPayloadWithChildren(_ children: [GUID]?) -> BookmarkBasePayload { | |
| let remappedChildren: [GUID]? | |
| if let children = children { | |
| if BookmarkRoots.RootGUID == self.guid { | |
| // Only the root contains roots, and so only its children | |
| // need to be translated. | |
| remappedChildren = children.map(BookmarkRoots.translateOutgoingRootGUID) | |
| } else { | |
| remappedChildren = children | |
| } | |
| } else { | |
| remappedChildren = nil | |
| } | |
| let json = self.asJSONWithChildren(remappedChildren) | |
| return BookmarkType.somePayloadFromJSON(json) | |
| } | |
| } | |
| /** | |
| * Hierarchy: | |
| * - BookmarkBasePayload | |
| * \_ FolderPayload | |
| * \_ LivemarkPayload | |
| * \_ SeparatorPayload | |
| * \_ BookmarkPayload | |
| * \_ BookmarkQueryPayload | |
| */ | |
| public enum BookmarkType: String { | |
| case livemark | |
| case separator | |
| case folder | |
| case bookmark | |
| case query | |
| case microsummary // Dead: now a bookmark. | |
| // The result might be invalid, but it won't be nil. | |
| public static func somePayloadFromJSON(_ json: JSON) -> BookmarkBasePayload { | |
| return payloadFromJSON(json) ?? BookmarkBasePayload(json) | |
| } | |
| public static func payloadFromJSON(_ json: JSON) -> BookmarkBasePayload? { | |
| if json["deleted"].bool ?? false { | |
| // Deleted records won't have a type. | |
| return BookmarkBasePayload(json) | |
| } | |
| guard let typeString = json["type"].string else { | |
| return nil | |
| } | |
| guard let type = BookmarkType(rawValue: typeString) else { | |
| return nil | |
| } | |
| switch type { | |
| case microsummary: | |
| fallthrough | |
| case bookmark: | |
| return BookmarkPayload(json) | |
| case folder: | |
| return FolderPayload(json) | |
| case livemark: | |
| return LivemarkPayload(json) | |
| case separator: | |
| return SeparatorPayload(json) | |
| case query: | |
| return BookmarkQueryPayload(json) | |
| } | |
| } | |
| public static func isValid(_ type: String?) -> Bool { | |
| guard let type = type else { | |
| return false | |
| } | |
| return BookmarkType(rawValue: type) != nil | |
| } | |
| } | |
| open class LivemarkPayload: BookmarkBasePayload { | |
| open var feedURI: String? { | |
| return self["feedUri"].string | |
| } | |
| open var siteURI: String? { | |
| return self["siteUri"].string | |
| } | |
| override open func isValid() -> Bool { | |
| if !super.isValid() { | |
| return false | |
| } | |
| return self.hasRequiredStringFields(["feedUri", "siteUri"]) | |
| } | |
| override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { | |
| guard let p = obj as? LivemarkPayload else { | |
| return false | |
| } | |
| if !super.equalPayloads(p) { | |
| return false | |
| } | |
| if self.deleted { | |
| return true | |
| } | |
| if self.feedURI != p.feedURI { | |
| return false | |
| } | |
| if self.siteURI != p.siteURI { | |
| return false | |
| } | |
| return true | |
| } | |
| override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem { | |
| if self.deleted { | |
| return BookmarkMirrorItem.deleted(.livemark, guid: self.id, modified: modified) | |
| } | |
| return BookmarkMirrorItem.livemark( | |
| self.id, | |
| dateAdded: self["dateAdded"].uInt64, | |
| modified: modified, | |
| hasDupe: self.hasDupe, | |
| // TODO: these might need to be weakened if real-world data is dirty. | |
| parentID: self["parentid"].stringValue, | |
| parentName: self["parentName"].string, | |
| title: self["title"].string, | |
| description: self["description"].string, | |
| feedURI: self.feedURI!, | |
| siteURI: self.siteURI! | |
| ) | |
| } | |
| } | |
| open class SeparatorPayload: BookmarkBasePayload { | |
| override open func isValid() -> Bool { | |
| if !super.isValid() { | |
| return false | |
| } | |
| if !self["pos"].isInt() { | |
| log.warning("Separator \(self.id) missing pos.") | |
| return false | |
| } | |
| return true | |
| } | |
| override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { | |
| guard let p = obj as? SeparatorPayload else { | |
| return false | |
| } | |
| if !super.equalPayloads(p) { | |
| return false | |
| } | |
| if self.deleted { | |
| return true | |
| } | |
| if self["pos"].int != p["pos"].int { | |
| return false | |
| } | |
| return true | |
| } | |
| override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem { | |
| if self.deleted { | |
| return BookmarkMirrorItem.deleted(.separator, guid: self.id, modified: modified) | |
| } | |
| return BookmarkMirrorItem.separator( | |
| self.id, | |
| dateAdded: self["dateAdded"].uInt64, | |
| modified: modified, | |
| hasDupe: self.hasDupe, | |
| // TODO: these might need to be weakened if real-world data is dirty. | |
| parentID: self["parentid"].string!, | |
| parentName: self["parentName"].string, | |
| pos: self["pos"].int! | |
| ) | |
| } | |
| } | |
| open class FolderPayload: BookmarkBasePayload { | |
| fileprivate var childrenAreValid: Bool { | |
| return self.hasStringArrayField("children") | |
| } | |
| override open func isValid() -> Bool { | |
| if !super.isValid() { | |
| return false | |
| } | |
| if !self.hasRequiredStringFields(["title"]) { | |
| log.warning("Folder \(self.id) missing title.") | |
| return false | |
| } | |
| if !self.hasOptionalStringFields(["description"]) { | |
| log.warning("Folder \(self.id) missing string description.") | |
| return false | |
| } | |
| if !self.childrenAreValid { | |
| log.warning("Folder \(self.id) has invalid children.") | |
| return false | |
| } | |
| return true | |
| } | |
| open var children: [String] { | |
| return self["children"].arrayValue.map { $0.string! } | |
| } | |
| override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { | |
| guard let p = obj as? FolderPayload else { | |
| return false | |
| } | |
| if !super.equalPayloads(p) { | |
| return false | |
| } | |
| if self.deleted { | |
| return true | |
| } | |
| if self["title"].string != p["title"].string { | |
| return false | |
| } | |
| if self["description"].string != p["description"].string { | |
| return false | |
| } | |
| if self.children != p.children { | |
| return false | |
| } | |
| return true | |
| } | |
| override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem { | |
| if self.deleted { | |
| return BookmarkMirrorItem.deleted(.folder, guid: self.id, modified: modified) | |
| } | |
| return BookmarkMirrorItem.folder( | |
| self.id, | |
| dateAdded: self["dateAdded"].uInt64, | |
| modified: modified, | |
| hasDupe: self.hasDupe, | |
| // TODO: these might need to be weakened if real-world data is dirty. | |
| parentID: self["parentid"].string!, | |
| parentName: self["parentName"].string, | |
| title: self["title"].string!, | |
| description: self["description"].string, | |
| children: self.children | |
| ) | |
| } | |
| } | |
| open class BookmarkPayload: BookmarkBasePayload { | |
| fileprivate static let requiredBookmarkStringFields = ["bmkUri"] | |
| // Title *should* be required, but can be missing for queries. Great. | |
| fileprivate static let optionalBookmarkStringFields = ["title", "keyword", "description"] | |
| fileprivate static let optionalBookmarkBooleanFields = ["loadInSidebar"] | |
| override open func isValid() -> Bool { | |
| if !super.isValid() { | |
| return false | |
| } | |
| if !self.hasRequiredStringFields(BookmarkPayload.requiredBookmarkStringFields) { | |
| log.warning("Bookmark \(self.id) missing required string field.") | |
| return false | |
| } | |
| if !self.hasStringArrayField("tags") { | |
| log.warning("Bookmark \(self.id) missing tags array. We'll replace with an empty array.") | |
| // Ignore. | |
| } | |
| if !self.hasOptionalStringFields(BookmarkPayload.optionalBookmarkStringFields) { | |
| return false | |
| } | |
| return self.hasOptionalBooleanFields(BookmarkPayload.optionalBookmarkBooleanFields) | |
| } | |
| override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { | |
| guard let p = obj as? BookmarkPayload else { | |
| return false | |
| } | |
| if !super.equalPayloads(p) { | |
| return false | |
| } | |
| if self.deleted { | |
| return true | |
| } | |
| if !BookmarkPayload.requiredBookmarkStringFields.every({ p[$0].string! == self[$0].string! }) { | |
| return false | |
| } | |
| // TODO: compare optional fields. | |
| if Set(self.tags) != Set(p.tags) { | |
| return false | |
| } | |
| if self["loadInSidebar"].bool != p["loadInSidebar"].bool { | |
| return false | |
| } | |
| return true | |
| } | |
| lazy var tags: [String] = { | |
| return self["tags"].arrayValue.flatMap { $0.string } | |
| }() | |
| lazy var tagsString: String = { | |
| if self["tags"].isArray() { | |
| return self["tags"].stringValue() ?? "[]" | |
| } | |
| return "[]" | |
| }() | |
| override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem { | |
| if self.deleted { | |
| return BookmarkMirrorItem.deleted(.bookmark, guid: self.id, modified: modified) | |
| } | |
| return BookmarkMirrorItem.bookmark( | |
| self.id, | |
| dateAdded: self["dateAdded"].uInt64, | |
| modified: modified, | |
| hasDupe: self.hasDupe, | |
| // TODO: these might need to be weakened if real-world data is dirty. | |
| parentID: self["parentid"].string!, | |
| parentName: self["parentName"].string, | |
| title: self["title"].string ?? "", | |
| description: self["description"].string, | |
| URI: self["bmkUri"].string!, | |
| tags: self.tagsString, // Stringify it so we can put the array in the DB. | |
| keyword: self["keyword"].string | |
| ) | |
| } | |
| } | |
| open class BookmarkQueryPayload: BookmarkPayload { | |
| override open func isValid() -> Bool { | |
| if !super.isValid() { | |
| return false | |
| } | |
| if !self.hasOptionalStringFields(["queryId", "folderName"]) { | |
| log.warning("Query \(self.id) missing queryId or folderName.") | |
| return false | |
| } | |
| return true | |
| } | |
| override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { | |
| guard let p = obj as? BookmarkQueryPayload else { | |
| return false | |
| } | |
| if !super.equalPayloads(p) { | |
| return false | |
| } | |
| if self.deleted { | |
| return true | |
| } | |
| if self["folderName"].string != p["folderName"].string { | |
| return false | |
| } | |
| if self["queryId"].string != p["queryId"].string { | |
| return false | |
| } | |
| return true | |
| } | |
| override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem { | |
| if self.deleted { | |
| return BookmarkMirrorItem.deleted(.query, guid: self.id, modified: modified) | |
| } | |
| return BookmarkMirrorItem.query( | |
| self.id, | |
| dateAdded: self["dateAdded"].uInt64, | |
| modified: modified, | |
| hasDupe: self.hasDupe, | |
| parentID: self["parentid"].string!, | |
| parentName: self["parentName"].string, | |
| title: self["title"].string ?? "", | |
| description: self["description"].string, | |
| URI: self["bmkUri"].string!, | |
| tags: self.tagsString, // Stringify it so we can put the array in the DB. | |
| keyword: self["keyword"].string, | |
| folderName: self["folderName"].string, | |
| queryID: self["queryID"].string | |
| ) | |
| } | |
| } | |
| open class BookmarkBasePayload: CleartextPayloadJSON, MirrorItemable { | |
| fileprivate static let requiredStringFields: [String] = ["parentid", "type"] | |
| fileprivate static let optionalBooleanFields: [String] = ["hasDupe"] | |
| static func deletedPayload(_ guid: GUID) -> BookmarkBasePayload { | |
| let remappedGUID = BookmarkRoots.translateOutgoingRootGUID(guid) | |
| return BookmarkBasePayload(JSON(["id": remappedGUID, "deleted": true])) | |
| } | |
| func hasStringArrayField(_ name: String) -> Bool { | |
| guard let arr = self[name].array else { | |
| return false | |
| } | |
| return arr.every { $0.isString() } | |
| } | |
| func hasRequiredStringFields(_ fields: [String]) -> Bool { | |
| return fields.every { self[$0].isString() } | |
| } | |
| func hasOptionalStringFields(_ fields: [String]) -> Bool { | |
| return fields.every { field in | |
| let val = self[field] | |
| // Yup, 404 is not found, so this means "string or nothing". | |
| let valid = val.isString() || val.isNull() || val.isError() | |
| if !valid { | |
| log.debug("Field \(field) is invalid: \(val).") | |
| } | |
| return valid | |
| } | |
| } | |
| func hasOptionalBooleanFields(_ fields: [String]) -> Bool { | |
| return fields.every { field in | |
| let val = self[field] | |
| // Yup, 404 is not found, so this means "boolean or nothing". | |
| let valid = val.isBool() || val.isNull() || val.error?.code == 404 | |
| if !valid { | |
| log.debug("Field \(field) is invalid: \(val).") | |
| } | |
| return valid | |
| } | |
| } | |
| override open func isValid() -> Bool { | |
| if !super.isValid() { | |
| return false | |
| } | |
| if self["deleted"].bool ?? false { | |
| return true | |
| } | |
| // If not deleted, we must be a specific, known, type! | |
| if !BookmarkType.isValid(self["type"].string) { | |
| return false | |
| } | |
| if !(self["parentName"].isString() || self.id == "places") { | |
| if self["parentid"].string! == "places" { | |
| log.debug("Accepting root with missing parent name.") | |
| } else { | |
| // Bug 1318414. | |
| log.warning("Accepting bookmark with missing parent name.") | |
| } | |
| } | |
| if !self.hasRequiredStringFields(BookmarkBasePayload.requiredStringFields) { | |
| log.warning("Item missing required string field.") | |
| return false | |
| } | |
| return self.hasOptionalBooleanFields(BookmarkBasePayload.optionalBooleanFields) | |
| } | |
| open var hasDupe: Bool { | |
| return self["hasDupe"].bool ?? false | |
| } | |
| /** | |
| * This only makes sense for valid payloads. | |
| */ | |
| override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { | |
| guard let p = obj as? BookmarkBasePayload else { | |
| return false | |
| } | |
| if !super.equalPayloads(p) { | |
| return false | |
| } | |
| if self.deleted { | |
| return true | |
| } | |
| if p.deleted { | |
| return self.deleted == p.deleted | |
| } | |
| // If either record is deleted, these other fields might be missing. | |
| // But we just checked, so we're good to roll on. | |
| let same: (String) -> Bool = { field in | |
| let left = self[field].string | |
| let right = p[field].string | |
| return left == right | |
| } | |
| if !BookmarkBasePayload.requiredStringFields.every(same) { | |
| return false | |
| } | |
| if p["parentName"].string != self["parentName"].string { | |
| return false | |
| } | |
| return self.hasDupe == p.hasDupe | |
| } | |
| // This goes here because extensions cannot override methods yet. | |
| open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem { | |
| precondition(self.deleted, "Non-deleted items should have a specific type.") | |
| return BookmarkMirrorItem.deleted(.bookmark, guid: self.id, modified: modified) | |
| } | |
| } |