diff options
Diffstat (limited to '')
-rw-r--r-- | browser/components/extensions/parent/ext-bookmarks.js | 515 |
1 files changed, 515 insertions, 0 deletions
diff --git a/browser/components/extensions/parent/ext-bookmarks.js b/browser/components/extensions/parent/ext-bookmarks.js new file mode 100644 index 0000000000..2c79f06322 --- /dev/null +++ b/browser/components/extensions/parent/ext-bookmarks.js @@ -0,0 +1,515 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +/* 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"; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +var { ExtensionError } = ExtensionUtils; + +const { TYPE_BOOKMARK, TYPE_FOLDER, TYPE_SEPARATOR } = PlacesUtils.bookmarks; + +const BOOKMARKS_TYPES_TO_API_TYPES_MAP = new Map([ + [TYPE_BOOKMARK, "bookmark"], + [TYPE_FOLDER, "folder"], + [TYPE_SEPARATOR, "separator"], +]); + +const BOOKMARK_SEPERATOR_URL = "data:"; + +XPCOMUtils.defineLazyGetter(this, "API_TYPES_TO_BOOKMARKS_TYPES_MAP", () => { + let theMap = new Map(); + + for (let [code, name] of BOOKMARKS_TYPES_TO_API_TYPES_MAP) { + theMap.set(name, code); + } + return theMap; +}); + +let listenerCount = 0; + +function getUrl(type, url) { + switch (type) { + case TYPE_BOOKMARK: + return url; + case TYPE_SEPARATOR: + return BOOKMARK_SEPERATOR_URL; + default: + return undefined; + } +} + +const getTree = (rootGuid, onlyChildren) => { + function convert(node, parent) { + let treenode = { + id: node.guid, + title: PlacesUtils.bookmarks.getLocalizedTitle(node) || "", + index: node.index, + dateAdded: node.dateAdded / 1000, + type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(node.typeCode), + url: getUrl(node.typeCode, node.uri), + }; + + if (parent && node.guid != PlacesUtils.bookmarks.rootGuid) { + treenode.parentId = parent.guid; + } + + if (node.typeCode == TYPE_FOLDER) { + treenode.dateGroupModified = node.lastModified / 1000; + + if (!onlyChildren) { + treenode.children = node.children + ? node.children.map(child => convert(child, node)) + : []; + } + } + + return treenode; + } + + return PlacesUtils.promiseBookmarksTree(rootGuid) + .then(root => { + if (onlyChildren) { + let children = root.children || []; + return children.map(child => convert(child, root)); + } + let treenode = convert(root, null); + treenode.parentId = root.parentGuid; + // It seems like the array always just contains the root node. + return [treenode]; + }) + .catch(e => Promise.reject({ message: e.message })); +}; + +const convertBookmarks = result => { + let node = { + id: result.guid, + title: PlacesUtils.bookmarks.getLocalizedTitle(result) || "", + index: result.index, + dateAdded: result.dateAdded.getTime(), + type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(result.type), + url: getUrl(result.type, result.url && result.url.href), + }; + + if (result.guid != PlacesUtils.bookmarks.rootGuid) { + node.parentId = result.parentGuid; + } + + if (result.type == TYPE_FOLDER) { + node.dateGroupModified = result.lastModified.getTime(); + } + + return node; +}; + +const throwIfRootId = id => { + if (id == PlacesUtils.bookmarks.rootGuid) { + throw new ExtensionError("The bookmark root cannot be modified"); + } +}; + +let observer = new (class extends EventEmitter { + constructor() { + super(); + this.handlePlacesEvents = this.handlePlacesEvents.bind(this); + } + + handlePlacesEvents(events) { + for (let event of events) { + switch (event.type) { + case "bookmark-added": + if (event.isTagging) { + continue; + } + let bookmark = { + id: event.guid, + parentId: event.parentGuid, + index: event.index, + title: event.title, + dateAdded: event.dateAdded, + type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(event.itemType), + url: getUrl(event.itemType, event.url), + }; + + if (event.itemType == TYPE_FOLDER) { + bookmark.dateGroupModified = bookmark.dateAdded; + } + + this.emit("created", bookmark); + break; + case "bookmark-removed": + if (event.isTagging || event.isDescendantRemoval) { + continue; + } + let node = { + id: event.guid, + parentId: event.parentGuid, + index: event.index, + type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(event.itemType), + url: getUrl(event.itemType, event.url), + title: event.title, + }; + + this.emit("removed", { + guid: event.guid, + info: { parentId: event.parentGuid, index: event.index, node }, + }); + break; + case "bookmark-moved": + this.emit("moved", { + guid: event.guid, + info: { + parentId: event.parentGuid, + index: event.index, + oldParentId: event.oldParentGuid, + oldIndex: event.oldIndex, + }, + }); + break; + case "bookmark-title-changed": + if (event.isTagging) { + continue; + } + + this.emit("changed", { + guid: event.guid, + info: { title: event.title }, + }); + break; + case "bookmark-url-changed": + if (event.isTagging) { + continue; + } + + this.emit("changed", { + guid: event.guid, + info: { url: event.url }, + }); + break; + } + } + } +})(); + +const decrementListeners = () => { + listenerCount -= 1; + if (!listenerCount) { + PlacesUtils.observers.removeListener( + [ + "bookmark-added", + "bookmark-removed", + "bookmark-moved", + "bookmark-title-changed", + "bookmark-url-changed", + ], + observer.handlePlacesEvents + ); + } +}; + +const incrementListeners = () => { + listenerCount++; + if (listenerCount == 1) { + PlacesUtils.observers.addListener( + [ + "bookmark-added", + "bookmark-removed", + "bookmark-moved", + "bookmark-title-changed", + "bookmark-url-changed", + ], + observer.handlePlacesEvents + ); + } +}; + +this.bookmarks = class extends ExtensionAPIPersistent { + PERSISTENT_EVENTS = { + onCreated({ fire }) { + let listener = (event, bookmark) => { + fire.sync(bookmark.id, bookmark); + }; + + observer.on("created", listener); + incrementListeners(); + return { + unregister() { + observer.off("created", listener); + decrementListeners(); + }, + convert(_fire) { + fire = _fire; + }, + }; + }, + + onRemoved({ fire }) { + let listener = (event, data) => { + fire.sync(data.guid, data.info); + }; + + observer.on("removed", listener); + incrementListeners(); + return { + unregister() { + observer.off("removed", listener); + decrementListeners(); + }, + convert(_fire) { + fire = _fire; + }, + }; + }, + + onChanged({ fire }) { + let listener = (event, data) => { + fire.sync(data.guid, data.info); + }; + + observer.on("changed", listener); + incrementListeners(); + return { + unregister() { + observer.off("changed", listener); + decrementListeners(); + }, + convert(_fire) { + fire = _fire; + }, + }; + }, + + onMoved({ fire }) { + let listener = (event, data) => { + fire.sync(data.guid, data.info); + }; + + observer.on("moved", listener); + incrementListeners(); + return { + unregister() { + observer.off("moved", listener); + decrementListeners(); + }, + convert(_fire) { + fire = _fire; + }, + }; + }, + }; + + getAPI(context) { + return { + bookmarks: { + async get(idOrIdList) { + let list = Array.isArray(idOrIdList) ? idOrIdList : [idOrIdList]; + + try { + let bookmarks = []; + for (let id of list) { + let bookmark = await PlacesUtils.bookmarks.fetch({ guid: id }); + if (!bookmark) { + throw new Error("Bookmark not found"); + } + bookmarks.push(convertBookmarks(bookmark)); + } + return bookmarks; + } catch (error) { + return Promise.reject({ message: error.message }); + } + }, + + getChildren: function(id) { + // TODO: We should optimize this. + return getTree(id, true); + }, + + getTree: function() { + return getTree(PlacesUtils.bookmarks.rootGuid, false); + }, + + getSubTree: function(id) { + return getTree(id, false); + }, + + search: function(query) { + return PlacesUtils.bookmarks + .search(query) + .then(result => result.map(convertBookmarks)); + }, + + getRecent: function(numberOfItems) { + return PlacesUtils.bookmarks + .getRecent(numberOfItems) + .then(result => result.map(convertBookmarks)); + }, + + create: function(bookmark) { + let info = { + title: bookmark.title || "", + }; + + info.type = API_TYPES_TO_BOOKMARKS_TYPES_MAP.get(bookmark.type); + if (!info.type) { + // If url is NULL or missing, it will be a folder. + if (bookmark.url !== null) { + info.type = TYPE_BOOKMARK; + } else { + info.type = TYPE_FOLDER; + } + } + + if (info.type === TYPE_BOOKMARK) { + info.url = bookmark.url || ""; + } + + if (bookmark.index !== null) { + info.index = bookmark.index; + } + + if (bookmark.parentId !== null) { + throwIfRootId(bookmark.parentId); + info.parentGuid = bookmark.parentId; + } else { + info.parentGuid = PlacesUtils.bookmarks.unfiledGuid; + } + + try { + return PlacesUtils.bookmarks + .insert(info) + .then(convertBookmarks) + .catch(error => Promise.reject({ message: error.message })); + } catch (e) { + return Promise.reject({ + message: `Invalid bookmark: ${JSON.stringify(info)}`, + }); + } + }, + + move: function(id, destination) { + throwIfRootId(id); + let info = { + guid: id, + }; + + if (destination.parentId !== null) { + throwIfRootId(destination.parentId); + info.parentGuid = destination.parentId; + } + info.index = + destination.index === null + ? PlacesUtils.bookmarks.DEFAULT_INDEX + : destination.index; + + try { + return PlacesUtils.bookmarks + .update(info) + .then(convertBookmarks) + .catch(error => Promise.reject({ message: error.message })); + } catch (e) { + return Promise.reject({ + message: `Invalid bookmark: ${JSON.stringify(info)}`, + }); + } + }, + + update: function(id, changes) { + throwIfRootId(id); + let info = { + guid: id, + }; + + if (changes.title !== null) { + info.title = changes.title; + } + if (changes.url !== null) { + info.url = changes.url; + } + + try { + return PlacesUtils.bookmarks + .update(info) + .then(convertBookmarks) + .catch(error => Promise.reject({ message: error.message })); + } catch (e) { + return Promise.reject({ + message: `Invalid bookmark: ${JSON.stringify(info)}`, + }); + } + }, + + remove: function(id) { + throwIfRootId(id); + let info = { + guid: id, + }; + + // The API doesn't give you the old bookmark at the moment + try { + return PlacesUtils.bookmarks + .remove(info, { preventRemovalOfNonEmptyFolders: true }) + .catch(error => Promise.reject({ message: error.message })); + } catch (e) { + return Promise.reject({ + message: `Invalid bookmark: ${JSON.stringify(info)}`, + }); + } + }, + + removeTree: function(id) { + throwIfRootId(id); + let info = { + guid: id, + }; + + try { + return PlacesUtils.bookmarks + .remove(info) + .catch(error => Promise.reject({ message: error.message })); + } catch (e) { + return Promise.reject({ + message: `Invalid bookmark: ${JSON.stringify(info)}`, + }); + } + }, + + onCreated: new EventManager({ + context, + module: "bookmarks", + event: "onCreated", + extensionApi: this, + }).api(), + + onRemoved: new EventManager({ + context, + module: "bookmarks", + event: "onRemoved", + extensionApi: this, + }).api(), + + onChanged: new EventManager({ + context, + module: "bookmarks", + event: "onChanged", + extensionApi: this, + }).api(), + + onMoved: new EventManager({ + context, + module: "bookmarks", + event: "onMoved", + extensionApi: this, + }).api(), + }, + }; + } +}; |