summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/places/host
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/jetpack/sdk/places/host')
-rw-r--r--toolkit/jetpack/sdk/places/host/host-bookmarks.js238
-rw-r--r--toolkit/jetpack/sdk/places/host/host-query.js179
-rw-r--r--toolkit/jetpack/sdk/places/host/host-tags.js92
3 files changed, 509 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/places/host/host-bookmarks.js b/toolkit/jetpack/sdk/places/host/host-bookmarks.js
new file mode 100644
index 000000000..3245c4070
--- /dev/null
+++ b/toolkit/jetpack/sdk/places/host/host-bookmarks.js
@@ -0,0 +1,238 @@
+/* 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";
+
+module.metadata = {
+ "stability": "experimental",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+const { Cc, Ci } = require('chrome');
+const browserHistory = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsIBrowserHistory);
+const asyncHistory = Cc["@mozilla.org/browser/history;1"].
+ getService(Ci.mozIAsyncHistory);
+const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+const taggingService = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+const ios = Cc['@mozilla.org/network/io-service;1'].
+ getService(Ci.nsIIOService);
+const { query } = require('./host-query');
+const {
+ defer, all, resolve, promised, reject
+} = require('../../core/promise');
+const { request, response } = require('../../addon/host');
+const { send } = require('../../addon/events');
+const { on, emit } = require('../../event/core');
+const { filter } = require('../../event/utils');
+const { URL, isValidURI } = require('../../url');
+const { newURI } = require('../../url/utils');
+
+const DEFAULT_INDEX = bmsrv.DEFAULT_INDEX;
+const UNSORTED_ID = bmsrv.unfiledBookmarksFolder;
+const ROOT_FOLDERS = [
+ bmsrv.unfiledBookmarksFolder, bmsrv.toolbarFolder,
+ bmsrv.tagsFolder, bmsrv.bookmarksMenuFolder
+];
+
+const EVENT_MAP = {
+ 'sdk-places-bookmarks-create': createBookmarkItem,
+ 'sdk-places-bookmarks-save': saveBookmarkItem,
+ 'sdk-places-bookmarks-last-updated': getBookmarkLastUpdated,
+ 'sdk-places-bookmarks-get': getBookmarkItem,
+ 'sdk-places-bookmarks-remove': removeBookmarkItem,
+ 'sdk-places-bookmarks-get-all': getAllBookmarks,
+ 'sdk-places-bookmarks-get-children': getChildren
+};
+
+function typeMap (type) {
+ if (typeof type === 'number') {
+ if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark';
+ if (bmsrv.TYPE_FOLDER === type) return 'group';
+ if (bmsrv.TYPE_SEPARATOR === type) return 'separator';
+ } else {
+ if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK;
+ if ('group' === type) return bmsrv.TYPE_FOLDER;
+ if ('separator' === type) return bmsrv.TYPE_SEPARATOR;
+ }
+}
+
+function getBookmarkLastUpdated ({id}) {
+ return resolve(bmsrv.getItemLastModified(id));
+}
+exports.getBookmarkLastUpdated;
+
+function createBookmarkItem (data) {
+ let error;
+
+ if (data.group == null) data.group = UNSORTED_ID;
+ if (data.index == null) data.index = DEFAULT_INDEX;
+
+ if (data.type === 'group')
+ data.id = bmsrv.createFolder(
+ data.group, data.title, data.index
+ );
+ else if (data.type === 'separator')
+ data.id = bmsrv.insertSeparator(
+ data.group, data.index
+ );
+ else
+ data.id = bmsrv.insertBookmark(
+ data.group, newURI(data.url), data.index, data.title
+ );
+
+ // In the event where default or no index is provided (-1),
+ // query the actual index for the response
+ if (data.index === -1)
+ data.index = bmsrv.getItemIndex(data.id);
+
+ try {
+ data.updated = bmsrv.getItemLastModified(data.id);
+ }
+ catch (e) {
+ console.exception(e);
+ }
+
+ return tag(data, true).then(() => data);
+}
+exports.createBookmarkItem = createBookmarkItem;
+
+function saveBookmarkItem (data) {
+ let id = data.id;
+ if (!id)
+ reject('Item is missing id');
+
+ let group = bmsrv.getFolderIdForItem(id);
+ let index = bmsrv.getItemIndex(id);
+ let type = bmsrv.getItemType(id);
+ let title = typeMap(type) !== 'separator' ?
+ bmsrv.getItemTitle(id) :
+ undefined;
+ let url = typeMap(type) === 'bookmark' ?
+ bmsrv.getBookmarkURI(id).spec :
+ undefined;
+
+ if (url != data.url)
+ bmsrv.changeBookmarkURI(id, newURI(data.url));
+ else if (typeMap(type) === 'bookmark')
+ data.url = url;
+
+ if (title != data.title)
+ bmsrv.setItemTitle(id, data.title);
+ else if (typeMap(type) !== 'separator')
+ data.title = title;
+
+ if (data.group && data.group !== group)
+ bmsrv.moveItem(id, data.group, data.index || -1);
+ else if (data.index != null && data.index !== index) {
+ // We use moveItem here instead of setItemIndex
+ // so we don't have to manage the indicies of the siblings
+ bmsrv.moveItem(id, group, data.index);
+ } else if (data.index == null)
+ data.index = index;
+
+ data.updated = bmsrv.getItemLastModified(data.id);
+
+ return tag(data).then(() => data);
+}
+exports.saveBookmarkItem = saveBookmarkItem;
+
+function removeBookmarkItem (data) {
+ let id = data.id;
+
+ if (!id)
+ reject('Item is missing id');
+
+ bmsrv.removeItem(id);
+ return resolve(null);
+}
+exports.removeBookmarkItem = removeBookmarkItem;
+
+function getBookmarkItem (data) {
+ let id = data.id;
+
+ if (!id)
+ reject('Item is missing id');
+
+ let type = bmsrv.getItemType(id);
+
+ data.type = typeMap(type);
+
+ if (type === bmsrv.TYPE_BOOKMARK || type === bmsrv.TYPE_FOLDER)
+ data.title = bmsrv.getItemTitle(id);
+
+ if (type === bmsrv.TYPE_BOOKMARK) {
+ data.url = bmsrv.getBookmarkURI(id).spec;
+ // Should be moved into host-tags as a method
+ data.tags = taggingService.getTagsForURI(newURI(data.url), {});
+ }
+
+ data.group = bmsrv.getFolderIdForItem(id);
+ data.index = bmsrv.getItemIndex(id);
+ data.updated = bmsrv.getItemLastModified(data.id);
+
+ return resolve(data);
+}
+exports.getBookmarkItem = getBookmarkItem;
+
+function getAllBookmarks () {
+ return query({}, { queryType: 1 }).then(bookmarks =>
+ all(bookmarks.map(getBookmarkItem)));
+}
+exports.getAllBookmarks = getAllBookmarks;
+
+function getChildren ({ id }) {
+ if (typeMap(bmsrv.getItemType(id)) !== 'group') return [];
+ let ids = [];
+ for (let i = 0; ids[ids.length - 1] !== -1; i++)
+ ids.push(bmsrv.getIdForItemAt(id, i));
+ ids.pop();
+ return all(ids.map(id => getBookmarkItem({ id: id })));
+}
+exports.getChildren = getChildren;
+
+/*
+ * Hook into host
+ */
+
+var reqStream = filter(request, (data) => /sdk-places-bookmarks/.test(data.event));
+on(reqStream, 'data', ({ event, id, data }) => {
+ if (!EVENT_MAP[event]) return;
+
+ let resData = { id: id, event: event };
+
+ promised(EVENT_MAP[event])(data).
+ then(res => resData.data = res, e => resData.error = e).
+ then(() => emit(response, 'data', resData));
+});
+
+function tag (data, isNew) {
+ // If a new item, we can skip checking what other tags
+ // are on the item
+ if (data.type !== 'bookmark') {
+ return resolve();
+ }
+ else if (!isNew) {
+ return send('sdk-places-tags-get-tags-by-url', { url: data.url })
+ .then(tags => {
+ return send('sdk-places-tags-untag', {
+ tags: tags.filter(tag => !~data.tags.indexOf(tag)),
+ url: data.url
+ });
+ }).then(() => send('sdk-places-tags-tag', {
+ url: data.url, tags: data.tags
+ }));
+ }
+ else if (data.tags && data.tags.length) {
+ return send('sdk-places-tags-tag', { url: data.url, tags: data.tags });
+ }
+ else
+ return resolve();
+}
+
diff --git a/toolkit/jetpack/sdk/places/host/host-query.js b/toolkit/jetpack/sdk/places/host/host-query.js
new file mode 100644
index 000000000..f2dbd6550
--- /dev/null
+++ b/toolkit/jetpack/sdk/places/host/host-query.js
@@ -0,0 +1,179 @@
+/* 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";
+
+module.metadata = {
+ "stability": "experimental",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+const { Cc, Ci } = require('chrome');
+const { all } = require('../../core/promise');
+const { safeMerge, omit } = require('../../util/object');
+const historyService = Cc['@mozilla.org/browser/nav-history-service;1']
+ .getService(Ci.nsINavHistoryService);
+const bookmarksService = Cc['@mozilla.org/browser/nav-bookmarks-service;1']
+ .getService(Ci.nsINavBookmarksService);
+const { request, response } = require('../../addon/host');
+const { newURI } = require('../../url/utils');
+const { send } = require('../../addon/events');
+const { on, emit } = require('../../event/core');
+const { filter } = require('../../event/utils');
+
+const ROOT_FOLDERS = [
+ bookmarksService.unfiledBookmarksFolder, bookmarksService.toolbarFolder,
+ bookmarksService.bookmarksMenuFolder
+];
+
+const EVENT_MAP = {
+ 'sdk-places-query': queryReceiver
+};
+
+// Properties that need to be manually
+// copied into a nsINavHistoryQuery object
+const MANUAL_QUERY_PROPERTIES = [
+ 'uri', 'folder', 'tags', 'url', 'folder'
+];
+
+const PLACES_PROPERTIES = [
+ 'uri', 'title', 'accessCount', 'time'
+];
+
+function execute (queries, options) {
+ return new Promise(resolve => {
+ let root = historyService
+ .executeQueries(queries, queries.length, options).root;
+ // Let's extract an eventual uri wildcard, if both domain and uri are set.
+ // See utils.js::urlQueryParser() for more details.
+ // In case of multiple queries, we only retain the first found wildcard.
+ let uriWildcard = queries.reduce((prev, query) => {
+ if (query.uri && query.domain) {
+ if (!prev)
+ prev = query.uri.spec;
+ query.uri = null;
+ }
+ return prev;
+ }, "");
+ resolve(collect([], root, uriWildcard));
+ });
+}
+
+function collect (acc, node, uriWildcard) {
+ node.containerOpen = true;
+ for (let i = 0; i < node.childCount; i++) {
+ let child = node.getChild(i);
+
+ if (!uriWildcard || child.uri.startsWith(uriWildcard)) {
+ acc.push(child);
+ }
+ if (child.type === child.RESULT_TYPE_FOLDER) {
+ let container = child.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ collect(acc, container, uriWildcard);
+ }
+ }
+ node.containerOpen = false;
+ return acc;
+}
+
+function query (queries, options) {
+ return new Promise((resolve, reject) => {
+ queries = queries || [];
+ options = options || {};
+ let optionsObj, queryObjs;
+
+ optionsObj = historyService.getNewQueryOptions();
+ queryObjs = [].concat(queries).map(createQuery);
+ if (!queryObjs.length) {
+ queryObjs = [historyService.getNewQuery()];
+ }
+ safeMerge(optionsObj, options);
+
+ /*
+ * Currently `places:` queries are not supported
+ */
+ optionsObj.excludeQueries = true;
+
+ execute(queryObjs, optionsObj).then((results) => {
+ if (optionsObj.queryType === 0) {
+ return results.map(normalize);
+ }
+ else if (optionsObj.queryType === 1) {
+ // Formats query results into more standard
+ // data structures for returning
+ return all(results.map(({itemId}) =>
+ send('sdk-places-bookmarks-get', { id: itemId })));
+ }
+ }).then(resolve, reject);
+ });
+}
+exports.query = query;
+
+function createQuery (query) {
+ query = query || {};
+ let queryObj = historyService.getNewQuery();
+
+ safeMerge(queryObj, omit(query, MANUAL_QUERY_PROPERTIES));
+
+ if (query.tags && Array.isArray(query.tags))
+ queryObj.tags = query.tags;
+ if (query.uri || query.url)
+ queryObj.uri = newURI(query.uri || query.url);
+ if (query.folder)
+ queryObj.setFolders([query.folder], 1);
+ return queryObj;
+}
+
+function queryReceiver (message) {
+ let queries = message.data.queries || message.data.query;
+ let options = message.data.options;
+ let resData = {
+ id: message.id,
+ event: message.event
+ };
+
+ query(queries, options).then(results => {
+ resData.data = results;
+ respond(resData);
+ }, reason => {
+ resData.error = reason;
+ respond(resData);
+ });
+}
+
+/*
+ * Converts a nsINavHistoryResultNode into a plain object
+ *
+ * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
+ */
+function normalize (historyObj) {
+ return PLACES_PROPERTIES.reduce((obj, prop) => {
+ if (prop === 'uri')
+ obj.url = historyObj.uri;
+ else if (prop === 'time') {
+ // Cast from microseconds to milliseconds
+ obj.time = Math.floor(historyObj.time / 1000)
+ }
+ else if (prop === 'accessCount')
+ obj.visitCount = historyObj[prop];
+ else
+ obj[prop] = historyObj[prop];
+ return obj;
+ }, {});
+}
+
+/*
+ * Hook into host
+ */
+
+var reqStream = filter(request, data => /sdk-places-query/.test(data.event));
+on(reqStream, 'data', function (e) {
+ if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e);
+});
+
+function respond (data) {
+ emit(response, 'data', data);
+}
diff --git a/toolkit/jetpack/sdk/places/host/host-tags.js b/toolkit/jetpack/sdk/places/host/host-tags.js
new file mode 100644
index 000000000..929a5d5af
--- /dev/null
+++ b/toolkit/jetpack/sdk/places/host/host-tags.js
@@ -0,0 +1,92 @@
+/* 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";
+
+module.metadata = {
+ "stability": "experimental",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+const { Cc, Ci } = require('chrome');
+const taggingService = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+const ios = Cc['@mozilla.org/network/io-service;1'].
+ getService(Ci.nsIIOService);
+const { URL } = require('../../url');
+const { newURI } = require('../../url/utils');
+const { request, response } = require('../../addon/host');
+const { on, emit } = require('../../event/core');
+const { filter } = require('../../event/utils');
+
+const EVENT_MAP = {
+ 'sdk-places-tags-tag': tag,
+ 'sdk-places-tags-untag': untag,
+ 'sdk-places-tags-get-tags-by-url': getTagsByURL,
+ 'sdk-places-tags-get-urls-by-tag': getURLsByTag
+};
+
+function tag (message) {
+ let data = message.data;
+ let resData = {
+ id: message.id,
+ event: message.event
+ };
+
+ resData.data = taggingService.tagURI(newURI(data.url), data.tags);
+ respond(resData);
+}
+
+function untag (message) {
+ let data = message.data;
+ let resData = {
+ id: message.id,
+ event: message.event
+ };
+
+ resData.data = taggingService.untagURI(newURI(data.url), data.tags);
+ respond(resData);
+}
+
+function getURLsByTag (message) {
+ let data = message.data;
+ let resData = {
+ id: message.id,
+ event: message.event
+ };
+
+ resData.data = taggingService
+ .getURIsForTag(data.tag).map(uri => uri.spec);
+ respond(resData);
+}
+
+function getTagsByURL (message) {
+ let data = message.data;
+ let resData = {
+ id: message.id,
+ event: message.event
+ };
+
+ resData.data = taggingService.getTagsForURI(newURI(data.url), {});
+ respond(resData);
+}
+
+/*
+ * Hook into host
+ */
+
+var reqStream = filter(request, function (data) {
+ return /sdk-places-tags/.test(data.event);
+});
+
+on(reqStream, 'data', function (e) {
+ if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e);
+});
+
+function respond (data) {
+ emit(response, 'data', data);
+}