summaryrefslogtreecommitdiffstats
path: root/browser/components/extensions/parent/ext-bookmarks.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/extensions/parent/ext-bookmarks.js')
-rw-r--r--browser/components/extensions/parent/ext-bookmarks.js515
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..6dbb41dc17
--- /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(),
+ },
+ };
+ }
+};