/* 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/. */ "use strict"; this.EXPORTED_SYMBOLS = ["Bookmarks"]; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://services-common/utils.js"); Cu.import("resource://services-crypto/utils.js"); Cu.import("resource://gre/modules/PlacesUtils.jsm"); Cu.import("resource:///modules/PlacesUIUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/CloudSyncPlacesWrapper.jsm"); Cu.import("resource://gre/modules/CloudSyncEventSource.jsm"); Cu.import("resource://gre/modules/CloudSyncBookmarksFolderCache.jsm"); const ITEM_TYPES = [ "NULL", "BOOKMARK", "FOLDER", "SEPARATOR", "DYNAMIC_CONTAINER", // no longer used by Places, but this ID should not be used for future item types ]; const CS_UNKNOWN = 0x1; const CS_FOLDER = 0x1 << 1; const CS_SEPARATOR = 0x1 << 2; const CS_QUERY = 0x1 << 3; const CS_LIVEMARK = 0x1 << 4; const CS_BOOKMARK = 0x1 << 5; const EXCLUDE_BACKUP_ANNO = "places/excludeFromBackup"; const DATA_VERSION = 1; function asyncCallback(ctx, func, args) { function invoke() { func.apply(ctx, args); } CommonUtils.nextTick(invoke); } var Record = function (params) { this.id = params.guid; this.parent = params.parent || null; this.index = params.position; this.title = params.title; this.dateAdded = Math.floor(params.dateAdded/1000); this.lastModified = Math.floor(params.lastModified/1000); this.uri = params.url; let annos = params.annos || {}; Object.defineProperty(this, "annos", { get: function () { return annos; }, enumerable: false }); switch (params.type) { case PlacesUtils.bookmarks.TYPE_FOLDER: if (PlacesUtils.LMANNO_FEEDURI in annos) { this.type = CS_LIVEMARK; this.feed = annos[PlacesUtils.LMANNO_FEEDURI]; this.site = annos[PlacesUtils.LMANNO_SITEURI]; } else { this.type = CS_FOLDER; } break; case PlacesUtils.bookmarks.TYPE_BOOKMARK: if (this.uri.startsWith("place:")) { this.type = CS_QUERY; } else { this.type = CS_BOOKMARK; } break; case PlacesUtils.bookmarks.TYPE_SEPARATOR: this.type = CS_SEPARATOR; break; default: this.type = CS_UNKNOWN; } }; Record.prototype = { version: DATA_VERSION, }; var Bookmarks = function () { let createRootFolder = function (name) { let ROOT_FOLDER_ANNO = "cloudsync/rootFolder/" + name; let ROOT_SHORTCUT_ANNO = "cloudsync/rootShortcut/" + name; let deferred = Promise.defer(); let placesRootId = PlacesUtils.placesRootId; let rootFolderId; let rootShortcutId; function createAdapterShortcut(result) { rootFolderId = result; let uri = "place:folder=" + rootFolderId; return PlacesWrapper.insertBookmark(PlacesUIUtils.allBookmarksFolderId, uri, PlacesUtils.bookmarks.DEFAULT_INDEX, name); } function setRootFolderCloudSyncAnnotation(result) { rootShortcutId = result; return PlacesWrapper.setItemAnnotation(rootFolderId, ROOT_FOLDER_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER); } function setRootShortcutCloudSyncAnnotation() { return PlacesWrapper.setItemAnnotation(rootShortcutId, ROOT_SHORTCUT_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER); } function setRootFolderExcludeFromBackupAnnotation() { return PlacesWrapper.setItemAnnotation(rootFolderId, EXCLUDE_BACKUP_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER); } function finish() { deferred.resolve(rootFolderId); } Promise.resolve(PlacesUtils.bookmarks.createFolder(placesRootId, name, PlacesUtils.bookmarks.DEFAULT_INDEX)) .then(createAdapterShortcut) .then(setRootFolderCloudSyncAnnotation) .then(setRootShortcutCloudSyncAnnotation) .then(setRootFolderExcludeFromBackupAnnotation) .then(finish, deferred.reject); return deferred.promise; }; let getRootFolder = function (name) { let ROOT_FOLDER_ANNO = "cloudsync/rootFolder/" + name; let ROOT_SHORTCUT_ANNO = "cloudsync/rootShortcut/" + name; let deferred = Promise.defer(); function checkRootFolder(folderIds) { if (!folderIds.length) { return createRootFolder(name); } return Promise.resolve(folderIds[0]); } function createFolderObject(folderId) { return new RootFolder(folderId, name); } PlacesWrapper.getLocalIdsWithAnnotation(ROOT_FOLDER_ANNO) .then(checkRootFolder, deferred.reject) .then(createFolderObject) .then(deferred.resolve, deferred.reject); return deferred.promise; }; let deleteRootFolder = function (name) { let ROOT_FOLDER_ANNO = "cloudsync/rootFolder/" + name; let ROOT_SHORTCUT_ANNO = "cloudsync/rootShortcut/" + name; let deferred = Promise.defer(); let placesRootId = PlacesUtils.placesRootId; function getRootShortcutId() { return PlacesWrapper.getLocalIdsWithAnnotation(ROOT_SHORTCUT_ANNO); } function deleteShortcut(shortcutIds) { if (!shortcutIds.length) { return Promise.resolve(); } return PlacesWrapper.removeItem(shortcutIds[0]); } function getRootFolderId() { return PlacesWrapper.getLocalIdsWithAnnotation(ROOT_FOLDER_ANNO); } function deleteFolder(folderIds) { let deleteFolderDeferred = Promise.defer(); if (!folderIds.length) { return Promise.resolve(); } let rootFolderId = folderIds[0]; PlacesWrapper.removeFolderChildren(rootFolderId).then( function () { return PlacesWrapper.removeItem(rootFolderId); } ).then(deleteFolderDeferred.resolve, deleteFolderDeferred.reject); return deleteFolderDeferred.promise; } getRootShortcutId().then(deleteShortcut) .then(getRootFolderId) .then(deleteFolder) .then(deferred.resolve, deferred.reject); return deferred.promise; }; /* PUBLIC API */ this.getRootFolder = getRootFolder.bind(this); this.deleteRootFolder = deleteRootFolder.bind(this); }; this.Bookmarks = Bookmarks; var RootFolder = function (rootId, rootName) { let suspended = true; let ignoreAll = false; let suspend = function () { if (!suspended) { PlacesUtils.bookmarks.removeObserver(observer); suspended = true; } }.bind(this); let resume = function () { if (suspended) { PlacesUtils.bookmarks.addObserver(observer, false); suspended = false; } }.bind(this); let eventTypes = [ "add", "remove", "change", "move", ]; let eventSource = new EventSource(eventTypes, suspend, resume); let folderCache = new FolderCache; folderCache.insert(rootId, null); let getCachedFolderIds = function (cache, roots) { let nodes = [...roots]; let results = []; while (nodes.length) { let node = nodes.shift(); results.push(node); let children = cache.getChildren(node); nodes = nodes.concat([...children]); } return results; }; let getLocalItems = function () { let deferred = Promise.defer(); let folders = getCachedFolderIds(folderCache, folderCache.getChildren(rootId)); function getFolders(ids) { let types = [ PlacesUtils.bookmarks.TYPE_FOLDER, ]; return PlacesWrapper.getItemsById(ids, types); } function getContents(parents) { parents.push(rootId); let types = [ PlacesUtils.bookmarks.TYPE_BOOKMARK, PlacesUtils.bookmarks.TYPE_SEPARATOR, ]; return PlacesWrapper.getItemsByParentId(parents, types) } function getParentGuids(results) { results = Array.prototype.concat.apply([], results); let promises = []; results.map(function (result) { let promise = PlacesWrapper.localIdToGuid(result.parent).then( function (guidResult) { result.parent = guidResult; return Promise.resolve(result); }, Promise.reject.bind(Promise) ); promises.push(promise); }); return Promise.all(promises); } function getAnnos(results) { results = Array.prototype.concat.apply([], results); let promises = []; results.map(function (result) { let promise = PlacesWrapper.getItemAnnotationsForLocalId(result.id).then( function (annos) { result.annos = annos; return Promise.resolve(result); }, Promise.reject.bind(Promise) ); promises.push(promise); }); return Promise.all(promises); } let promises = [ getFolders(folders), getContents(folders), ]; Promise.all(promises) .then(getParentGuids) .then(getAnnos) .then(function (results) { results = results.map((result) => new Record(result)); deferred.resolve(results); }, deferred.reject); return deferred.promise; }; let getLocalItemsById = function (guids) { let deferred = Promise.defer(); let types = [ PlacesUtils.bookmarks.TYPE_BOOKMARK, PlacesUtils.bookmarks.TYPE_FOLDER, PlacesUtils.bookmarks.TYPE_SEPARATOR, PlacesUtils.bookmarks.TYPE_DYNAMIC_CONTAINER, ]; function getParentGuids(results) { let promises = []; results.map(function (result) { let promise = PlacesWrapper.localIdToGuid(result.parent).then( function (guidResult) { result.parent = guidResult; return Promise.resolve(result); }, Promise.reject.bind(Promise) ); promises.push(promise); }); return Promise.all(promises); } PlacesWrapper.getItemsByGuid(guids, types) .then(getParentGuids) .then(function (results) { results = results.map((result) => new Record(result)); deferred.resolve(results); }, deferred.reject); return deferred.promise; }; let _createItem = function (item) { let deferred = Promise.defer(); function getFolderId() { if (item.parent) { return PlacesWrapper.guidToLocalId(item.parent); } return Promise.resolve(rootId); } function create(folderId) { let deferred = Promise.defer(); if (!folderId) { folderId = rootId; } let index = item.hasOwnProperty("index") ? item.index : PlacesUtils.bookmarks.DEFAULT_INDEX; function complete(localId) { folderCache.insert(localId, folderId); deferred.resolve(localId); } switch (item.type) { case CS_BOOKMARK: case CS_QUERY: PlacesWrapper.insertBookmark(folderId, item.uri, index, item.title, item.id) .then(complete, deferred.reject); break; case CS_FOLDER: PlacesWrapper.createFolder(folderId, item.title, index, item.id) .then(complete, deferred.reject); break; case CS_SEPARATOR: PlacesWrapper.insertSeparator(folderId, index, item.id) .then(complete, deferred.reject); break; case CS_LIVEMARK: let livemark = { title: item.title, parentId: folderId, index: item.index, feedURI: item.feed, siteURI: item.site, guid: item.id, }; PlacesUtils.livemarks.addLivemark(livemark) .then(complete, deferred.reject); break; default: deferred.reject("invalid item type: " + item.type); } return deferred.promise; } getFolderId().then(create) .then(deferred.resolve, deferred.reject); return deferred.promise; }; let _deleteItem = function (item) { let deferred = Promise.defer(); PlacesWrapper.guidToLocalId(item.id).then( function (localId) { folderCache.remove(localId); return PlacesWrapper.removeItem(localId); } ).then(deferred.resolve, deferred.reject); return deferred.promise; }; let _updateItem = function (item) { let deferred = Promise.defer(); PlacesWrapper.guidToLocalId(item.id).then( function (localId) { let promises = []; if (item.hasOwnProperty("dateAdded")) { promises.push(PlacesWrapper.setItemDateAdded(localId, item.dateAdded)); } if (item.hasOwnProperty("lastModified")) { promises.push(PlacesWrapper.setItemLastModified(localId, item.lastModified)); } if ((CS_BOOKMARK | CS_FOLDER) & item.type && item.hasOwnProperty("title")) { promises.push(PlacesWrapper.setItemTitle(localId, item.title)); } if (CS_BOOKMARK & item.type && item.hasOwnProperty("uri")) { promises.push(PlacesWrapper.changeBookmarkURI(localId, item.uri)); } if (item.hasOwnProperty("parent")) { let deferred = Promise.defer(); PlacesWrapper.guidToLocalId(item.parent) .then( function (parent) { let index = item.hasOwnProperty("index") ? item.index : PlacesUtils.bookmarks.DEFAULT_INDEX; if (CS_FOLDER & item.type) { folderCache.setParent(localId, parent); } return PlacesWrapper.moveItem(localId, parent, index); } ) .then(deferred.resolve, deferred.reject); promises.push(deferred.promise); } if (item.hasOwnProperty("index") && !item.hasOwnProperty("parent")) { promises.push(Task.spawn(function* () { let localItem = (yield getLocalItemsById([item.id]))[0]; let parent = yield PlacesWrapper.guidToLocalId(localItem.parent); let index = item.index; if (CS_FOLDER & item.type) { folderCache.setParent(localId, parent); } yield PlacesWrapper.moveItem(localId, parent, index); })); } Promise.all(promises) .then(deferred.resolve, deferred.reject); } ); return deferred.promise; }; let mergeRemoteItems = function (items) { ignoreAll = true; let deferred = Promise.defer(); let newFolders = {}; let newItems = []; let updatedItems = []; let deletedItems = []; let sortItems = function () { let promises = []; let exists = function (item) { let existsDeferred = Promise.defer(); if (!item.id) { Object.defineProperty(item, "__exists__", { value: false, enumerable: false }); existsDeferred.resolve(item); } else { PlacesWrapper.guidToLocalId(item.id).then( function (localId) { Object.defineProperty(item, "__exists__", { value: localId ? true : false, enumerable: false }); existsDeferred.resolve(item); }, existsDeferred.reject ); } return existsDeferred.promise; } let handleSortedItem = function (item) { if (!item.__exists__ && !item.deleted) { if (CS_FOLDER == item.type) { newFolders[item.id] = item; item._children = []; } else { newItems.push(item); } } else if (item.__exists__ && item.deleted) { deletedItems.push(item); } else if (item.__exists__) { updatedItems.push(item); } } for (let item of items) { if (!item || 'object' !== typeof(item)) { continue; } let promise = exists(item).then(handleSortedItem, Promise.reject.bind(Promise)); promises.push(promise); } return Promise.all(promises); } let processNewFolders = function () { let newFolderGuids = Object.keys(newFolders); let newFolderRoots = []; for (let guid of newFolderGuids) { let item = newFolders[guid]; if (item.parent && newFolderGuids.indexOf(item.parent) >= 0) { let parent = newFolders[item.parent]; parent._children.push(item.id); } else { newFolderRoots.push(guid); } }; let promises = []; for (let guid of newFolderRoots) { let root = newFolders[guid]; let promise = Promise.resolve(); promise = promise.then( function () { return _createItem(root); }, Promise.reject.bind(Promise) ); let items = [].concat(root._children); while (items.length) { let item = newFolders[items.shift()]; items = items.concat(item._children); promise = promise.then( function () { return _createItem(item); }, Promise.reject.bind(Promise) ); } promises.push(promise); } return Promise.all(promises); } let processItems = function () { let promises = []; for (let item of newItems) { promises.push(_createItem(item)); } for (let item of updatedItems) { promises.push(_updateItem(item)); } for (let item of deletedItems) { _deleteItem(item); } return Promise.all(promises); } sortItems().then(processNewFolders) .then(processItems) .then(function () { ignoreAll = false; deferred.resolve(items); }, function (err) { ignoreAll = false; deferred.reject(err); }); return deferred.promise; }; let ignore = function (id, parent) { if (ignoreAll) { return true; } if (rootId == parent || folderCache.has(parent)) { return false; } return true; }; let handleItemAdded = function (id, parent, index, type, uri, title, dateAdded, guid, parentGuid) { let deferred = Promise.defer(); if (PlacesUtils.bookmarks.TYPE_FOLDER == type) { folderCache.insert(id, parent); } eventSource.emit("add", guid); deferred.resolve(); return deferred.promise; }; let handleItemRemoved = function (id, parent, index, type, uri, guid, parentGuid) { let deferred = Promise.defer(); if (PlacesUtils.bookmarks.TYPE_FOLDER == type) { folderCache.remove(id); } eventSource.emit("remove", guid); deferred.resolve(); return deferred.promise; }; let handleItemChanged = function (id, property, isAnnotation, newValue, lastModified, type, parent, guid, parentGuid) { let deferred = Promise.defer(); eventSource.emit('change', guid); deferred.resolve(); return deferred.promise; }; let handleItemMoved = function (id, oldParent, oldIndex, newParent, newIndex, type, guid, oldParentGuid, newParentGuid) { let deferred = Promise.defer(); function complete() { eventSource.emit('move', guid); deferred.resolve(); } if (PlacesUtils.bookmarks.TYPE_FOLDER != type) { complete(); return deferred.promise; } if (folderCache.has(oldParent) && folderCache.has(newParent)) { // Folder move inside cloudSync root, so just update parents/children. folderCache.setParent(id, newParent); complete(); } else if (!folderCache.has(oldParent)) { // Folder moved in from ouside cloudSync root. PlacesWrapper.updateCachedFolderIds(folderCache, newParent) .then(complete, complete); } else if (!folderCache.has(newParent)) { // Folder moved out from inside cloudSync root. PlacesWrapper.updateCachedFolderIds(folderCache, oldParent) .then(complete, complete); } return deferred.promise; }; let observer = { onBeginBatchUpdate: function () { }, onEndBatchUpdate: function () { }, onItemAdded: function (id, parent, index, type, uri, title, dateAdded, guid, parentGuid) { if (ignore(id, parent)) { return; } asyncCallback(this, handleItemAdded, Array.prototype.slice.call(arguments)); }, onItemRemoved: function (id, parent, index, type, uri, guid, parentGuid) { if (ignore(id, parent)) { return; } asyncCallback(this, handleItemRemoved, Array.prototype.slice.call(arguments)); }, onItemChanged: function (id, property, isAnnotation, newValue, lastModified, type, parent, guid, parentGuid) { if (ignore(id, parent)) { return; } asyncCallback(this, handleItemChanged, Array.prototype.slice.call(arguments)); }, onItemMoved: function (id, oldParent, oldIndex, newParent, newIndex, type, guid, oldParentGuid, newParentGuid) { if (ignore(id, oldParent) && ignore(id, newParent)) { return; } asyncCallback(this, handleItemMoved, Array.prototype.slice.call(arguments)); } }; /* PUBLIC API */ this.addEventListener = eventSource.addEventListener; this.removeEventListener = eventSource.removeEventListener; this.getLocalItems = getLocalItems.bind(this); this.getLocalItemsById = getLocalItemsById.bind(this); this.mergeRemoteItems = mergeRemoteItems.bind(this); let rootGuid = null; // resolved before becoming ready (below) this.__defineGetter__("id", function () { return rootGuid; }); this.__defineGetter__("name", function () { return rootName; }); let deferred = Promise.defer(); let getGuidForRootFolder = function () { return PlacesWrapper.localIdToGuid(rootId); } PlacesWrapper.updateCachedFolderIds(folderCache, rootId) .then(getGuidForRootFolder, getGuidForRootFolder) .then(function (guid) { rootGuid = guid; deferred.resolve(this); }.bind(this), deferred.reject); return deferred.promise; }; RootFolder.prototype = { BOOKMARK: CS_BOOKMARK, FOLDER: CS_FOLDER, SEPARATOR: CS_SEPARATOR, QUERY: CS_QUERY, LIVEMARK: CS_LIVEMARK, };