mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 23:38:38 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
948
mobile/ios/Storage/SQL/SQLiteBookmarksModel.swift
Normal file
948
mobile/ios/Storage/SQL/SQLiteBookmarksModel.swift
Normal file
|
|
@ -0,0 +1,948 @@
|
|||
/* 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 Deferred
|
||||
import Foundation
|
||||
import Shared
|
||||
|
||||
private let log = Logger.syncLogger
|
||||
private let desktopBookmarksLabel: String = NSLocalizedString("Desktop Bookmarks", tableName: "BookmarkPanel", comment: "The folder name for the virtual folder that contains all desktop bookmarks.")
|
||||
|
||||
public enum Direction {
|
||||
case buffer
|
||||
case local
|
||||
|
||||
var structureTable: String {
|
||||
switch self {
|
||||
case .local:
|
||||
return TableBookmarksLocalStructure
|
||||
case .buffer:
|
||||
return TableBookmarksBufferStructure
|
||||
}
|
||||
}
|
||||
|
||||
var valueTable: String {
|
||||
switch self {
|
||||
case .local:
|
||||
return TableBookmarksLocal
|
||||
case .buffer:
|
||||
return TableBookmarksBuffer
|
||||
}
|
||||
}
|
||||
|
||||
var valueView: String {
|
||||
switch self {
|
||||
case .local:
|
||||
return ViewBookmarksLocalOnMirror
|
||||
case .buffer:
|
||||
return ViewBookmarksBufferWithDeletionsOnMirror
|
||||
}
|
||||
}
|
||||
|
||||
var structureView: String {
|
||||
switch self {
|
||||
case .local:
|
||||
return ViewBookmarksLocalStructureOnMirror
|
||||
case .buffer:
|
||||
return ViewBookmarksBufferStructureOnMirror
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol KeywordSearchSource {
|
||||
func getURLForKeywordSearch(_ keyword: String) -> Deferred<Maybe<String>>
|
||||
}
|
||||
|
||||
open class SQLiteBookmarksModelFactory: BookmarksModelFactory {
|
||||
fileprivate let bookmarks: SQLiteBookmarks
|
||||
fileprivate let direction: Direction
|
||||
|
||||
public init(bookmarks: SQLiteBookmarks, direction: Direction) {
|
||||
self.bookmarks = bookmarks
|
||||
self.direction = direction
|
||||
}
|
||||
|
||||
public func factoryForIndex(_ index: Int, inFolder folder: BookmarkFolder) -> BookmarksModelFactory {
|
||||
return self
|
||||
}
|
||||
|
||||
fileprivate func withDifferentDirection(_ direction: Direction) -> SQLiteBookmarksModelFactory {
|
||||
if self.direction == direction {
|
||||
return self
|
||||
}
|
||||
return SQLiteBookmarksModelFactory(bookmarks: self.bookmarks, direction: direction)
|
||||
}
|
||||
|
||||
fileprivate func getChildrenWithParent(_ parentGUID: GUID, excludingGUIDs: [GUID]?=nil, includeIcon: Bool) -> Deferred<Maybe<Cursor<BookmarkNode>>> {
|
||||
return self.bookmarks.getChildrenWithParent(parentGUID, direction: self.direction, excludingGUIDs: excludingGUIDs, includeIcon: includeIcon)
|
||||
}
|
||||
|
||||
fileprivate func getRootChildren() -> Deferred<Maybe<Cursor<BookmarkNode>>> {
|
||||
return self.getChildrenWithParent(BookmarkRoots.RootGUID, excludingGUIDs: [BookmarkRoots.RootGUID], includeIcon: true)
|
||||
}
|
||||
|
||||
fileprivate func getChildren(_ guid: String) -> Deferred<Maybe<Cursor<BookmarkNode>>> {
|
||||
return self.getChildrenWithParent(guid, includeIcon: true)
|
||||
}
|
||||
|
||||
func folderForGUID(_ guid: GUID, title: String) -> Deferred<Maybe<BookmarkFolder>> {
|
||||
return self.getChildren(guid)
|
||||
>>== { cursor in
|
||||
|
||||
if cursor.status == .failure {
|
||||
return deferMaybe(DatabaseError(description: "Couldn't get children: \(cursor.statusMessage)."))
|
||||
}
|
||||
|
||||
return deferMaybe(SQLiteBookmarkFolder(guid: guid, title: title, children: cursor))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func modelWithRoot(_ root: BookmarkFolder) -> Deferred<Maybe<BookmarksModel>> {
|
||||
return deferMaybe(BookmarksModel(modelFactory: self, root: root))
|
||||
}
|
||||
|
||||
open func modelForFolder(_ guid: String, title: String) -> Deferred<Maybe<BookmarksModel>> {
|
||||
if guid == BookmarkRoots.MobileFolderGUID {
|
||||
return self.modelForRoot()
|
||||
}
|
||||
|
||||
if guid == BookmarkRoots.FakeDesktopFolderGUID {
|
||||
return self.modelForDesktopBookmarks()
|
||||
}
|
||||
|
||||
let outputTitle = titleForSpecialGUID(guid) ?? title
|
||||
return self.folderForGUID(guid, title: outputTitle)
|
||||
>>== self.modelWithRoot
|
||||
}
|
||||
|
||||
open func modelForFolder(_ folder: BookmarkFolder) -> Deferred<Maybe<BookmarksModel>> {
|
||||
return self.modelForFolder(folder.guid, title: folder.title)
|
||||
}
|
||||
|
||||
open func modelForFolder(_ guid: String) -> Deferred<Maybe<BookmarksModel>> {
|
||||
return self.modelForFolder(guid, title: "")
|
||||
}
|
||||
|
||||
open func modelForRoot() -> Deferred<Maybe<BookmarksModel>> {
|
||||
log.debug("Getting model for root.")
|
||||
let getFolder = self.folderForGUID(BookmarkRoots.MobileFolderGUID, title: BookmarksFolderTitleMobile)
|
||||
if self.direction == .buffer {
|
||||
return getFolder >>== self.modelWithRoot
|
||||
}
|
||||
|
||||
// Return a virtual model containing "Desktop bookmarks" prepended to the local mobile bookmarks.
|
||||
return getFolder >>== { folder in
|
||||
self.extendWithDesktopBookmarksFolder(folder, factory: self)
|
||||
}
|
||||
}
|
||||
|
||||
open var nullModel: BookmarksModel {
|
||||
let children = Cursor<BookmarkNode>(status: .failure, msg: "Null model")
|
||||
let folder = SQLiteBookmarkFolder(guid: "Null", title: "Null", children: children)
|
||||
return BookmarksModel(modelFactory: self, root: folder)
|
||||
}
|
||||
|
||||
open func isBookmarked(_ url: String) -> Deferred<Maybe<Bool>> {
|
||||
return self.bookmarks.isBookmarked(url, direction: self.direction)
|
||||
}
|
||||
|
||||
open func removeByURL(_ url: String) -> Success {
|
||||
if self.direction == Direction.buffer {
|
||||
return deferMaybe(DatabaseError(description: "Refusing to remove URL from buffer in model."))
|
||||
}
|
||||
|
||||
// Find all of the records for the provided URL. Don't bother with
|
||||
// any that are already deleted!
|
||||
return self.bookmarks.nonDeletedGUIDsForURL(url)
|
||||
>>== self.bookmarks.removeGUIDs
|
||||
}
|
||||
|
||||
open func removeByGUID(_ guid: GUID) -> Success {
|
||||
if self.direction == Direction.buffer {
|
||||
return deferMaybe(DatabaseError(description: "Refusing to remove GUID from buffer in model."))
|
||||
}
|
||||
|
||||
log.debug("removeByGUID: \(guid)")
|
||||
return self.bookmarks.removeGUIDs([guid])
|
||||
}
|
||||
|
||||
func hasDesktopBookmarks() -> Deferred<Maybe<Bool>> {
|
||||
// This is very lazy, but it has the nice property of keeping Desktop Bookmarks visible
|
||||
// for a while after you mark the last desktop child as deleted.
|
||||
let parents: Args = [
|
||||
// Local.
|
||||
BookmarkRoots.MenuFolderGUID,
|
||||
BookmarkRoots.ToolbarFolderGUID,
|
||||
BookmarkRoots.UnfiledFolderGUID,
|
||||
|
||||
// Mirror.
|
||||
BookmarkRoots.MenuFolderGUID,
|
||||
BookmarkRoots.ToolbarFolderGUID,
|
||||
BookmarkRoots.UnfiledFolderGUID,
|
||||
]
|
||||
|
||||
let sql =
|
||||
"SELECT 1 FROM \(self.direction.structureTable) WHERE parent IN (?, ?, ?)" +
|
||||
" UNION ALL " +
|
||||
"SELECT 1 FROM \(TableBookmarksMirrorStructure) WHERE parent IN (?, ?, ?)" +
|
||||
" LIMIT 1"
|
||||
|
||||
return self.bookmarks.db.queryReturnsResults(sql, args: parents)
|
||||
}
|
||||
|
||||
func getDesktopRoots() -> Deferred<Maybe<Cursor<BookmarkNode>>> {
|
||||
if self.direction == .buffer {
|
||||
// DesktopRoots excludes the Mobile folder, local and non-local mobile are aggregated
|
||||
return self.bookmarks.getRecordsWithGUIDs(BookmarkRoots.DesktopRoots, direction: self.direction, includeIcon: false)
|
||||
}
|
||||
|
||||
// We deliberately exclude the mobile folder, because we're inverting the containment
|
||||
// relationship here.
|
||||
let exclude = [BookmarkRoots.MobileFolderGUID, BookmarkRoots.RootGUID]
|
||||
return self.getChildrenWithParent(BookmarkRoots.RootGUID, excludingGUIDs: exclude, includeIcon: false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend the provided mobile bookmarks folder with a single folder.
|
||||
* The prepended folder is "Desktop Bookmarks". It contains mirrored folders.
|
||||
*/
|
||||
open func extendWithDesktopBookmarksFolder(_ mobile: BookmarkFolder, factory: BookmarksModelFactory) -> Deferred<Maybe<BookmarksModel>> {
|
||||
|
||||
func onlyMobile() -> Deferred<Maybe<BookmarksModel>> {
|
||||
// No desktop bookmarks.
|
||||
log.debug("No desktop bookmarks. Only showing mobile.")
|
||||
return deferMaybe(BookmarksModel(modelFactory: factory, root: mobile))
|
||||
}
|
||||
|
||||
return self.hasDesktopBookmarks() >>== { yes in
|
||||
if !yes {
|
||||
return onlyMobile()
|
||||
}
|
||||
|
||||
return self.getDesktopRoots() >>== { cursor in
|
||||
if cursor.count == 0 {
|
||||
// This shouldn't occur.
|
||||
return onlyMobile()
|
||||
}
|
||||
|
||||
let desktop = self.folderForDesktopBookmarksCursor(cursor)
|
||||
let prepended = PrependedBookmarkFolder(main: mobile, prepend: desktop)
|
||||
return deferMaybe(BookmarksModel(modelFactory: factory, root: prepended))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func modelForDesktopBookmarks() -> Deferred<Maybe<BookmarksModel>> {
|
||||
return self.getDesktopRoots() >>== { cursor in
|
||||
let desktop = self.folderForDesktopBookmarksCursor(cursor)
|
||||
return deferMaybe(BookmarksModel(modelFactory: self, root: desktop))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func folderForDesktopBookmarksCursor(_ cursor: Cursor<BookmarkNode>) -> SQLiteBookmarkFolder {
|
||||
return SQLiteBookmarkFolder(guid: BookmarkRoots.FakeDesktopFolderGUID, title: desktopBookmarksLabel, children: cursor)
|
||||
}
|
||||
}
|
||||
|
||||
class EditableBufferBookmarksSQLiteBookmarksModelFactory: SQLiteBookmarksModelFactory {
|
||||
override func getChildrenWithParent(_ parentGUID: GUID, excludingGUIDs: [GUID]?, includeIcon: Bool) -> Deferred<Maybe<Cursor<BookmarkNode>>> {
|
||||
if parentGUID == BookmarkRoots.MobileFolderGUID {
|
||||
return self.bookmarks.getChildrenWithParent(parentGUID, direction: self.direction, excludingGUIDs: excludingGUIDs, includeIcon: includeIcon, factory: BookmarkFactory.editableItemsFactory)
|
||||
}
|
||||
return super.getChildrenWithParent(parentGUID, excludingGUIDs: excludingGUIDs, includeIcon: includeIcon)
|
||||
}
|
||||
|
||||
override func removeByGUID(_ guid: GUID) -> Success {
|
||||
log.debug("Removing \(guid) from buffer.")
|
||||
return self.bookmarks.markBufferBookmarkAsDeleted(guid)
|
||||
}
|
||||
}
|
||||
|
||||
private func isEditableExpression(_ direction: Direction) -> String {
|
||||
if direction == .buffer {
|
||||
return "0"
|
||||
}
|
||||
|
||||
return "SELECT exists( " +
|
||||
" SELECT exists(SELECT 1 FROM \(TableBookmarksBuffer)) AS hasBuffer, exists(SELECT 1 FROM \(TableBookmarksMirror)) AS hasMirror " +
|
||||
" WHERE hasBuffer IS 0 OR hasMirror IS 0" +
|
||||
")"
|
||||
}
|
||||
|
||||
extension SQLiteBookmarks {
|
||||
|
||||
fileprivate func getRecordsWithGUIDs(_ guids: [GUID], direction: Direction, includeIcon: Bool) -> Deferred<Maybe<Cursor<BookmarkNode>>> {
|
||||
|
||||
let args: Args = guids
|
||||
let varlist = BrowserDB.varlist(args.count)
|
||||
let values =
|
||||
"SELECT -1 AS id, guid, type, date_added, is_deleted, parentid, parentName, feedUri, pos, title, bmkUri, siteUri, folderName, faviconID, (\(isEditableExpression(direction))) AS isEditable " +
|
||||
"FROM \(direction.valueView) WHERE guid IN \(varlist) AND NOT is_deleted"
|
||||
|
||||
let withIcon = [
|
||||
"SELECT bookmarks.id AS id, bookmarks.guid AS guid, bookmarks.type AS type,",
|
||||
" bookmarks.date_added AS date_added,",
|
||||
" bookmarks.is_deleted AS is_deleted,",
|
||||
" bookmarks.parentid AS parentid, bookmarks.parentName AS parentName,",
|
||||
" bookmarks.feedUri AS feedUri, bookmarks.pos AS pos, title AS title,",
|
||||
" bookmarks.bmkUri AS bmkUri, bookmarks.siteUri AS siteUri,",
|
||||
" bookmarks.folderName AS folderName,",
|
||||
" bookmarks.isEditable AS isEditable,",
|
||||
" favicons.url AS iconURL, favicons.date AS iconDate, favicons.type AS iconType",
|
||||
"FROM (", values, ") AS bookmarks",
|
||||
"LEFT OUTER JOIN favicons ON bookmarks.faviconID = favicons.id",
|
||||
"ORDER BY title ASC",
|
||||
].joined(separator: " ")
|
||||
|
||||
let sql = (includeIcon ? withIcon : values) + " ORDER BY title ASC"
|
||||
return self.db.runQuery(sql, args: args, factory: BookmarkFactory.factory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the children of the provided parent.
|
||||
* Rows are ordered by positional index.
|
||||
* This method is aware of is_overridden and deletion, using local override structure by preference.
|
||||
* Note that a folder can be empty locally; we thus use the flag rather than looking at the structure itself.
|
||||
*/
|
||||
func getChildrenWithParent(_ parentGUID: GUID, direction: Direction, excludingGUIDs: [GUID]?=nil, includeIcon: Bool, factory: @escaping (SDRow) -> BookmarkNode = BookmarkFactory.factory) -> Deferred<Maybe<Cursor<BookmarkNode>>> {
|
||||
|
||||
precondition((excludingGUIDs ?? []).count < 100, "Sanity bound for the number of GUIDs we can exclude.")
|
||||
|
||||
let valueView = direction.valueView
|
||||
let structureView = direction.structureView
|
||||
|
||||
let structure =
|
||||
"SELECT parent, child AS guid, idx FROM \(structureView) " +
|
||||
"WHERE parent = ?"
|
||||
|
||||
let values =
|
||||
"SELECT -1 AS id, guid, type, date_added, is_deleted, parentid, parentName, feedUri, pos, title, bmkUri, siteUri, folderName, faviconID, (\(isEditableExpression(direction))) AS isEditable " +
|
||||
"FROM \(valueView)"
|
||||
|
||||
// We exclude queries and dynamic containers, because we can't
|
||||
// usefully display them.
|
||||
let typeQuery = BookmarkNodeType.query.rawValue
|
||||
let typeDynamic = BookmarkNodeType.dynamicContainer.rawValue
|
||||
let typeFilter = " vals.type NOT IN (\(typeQuery), \(typeDynamic))"
|
||||
|
||||
let args: Args
|
||||
let exclusion: String
|
||||
if let excludingGUIDs = excludingGUIDs {
|
||||
args = ([parentGUID] + excludingGUIDs).map { $0 }
|
||||
exclusion = "\(typeFilter) AND vals.guid NOT IN " + BrowserDB.varlist(excludingGUIDs.count)
|
||||
} else {
|
||||
args = [parentGUID]
|
||||
exclusion = typeFilter
|
||||
}
|
||||
|
||||
let fleshed =
|
||||
"SELECT vals.id AS id, vals.guid AS guid, vals.type AS type, vals.date_added AS date_added, vals.is_deleted AS is_deleted, " +
|
||||
" vals.parentid AS parentid, vals.parentName AS parentName, vals.feedUri AS feedUri, " +
|
||||
" vals.siteUri AS siteUri," +
|
||||
" vals.pos AS pos, vals.title AS title, vals.bmkUri AS bmkUri, vals.folderName AS folderName, " +
|
||||
" vals.faviconID AS faviconID, " +
|
||||
" vals.isEditable AS isEditable, " +
|
||||
" structure.idx AS idx, " +
|
||||
" structure.parent AS _parent " +
|
||||
"FROM (\(structure)) AS structure JOIN (\(values)) AS vals " +
|
||||
"ON vals.guid = structure.guid " +
|
||||
"WHERE " + exclusion
|
||||
|
||||
let withIcon =
|
||||
"SELECT bookmarks.id AS id, bookmarks.guid AS guid, bookmarks.type AS type, " +
|
||||
" bookmarks.date_added AS date_added, " +
|
||||
" bookmarks.is_deleted AS is_deleted, " +
|
||||
" bookmarks.parentid AS parentid, bookmarks.parentName AS parentName, " +
|
||||
" bookmarks.feedUri AS feedUri, bookmarks.siteUri AS siteUri, " +
|
||||
" bookmarks.pos AS pos, title AS title, " +
|
||||
" bookmarks.bmkUri AS bmkUri, bookmarks.folderName AS folderName, " +
|
||||
" bookmarks.idx AS idx, bookmarks._parent AS _parent, " +
|
||||
" bookmarks.isEditable AS isEditable, " +
|
||||
" favicons.url AS iconURL, favicons.date AS iconDate, favicons.type AS iconType " +
|
||||
"FROM (\(fleshed)) AS bookmarks " +
|
||||
"LEFT OUTER JOIN favicons ON bookmarks.faviconID = favicons.id"
|
||||
|
||||
let sql = (includeIcon ? withIcon : fleshed) + " ORDER BY idx ASC"
|
||||
return self.db.runQuery(sql, args: args, factory: factory)
|
||||
}
|
||||
|
||||
// This is only used from tests.
|
||||
func clearBookmarks() -> Success {
|
||||
log.warning("CALLING clearBookmarks -- this should only be used from tests.")
|
||||
return self.db.run([
|
||||
("DELETE FROM \(TableBookmarksLocal) WHERE parentid IS NOT ?", [BookmarkRoots.RootGUID]),
|
||||
self.favicons.getCleanupFaviconsQuery()
|
||||
])
|
||||
}
|
||||
|
||||
public func removeGUIDs(_ guids: [GUID]) -> Success {
|
||||
log.debug("removeGUIDs: \(guids)")
|
||||
|
||||
// Override any parents that aren't already overridden. We're about to remove some
|
||||
// of their children.
|
||||
return self.overrideParentsOfGUIDs(guids)
|
||||
|
||||
// Find, recursively, any children of the provided GUIDs. This will only be the case
|
||||
// if you specify folders. This is a special case because we're removing *all*
|
||||
// children, so we don't need to do the reindexing dance.
|
||||
>>> { self.deleteChildrenOfGUIDs(guids) }
|
||||
|
||||
// Override any records that aren't already overridden.
|
||||
>>> { self.overrideGUIDs(guids) }
|
||||
|
||||
// Then delete the already-overridden records. We do this one at a time in order
|
||||
// to get indices correct in edge cases. (We do bulk-delete their children
|
||||
// one layer at a time, at least.)
|
||||
>>> { walk(guids, f: self.removeLocalByGUID) }
|
||||
}
|
||||
|
||||
fileprivate func nonDeletedGUIDsForURL(_ url: String) -> Deferred<Maybe<([GUID])>> {
|
||||
let sql = "SELECT DISTINCT guid FROM \(ViewBookmarksLocalOnMirror) WHERE bmkUri = ? AND is_deleted = 0"
|
||||
let args: Args = [url]
|
||||
|
||||
return self.db.runQuery(sql, args: args, factory: { $0[0] as! GUID }) >>== { guids in
|
||||
return deferMaybe(guids.asArray())
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func overrideParentsOfGUIDs(_ guids: [GUID]) -> Success {
|
||||
log.debug("Overriding parents of \(guids).")
|
||||
|
||||
// TODO: Yes, this can be done in one go.
|
||||
let getParentsSQL =
|
||||
"SELECT DISTINCT parent FROM \(ViewBookmarksLocalStructureOnMirror) " +
|
||||
"WHERE child IN \(BrowserDB.varlist(guids.count)) AND is_overridden = 0"
|
||||
let getParentsArgs: Args = guids
|
||||
|
||||
return self.db.runQuery(getParentsSQL, args: getParentsArgs, factory: { $0[0] as! GUID })
|
||||
>>== { parentsCursor in
|
||||
let parents = parentsCursor.asArray()
|
||||
log.debug("Overriding parents: \(parents).")
|
||||
let (sql, args) = self.getSQLToOverrideFolders(parents, atModifiedTime: Date.now())
|
||||
return self.db.run(sql.map { ($0, args) })
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func overrideGUIDs(_ guids: [GUID]) -> Success {
|
||||
log.debug("Overriding GUIDs: \(guids).")
|
||||
let (sql, args) = self.getSQLToOverrideNonFolders(guids, atModifiedTime: Date.now())
|
||||
return self.db.run(sql.map { ($0, args) })
|
||||
}
|
||||
|
||||
// Recursive.
|
||||
fileprivate func deleteChildrenOfGUIDs(_ guids: [GUID]) -> Success {
|
||||
if guids.isEmpty {
|
||||
return succeed()
|
||||
}
|
||||
|
||||
precondition(BookmarkRoots.All.intersection(guids).isEmpty, "You can't even touch the roots for removal.")
|
||||
|
||||
log.debug("Deleting children of \(guids).")
|
||||
|
||||
let topArgs: Args = guids
|
||||
let topVarlist = BrowserDB.varlist(topArgs.count)
|
||||
let query =
|
||||
"SELECT child FROM \(ViewBookmarksLocalStructureOnMirror) " +
|
||||
"WHERE parent IN \(topVarlist)"
|
||||
|
||||
// We're deleting whole folders, so we don't need to worry about indices.
|
||||
return self.db.runQuery(query, args: topArgs, factory: { $0[0] as! GUID })
|
||||
>>== { children in
|
||||
let childGUIDs = children.asArray()
|
||||
log.debug("… children of \(guids) are \(childGUIDs).")
|
||||
|
||||
if childGUIDs.isEmpty {
|
||||
log.debug("No children; nothing more to do.")
|
||||
return succeed()
|
||||
}
|
||||
|
||||
let childArgs: Args = childGUIDs
|
||||
let childVarlist = BrowserDB.varlist(childArgs.count)
|
||||
|
||||
// Mirror the children if they're not already.
|
||||
// We use the non-folder version of this query because we're recursively
|
||||
// destroying structure right after this, so there's no point cloning the
|
||||
// mirror structure.
|
||||
// Then delete the children's children, so we don't leave orphans. This is
|
||||
// recursive, so by the time this succeeds we know that all of these records
|
||||
// have no remaining children.
|
||||
let (overrideSQL, overrideArgs) = self.getSQLToOverrideNonFolders(childGUIDs, atModifiedTime: Date.now())
|
||||
|
||||
return self.deleteChildrenOfGUIDs(childGUIDs)
|
||||
>>> { self.db.run(overrideSQL.map { ($0, overrideArgs) }) }
|
||||
>>> {
|
||||
// Delete the children themselves.
|
||||
|
||||
// Remove each child from structure. We use the top list to save effort.
|
||||
let deleteStructure =
|
||||
"DELETE FROM \(TableBookmarksLocalStructure) WHERE parent IN \(topVarlist)"
|
||||
|
||||
// If a bookmark is New, delete it outright.
|
||||
let deleteNew =
|
||||
"DELETE FROM \(TableBookmarksLocal) WHERE guid IN \(childVarlist) AND sync_status = \(SyncStatus.new.rawValue)"
|
||||
|
||||
// If a bookmark is Changed, mark it as deleted and bump its modified time.
|
||||
let markChanged = self.getMarkDeletedSQLWithWhereFragment("guid IN \(childVarlist)")
|
||||
|
||||
return self.db.run([
|
||||
(deleteStructure, topArgs),
|
||||
(deleteNew, childArgs),
|
||||
(markChanged, childArgs),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func getMarkDeletedSQLWithWhereFragment(_ whereFragment: String) -> String {
|
||||
let sql =
|
||||
"UPDATE \(TableBookmarksLocal) SET" +
|
||||
" date_added = NULL" +
|
||||
", is_deleted = 1" +
|
||||
", local_modified = \(Date.now())" +
|
||||
", bmkUri = NULL" +
|
||||
", feedUri = NULL" +
|
||||
", siteUri = NULL" +
|
||||
", pos = NULL" +
|
||||
", title = NULL" +
|
||||
", tags = NULL" +
|
||||
", keyword = NULL" +
|
||||
", description = NULL" +
|
||||
", parentid = NULL" +
|
||||
", parentName = NULL" +
|
||||
", folderName = NULL" +
|
||||
", queryId = NULL" +
|
||||
" WHERE \(whereFragment) AND sync_status = \(SyncStatus.changed.rawValue)"
|
||||
|
||||
return sql
|
||||
}
|
||||
/**
|
||||
* This depends on the record's parent already being overridden if necessary.
|
||||
*/
|
||||
fileprivate func removeLocalByGUID(_ guid: GUID) -> Success {
|
||||
let args: Args = [guid]
|
||||
|
||||
// Find the index we're currently occupying.
|
||||
let previousIndexSubquery = "SELECT idx FROM \(TableBookmarksLocalStructure) WHERE child = ?"
|
||||
|
||||
// Fix up the indices of subsequent siblings.
|
||||
let updateIndices =
|
||||
"UPDATE \(TableBookmarksLocalStructure) SET idx = (idx - 1) WHERE idx > (\(previousIndexSubquery))"
|
||||
|
||||
// If the bookmark is New, delete it outright.
|
||||
let deleteNew =
|
||||
"DELETE FROM \(TableBookmarksLocal) WHERE guid = ? AND sync_status = \(SyncStatus.new.rawValue)"
|
||||
|
||||
// If the bookmark is Changed, mark it as deleted and bump its modified time.
|
||||
let markChanged = self.getMarkDeletedSQLWithWhereFragment("guid = ?")
|
||||
|
||||
// Its parent must be either New or Changed, so we don't need to re-mirror it.
|
||||
// TODO: bump the parent's modified time, because the child list changed?
|
||||
|
||||
// Now delete from structure.
|
||||
let deleteStructure =
|
||||
"DELETE FROM \(TableBookmarksLocalStructure) WHERE child = ?"
|
||||
|
||||
return self.db.run([
|
||||
(updateIndices, args),
|
||||
(deleteNew, args),
|
||||
(markChanged, args),
|
||||
(deleteStructure, args),
|
||||
])
|
||||
}
|
||||
|
||||
fileprivate func markBufferBookmarkAsDeleted(_ guid: GUID) -> Success {
|
||||
let insertInPendingDeletions =
|
||||
"INSERT OR IGNORE INTO \(TablePendingBookmarksDeletions) " +
|
||||
"(id) " +
|
||||
"VALUES (?)"
|
||||
let args: Args = [guid]
|
||||
return self.db.run(insertInPendingDeletions, withArgs: args)
|
||||
}
|
||||
}
|
||||
|
||||
class SQLiteBookmarkFolder: BookmarkFolder {
|
||||
fileprivate let cursor: Cursor<BookmarkNode>
|
||||
override var count: Int {
|
||||
return cursor.count
|
||||
}
|
||||
|
||||
override subscript(index: Int) -> BookmarkNode {
|
||||
let bookmark = cursor[index]
|
||||
return bookmark! as BookmarkNode
|
||||
}
|
||||
|
||||
init(guid: String, title: String, children: Cursor<BookmarkNode>) {
|
||||
self.cursor = children
|
||||
super.init(guid: guid, title: title)
|
||||
}
|
||||
|
||||
override func removeItemWithGUID(_ guid: GUID) -> BookmarkFolder? {
|
||||
let without = cursor.asArray().filter { $0.guid != guid }
|
||||
return MemoryBookmarkFolder(guid: self.guid, title: self.title, children: without)
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkFactory {
|
||||
fileprivate class func addIcon(_ bookmark: BookmarkNode, row: SDRow) {
|
||||
// TODO: share this logic with SQLiteHistory.
|
||||
if let faviconURL = row["iconURL"] as? String,
|
||||
let date = row["iconDate"] as? Double,
|
||||
let faviconType = row["iconType"] as? Int,
|
||||
let type = IconType(rawValue: faviconType) {
|
||||
bookmark.favicon = Favicon(url: faviconURL,
|
||||
date: Date(timeIntervalSince1970: date),
|
||||
type: type)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class func livemarkFactory(_ row: SDRow) -> BookmarkItem {
|
||||
let id = row["id"] as? Int
|
||||
let guid = row["guid"] as! String
|
||||
let url = row["siteUri"] as! String
|
||||
let title = row["title"] as? String ?? "Livemark" // TODO
|
||||
let isEditable = row.getBoolean("isEditable") // Defaults to false.
|
||||
let bookmark = BookmarkItem(guid: guid, title: title, url: url, isEditable: isEditable)
|
||||
bookmark.id = id
|
||||
BookmarkFactory.addIcon(bookmark, row: row)
|
||||
return bookmark
|
||||
}
|
||||
|
||||
// We ignore queries altogether inside the model factory.
|
||||
fileprivate class func queryFactory(_ row: SDRow) -> BookmarkItem {
|
||||
log.warning("Creating a BookmarkItem from a query. This is almost certainly unexpected.")
|
||||
let id = row["id"] as? Int
|
||||
let guid = row["guid"] as! String
|
||||
let title = row["title"] as? String ?? SQLiteBookmarks.defaultItemTitle
|
||||
let isEditable = row.getBoolean("isEditable") // Defaults to false.
|
||||
let bookmark = BookmarkItem(guid: guid, title: title, url: "about:blank", isEditable: isEditable)
|
||||
bookmark.id = id
|
||||
BookmarkFactory.addIcon(bookmark, row: row)
|
||||
return bookmark
|
||||
}
|
||||
|
||||
fileprivate class func separatorFactory(_ row: SDRow) -> BookmarkSeparator {
|
||||
let id = row["id"] as? Int
|
||||
let guid = row["guid"] as! String
|
||||
let separator = BookmarkSeparator(guid: guid)
|
||||
separator.id = id
|
||||
return separator
|
||||
}
|
||||
|
||||
fileprivate class func itemRowFactory(_ row: SDRow, forceEditable: Bool = false) -> BookmarkItem {
|
||||
let id = row["id"] as? Int
|
||||
let guid = row["guid"] as! String
|
||||
let url = row["bmkUri"] as! String
|
||||
let title = row["title"] as? String ?? url
|
||||
let isEditable = forceEditable || row.getBoolean("isEditable") // Defaults to false.
|
||||
let bookmark = BookmarkItem(guid: guid, title: title, url: url, isEditable: isEditable)
|
||||
bookmark.id = id
|
||||
BookmarkFactory.addIcon(bookmark, row: row)
|
||||
return bookmark
|
||||
}
|
||||
|
||||
fileprivate class func itemFactory(_ row: SDRow) -> BookmarkItem {
|
||||
return BookmarkFactory.itemRowFactory(row, forceEditable: false)
|
||||
}
|
||||
|
||||
fileprivate class func folderFactory(_ row: SDRow) -> BookmarkFolder {
|
||||
let id = row["id"] as? Int
|
||||
let guid = row["guid"] as! String
|
||||
let isEditable = row.getBoolean("isEditable") // Defaults to false.
|
||||
let title = titleForSpecialGUID(guid) ??
|
||||
row["title"] as? String ??
|
||||
SQLiteBookmarks.defaultFolderTitle
|
||||
|
||||
let folder = BookmarkFolder(guid: guid, title: title, isEditable: isEditable)
|
||||
folder.id = id
|
||||
BookmarkFactory.addIcon(folder, row: row)
|
||||
return folder
|
||||
}
|
||||
|
||||
class func factory(_ row: SDRow) -> BookmarkNode {
|
||||
return BookmarkFactory.rowFactory(row, forceEditable: false)
|
||||
}
|
||||
|
||||
class func editableItemsFactory(_ row: SDRow) -> BookmarkNode {
|
||||
return BookmarkFactory.rowFactory(row, forceEditable: true)
|
||||
}
|
||||
|
||||
class func rowFactory(_ row: SDRow, forceEditable: Bool = false) -> BookmarkNode {
|
||||
if let typeCode = row["type"] as? Int, let type = BookmarkNodeType(rawValue: typeCode) {
|
||||
switch type {
|
||||
case .bookmark:
|
||||
return itemRowFactory(row, forceEditable: forceEditable)
|
||||
case .dynamicContainer:
|
||||
// This should never be hit: we exclude dynamic containers from our models.
|
||||
fallthrough
|
||||
case .folder:
|
||||
return folderFactory(row)
|
||||
case .separator:
|
||||
return separatorFactory(row)
|
||||
case .livemark:
|
||||
return livemarkFactory(row)
|
||||
case .query:
|
||||
// This should never be hit: we exclude queries from our models.
|
||||
return queryFactory(row)
|
||||
}
|
||||
}
|
||||
assert(false, "Invalid bookmark data.")
|
||||
return itemFactory(row) // This will fail, but it keeps the compiler happy.
|
||||
}
|
||||
|
||||
// N.B., doesn't include children!
|
||||
class func mirrorItemFactory(_ row: SDRow) -> BookmarkMirrorItem {
|
||||
// TODO
|
||||
// let id = row["id"] as! Int
|
||||
|
||||
let guid = row["guid"] as! GUID
|
||||
let typeCode = row["type"] as! Int
|
||||
let is_deleted = row.getBoolean("is_deleted")
|
||||
let parentid = row["parentid"] as? GUID
|
||||
let parentName = row["parentName"] as? String
|
||||
let feedUri = row["feedUri"] as? String
|
||||
let siteUri = row["siteUri"] as? String
|
||||
let pos = row["pos"] as? Int
|
||||
let title = row["title"] as? String
|
||||
let description = row["description"] as? String
|
||||
let bmkUri = row["bmkUri"] as? String
|
||||
let tags = row["tags"] as? String
|
||||
let keyword = row["keyword"] as? String
|
||||
let folderName = row["folderName"] as? String
|
||||
let queryId = row["queryId"] as? String
|
||||
let date_added = row.getTimestamp("date_added")
|
||||
|
||||
// Local and mirror only.
|
||||
let faviconID = row["faviconID"] as? Int
|
||||
|
||||
// Local only.
|
||||
let local_modified = row.getTimestamp("local_modified")
|
||||
|
||||
// Mirror and buffer.
|
||||
let server_modified = row.getTimestamp("server_modified")
|
||||
let hasDupe = row.getBoolean("hasDupe")
|
||||
|
||||
// Mirror only. TODO
|
||||
//let is_overridden = row.getBoolean("is_overridden")
|
||||
|
||||
// Use the struct initializer directly. Yes, this doesn't validate as strongly as
|
||||
// using the static constructors, but it'll be as valid as the contents of the DB.
|
||||
let type = BookmarkNodeType(rawValue: typeCode)!
|
||||
|
||||
// This one might really be missing (it's local-only), so do this the hard way.
|
||||
let syncStatus: SyncStatus?
|
||||
if let s = row["sync_status"] as? Int {
|
||||
syncStatus = SyncStatus(rawValue: s)
|
||||
} else {
|
||||
syncStatus = nil
|
||||
}
|
||||
let item = BookmarkMirrorItem(guid: guid, type: type, dateAdded: date_added, serverModified: server_modified ?? 0,
|
||||
isDeleted: is_deleted, hasDupe: hasDupe, parentID: parentid, parentName: parentName,
|
||||
feedURI: feedUri, siteURI: siteUri,
|
||||
pos: pos,
|
||||
title: title, description: description,
|
||||
bookmarkURI: bmkUri, tags: tags, keyword: keyword,
|
||||
folderName: folderName, queryID: queryId,
|
||||
children: nil,
|
||||
faviconID: faviconID, localModified: local_modified,
|
||||
syncStatus: syncStatus)
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLiteBookmarks: SearchableBookmarks {
|
||||
public func bookmarksByURL(_ url: URL) -> Deferred<Maybe<Cursor<BookmarkItem>>> {
|
||||
let inner =
|
||||
"SELECT id, type, date_added, guid, bmkUri, title, faviconID FROM \(TableBookmarksLocal) " +
|
||||
"WHERE " +
|
||||
"type = \(BookmarkNodeType.bookmark.rawValue) AND is_deleted IS NOT 1 AND bmkUri = ? " +
|
||||
"UNION ALL " +
|
||||
"SELECT id, type, date_added, guid, bmkUri, title, faviconID FROM \(TableBookmarksMirror) " +
|
||||
"WHERE " +
|
||||
"type = \(BookmarkNodeType.bookmark.rawValue) AND is_overridden IS NOT 1 AND is_deleted IS NOT 1 AND bmkUri = ? "
|
||||
|
||||
let sql =
|
||||
"SELECT bookmarks.id AS id, bookmarks.type AS type, bookmarks.date_added AS date_added, guid, bookmarks.bmkUri AS bmkUri, title, " +
|
||||
"favicons.url AS iconURL, favicons.date AS iconDate, favicons.type AS iconType " +
|
||||
"FROM (\(inner)) AS bookmarks " +
|
||||
"LEFT OUTER JOIN favicons ON bookmarks.faviconID = favicons.id"
|
||||
|
||||
let u = url.absoluteString
|
||||
let args: Args = [u, u]
|
||||
return db.runQuery(sql, args: args, factory: BookmarkFactory.itemFactory)
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLiteBookmarks {
|
||||
// We're in sync (even partially) if the mirror is non-empty.
|
||||
// We can show fallback desktop bookmarks if the mirror is empty and the buffer contains
|
||||
// children of the roots.
|
||||
func hasOnlyUnmergedRemoteBookmarks() -> Deferred<Maybe<Bool>> {
|
||||
let parents: Args = [
|
||||
BookmarkRoots.MenuFolderGUID,
|
||||
BookmarkRoots.ToolbarFolderGUID,
|
||||
BookmarkRoots.UnfiledFolderGUID,
|
||||
BookmarkRoots.MobileFolderGUID,
|
||||
]
|
||||
let sql = [
|
||||
"SELECT",
|
||||
"not exists(SELECT 1 FROM \(TableBookmarksMirror))",
|
||||
"AND",
|
||||
"exists(SELECT 1 FROM \(TableBookmarksBufferStructure) WHERE parent IN (?, ?, ?, ?))",
|
||||
].joined(separator: " ")
|
||||
return self.db.runQuery(sql, args: parents, factory: { $0[0] as! Int == 1 })
|
||||
>>== { row in
|
||||
guard row.status == .success,
|
||||
let result = row[0] else {
|
||||
// if the query did not succeed, we should return false so that we can use local bookmarks
|
||||
return deferMaybe(false)
|
||||
}
|
||||
return deferMaybe(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It's a factory where the root contains Desktop Bookmarks from the buffer, and
|
||||
// mobile bookmarks from local.
|
||||
open class UnsyncedBookmarksFallbackModelFactory: BookmarksModelFactory {
|
||||
let localFactory: SQLiteBookmarksModelFactory
|
||||
let bufferFactory: SQLiteBookmarksModelFactory
|
||||
|
||||
init(bookmarks: SQLiteBookmarks) {
|
||||
// This relies on SQLiteBookmarks being the storage for both directions.
|
||||
self.localFactory = SQLiteBookmarksModelFactory(bookmarks: bookmarks, direction: .local)
|
||||
if AppConstants.MOZ_SIMPLE_BOOKMARKS_SYNCING {
|
||||
self.bufferFactory = EditableBufferBookmarksSQLiteBookmarksModelFactory(bookmarks: bookmarks, direction: .buffer)
|
||||
} else {
|
||||
self.bufferFactory = SQLiteBookmarksModelFactory(bookmarks: bookmarks, direction: .buffer)
|
||||
}
|
||||
}
|
||||
|
||||
// This is a special-case class, so here's the special-case behavior to
|
||||
// know how to handle a folder that contains items drawn from different
|
||||
// parts of the database. We look for the special kinds of folders we
|
||||
// nest at the top level, and then we pick a folder to match.
|
||||
public func factoryForIndex(_ index: Int, inFolder folder: BookmarkFolder) -> BookmarksModelFactory {
|
||||
let concatenated: ConcatenatedBookmarkFolder
|
||||
let i: Int
|
||||
|
||||
// We have either just remote and local mobile bookmarks, or we have Desktop Bookmarks
|
||||
// followed by remote and local mobile bookmarks. Handle either.
|
||||
if let prepended = folder as? PrependedBookmarkFolder {
|
||||
if index == 0 {
|
||||
return self
|
||||
}
|
||||
|
||||
guard let c = prepended.main as? ConcatenatedBookmarkFolder else {
|
||||
return self
|
||||
}
|
||||
i = index - 1 // Drop the prepend.
|
||||
concatenated = c
|
||||
} else {
|
||||
guard let c = folder as? ConcatenatedBookmarkFolder else {
|
||||
return self
|
||||
}
|
||||
i = index
|
||||
concatenated = c
|
||||
}
|
||||
|
||||
if i < concatenated.pivot {
|
||||
return self.bufferFactory // This comes first in our concatenation.
|
||||
}
|
||||
return self.localFactory
|
||||
}
|
||||
|
||||
open func modelForFolder(_ folder: BookmarkFolder) -> Deferred<Maybe<BookmarksModel>> {
|
||||
return self.modelForFolder(folder.guid, title: folder.title)
|
||||
}
|
||||
|
||||
open func modelForFolder(_ guid: GUID) -> Deferred<Maybe<BookmarksModel>> {
|
||||
return self.modelForFolder(guid, title: "")
|
||||
}
|
||||
|
||||
open func modelForFolder(_ guid: GUID, title: String) -> Deferred<Maybe<BookmarksModel>> {
|
||||
if guid == BookmarkRoots.MobileFolderGUID {
|
||||
return self.modelForRoot()
|
||||
}
|
||||
|
||||
if guid == BookmarkRoots.FakeDesktopFolderGUID {
|
||||
return self.bufferFactory.modelForFolder(guid, title: title)
|
||||
}
|
||||
|
||||
return self.localFactory.modelForFolder(guid, title: title)
|
||||
}
|
||||
|
||||
open func modelForRoot() -> Deferred<Maybe<BookmarksModel>> {
|
||||
log.debug("Getting model for fallback root.")
|
||||
// Return a virtual model containing "Desktop bookmarks" prepended to the local mobile bookmarks.
|
||||
return self.localFactory.folderForGUID(BookmarkRoots.MobileFolderGUID, title: BookmarksFolderTitleMobile)
|
||||
>>== {
|
||||
localMobileFolder in
|
||||
|
||||
self.bufferFactory.folderForGUID(BookmarkRoots.MobileFolderGUID, title: BookmarksFolderTitleMobile) >>== {
|
||||
bufferMobileFolder in
|
||||
|
||||
let bufferAndLocalMobile = ConcatenatedBookmarkFolder(main: bufferMobileFolder, append: localMobileFolder)
|
||||
return self.bufferFactory.hasDesktopBookmarks() >>== { yes in
|
||||
guard yes else {
|
||||
return deferMaybe(BookmarksModel(modelFactory: self, root: bufferAndLocalMobile))
|
||||
}
|
||||
return self.bufferFactory.getDesktopRoots() >>== { cursor in
|
||||
let desktop = self.bufferFactory.folderForDesktopBookmarksCursor(cursor)
|
||||
let withDesktopPrepended = PrependedBookmarkFolder(main: bufferAndLocalMobile, prepend: desktop)
|
||||
return deferMaybe(BookmarksModel(modelFactory: self, root: withDesktopPrepended))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Whenever async construction is necessary, we fall into a pattern of needing
|
||||
// a placeholder that behaves correctly for the period between kickoff and set.
|
||||
open var nullModel: BookmarksModel {
|
||||
let children = Cursor<BookmarkNode>(status: .failure, msg: "Null model")
|
||||
let folder = SQLiteBookmarkFolder(guid: "Null", title: "Null", children: children)
|
||||
return BookmarksModel(modelFactory: self, root: folder)
|
||||
}
|
||||
|
||||
open func isBookmarked(_ url: String) -> Deferred<Maybe<Bool>> {
|
||||
// We don't include buffer items in this check, because we can't un-star them!
|
||||
return self.localFactory.isBookmarked(url)
|
||||
}
|
||||
|
||||
open func removeByGUID(_ guid: GUID) -> Success {
|
||||
return self.localFactory.removeByGUID(guid)
|
||||
}
|
||||
|
||||
open func removeByURL(_ url: String) -> Success {
|
||||
return self.localFactory.removeByURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
open class MergedSQLiteBookmarks: BookmarksModelFactorySource, KeywordSearchSource {
|
||||
let local: SQLiteBookmarks
|
||||
let buffer: SQLiteBookmarkBufferStorage
|
||||
|
||||
// Figuring out our factory can require hitting the DB, so this is async.
|
||||
// Note that we check *every time* -- we don't want to get stuck in a dead
|
||||
// end when you might sync soon.
|
||||
open var modelFactory: Deferred<Maybe<BookmarksModelFactory>> {
|
||||
return self.local.hasOnlyUnmergedRemoteBookmarks() >>== { yes in
|
||||
if yes {
|
||||
log.debug("Only unmerged remote bookmarks; using fallback factory.")
|
||||
return deferMaybe(UnsyncedBookmarksFallbackModelFactory(bookmarks: self.local))
|
||||
}
|
||||
log.debug("Using local+mirror bookmark factory.")
|
||||
return self.local.modelFactory
|
||||
}
|
||||
}
|
||||
|
||||
public init(db: BrowserDB) {
|
||||
self.local = SQLiteBookmarks(db: db)
|
||||
self.buffer = SQLiteBookmarkBufferStorage(db: db)
|
||||
}
|
||||
|
||||
open func getURLForKeywordSearch(_ keyword: String) -> Deferred<Maybe<String>> {
|
||||
return self.local.getURLForKeywordSearch(keyword)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue