Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
ControlCenter
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
/* 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)
}
}