511 lines
13 KiB
JavaScript
511 lines
13 KiB
JavaScript
/* -*- 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";
|
|
|
|
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:";
|
|
|
|
ChromeUtils.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(),
|
|
},
|
|
};
|
|
}
|
|
};
|