mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-09 01:08:39 +09:00
571 lines
16 KiB
Swift
571 lines
16 KiB
Swift
/* 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)
|
|
}
|
|
}
|