summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/places
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-10 02:51:36 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-10 02:51:36 -0500
commit37d5300335d81cecbecc99812747a657588c63eb (patch)
tree765efa3b6a56bb715d9813a8697473e120436278 /toolkit/jetpack/sdk/places
parentb2bdac20c02b12f2057b9ef70b0a946113a00e00 (diff)
parent4fb11cd5966461bccc3ed1599b808237be6b0de9 (diff)
downloadUXP-37d5300335d81cecbecc99812747a657588c63eb.tar
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.gz
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.lz
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.xz
UXP-37d5300335d81cecbecc99812747a657588c63eb.zip
Merge branch 'ext-work'
Diffstat (limited to 'toolkit/jetpack/sdk/places')
-rw-r--r--toolkit/jetpack/sdk/places/bookmarks.js395
-rw-r--r--toolkit/jetpack/sdk/places/contract.js73
-rw-r--r--toolkit/jetpack/sdk/places/events.js128
-rw-r--r--toolkit/jetpack/sdk/places/favicon.js49
-rw-r--r--toolkit/jetpack/sdk/places/history.js65
-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
-rw-r--r--toolkit/jetpack/sdk/places/utils.js268
9 files changed, 1487 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/places/bookmarks.js b/toolkit/jetpack/sdk/places/bookmarks.js
new file mode 100644
index 000000000..c4f9528f1
--- /dev/null
+++ b/toolkit/jetpack/sdk/places/bookmarks.js
@@ -0,0 +1,395 @@
+/* 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": "unstable",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+/*
+ * Requiring hosts so they can subscribe to client messages
+ */
+require('./host/host-bookmarks');
+require('./host/host-tags');
+require('./host/host-query');
+
+const { Cc, Ci } = require('chrome');
+const { Class } = require('../core/heritage');
+const { send } = require('../addon/events');
+const { defer, reject, all, resolve, promised } = require('../core/promise');
+const { EventTarget } = require('../event/target');
+const { emit } = require('../event/core');
+const { identity, defer:async } = require('../lang/functional');
+const { extend, merge } = require('../util/object');
+const { fromIterator } = require('../util/array');
+const {
+ constructTree, fetchItem, createQuery,
+ isRootGroup, createQueryOptions
+} = require('./utils');
+const {
+ bookmarkContract, groupContract, separatorContract
+} = require('./contract');
+const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+/*
+ * Mapping of uncreated bookmarks with their created
+ * counterparts
+ */
+const itemMap = new WeakMap();
+
+/*
+ * Constant used by nsIHistoryQuery; 1 is a bookmark query
+ * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
+ */
+const BOOKMARK_QUERY = 1;
+
+/*
+ * Bookmark Item classes
+ */
+
+const Bookmark = Class({
+ extends: [
+ bookmarkContract.properties(identity)
+ ],
+ initialize: function initialize (options) {
+ merge(this, bookmarkContract(extend(defaults, options)));
+ },
+ type: 'bookmark',
+ toString: () => '[object Bookmark]'
+});
+exports.Bookmark = Bookmark;
+
+const Group = Class({
+ extends: [
+ groupContract.properties(identity)
+ ],
+ initialize: function initialize (options) {
+ // Don't validate if root group
+ if (isRootGroup(options))
+ merge(this, options);
+ else
+ merge(this, groupContract(extend(defaults, options)));
+ },
+ type: 'group',
+ toString: () => '[object Group]'
+});
+exports.Group = Group;
+
+const Separator = Class({
+ extends: [
+ separatorContract.properties(identity)
+ ],
+ initialize: function initialize (options) {
+ merge(this, separatorContract(extend(defaults, options)));
+ },
+ type: 'separator',
+ toString: () => '[object Separator]'
+});
+exports.Separator = Separator;
+
+/*
+ * Functions
+ */
+
+function save (items, options) {
+ items = [].concat(items);
+ options = options || {};
+ let emitter = EventTarget();
+ let results = [];
+ let errors = [];
+ let root = constructTree(items);
+ let cache = new Map();
+
+ let isExplicitSave = item => !!~items.indexOf(item);
+ // `walk` returns an aggregate promise indicating the completion
+ // of the `commitItem` on each node, not whether or not that
+ // commit was successful
+
+ // Force this to be async, as if a ducktype fails validation,
+ // the promise implementation will fire an error event, which will
+ // not trigger the handler as it's not yet bound
+ //
+ // Can remove after `Promise.jsm` is implemented in Bug 881047,
+ // which will guarantee next tick execution
+ async(() => root.walk(preCommitItem).then(commitComplete))();
+
+ function preCommitItem ({value:item}) {
+ // Do nothing if tree root, default group (unsavable),
+ // or if it's a dependency and not explicitly saved (in the list
+ // of items to be saved), and not needed to be saved
+ if (item === null || // node is the tree root
+ isRootGroup(item) ||
+ (getId(item) && !isExplicitSave(item)))
+ return;
+
+ return promised(validate)(item)
+ .then(() => commitItem(item, options))
+ .then(data => construct(data, cache))
+ .then(savedItem => {
+ // If item was just created, make a map between
+ // the creation object and created object,
+ // so we can reference the item that doesn't have an id
+ if (!getId(item))
+ saveId(item, savedItem.id);
+
+ // Emit both the processed item, and original item
+ // so a mapping can be understood in handler
+ emit(emitter, 'data', savedItem, item);
+
+ // Push to results iff item was explicitly saved
+ if (isExplicitSave(item))
+ results[items.indexOf(item)] = savedItem;
+ }, reason => {
+ // Force reason to be a string for consistency
+ reason = reason + '';
+ // Emit both the reason, and original item
+ // so a mapping can be understood in handler
+ emit(emitter, 'error', reason + '', item);
+ // Store unsaved item in results list
+ results[items.indexOf(item)] = item;
+ errors.push(reason);
+ });
+ }
+
+ // Called when traversal of the node tree is completed and all
+ // items have been committed
+ function commitComplete () {
+ emit(emitter, 'end', results);
+ }
+
+ return emitter;
+}
+exports.save = save;
+
+function search (queries, options) {
+ queries = [].concat(queries);
+ let emitter = EventTarget();
+ let cache = new Map();
+ let queryObjs = queries.map(createQuery.bind(null, BOOKMARK_QUERY));
+ let optionsObj = createQueryOptions(BOOKMARK_QUERY, options);
+
+ // Can remove after `Promise.jsm` is implemented in Bug 881047,
+ // which will guarantee next tick execution
+ async(() => {
+ send('sdk-places-query', { queries: queryObjs, options: optionsObj })
+ .then(handleQueryResponse);
+ })();
+
+ function handleQueryResponse (data) {
+ let deferreds = data.map(item => {
+ return construct(item, cache).then(bookmark => {
+ emit(emitter, 'data', bookmark);
+ return bookmark;
+ }, reason => {
+ emit(emitter, 'error', reason);
+ errors.push(reason);
+ });
+ });
+
+ all(deferreds).then(data => {
+ emit(emitter, 'end', data);
+ }, () => emit(emitter, 'end', []));
+ }
+
+ return emitter;
+}
+exports.search = search;
+
+function remove (items) {
+ return [].concat(items).map(item => {
+ item.remove = true;
+ return item;
+ });
+}
+
+exports.remove = remove;
+
+/*
+ * Internal Utilities
+ */
+
+function commitItem (item, options) {
+ // Get the item's ID, or getId it's saved version if it exists
+ let id = getId(item);
+ let data = normalize(item);
+ let promise;
+
+ data.id = id;
+
+ if (!id) {
+ promise = send('sdk-places-bookmarks-create', data);
+ } else if (item.remove) {
+ promise = send('sdk-places-bookmarks-remove', { id: id });
+ } else {
+ promise = send('sdk-places-bookmarks-last-updated', {
+ id: id
+ }).then(function (updated) {
+ // If attempting to save an item that is not the
+ // latest snapshot of a bookmark item, execute
+ // the resolution function
+ if (updated !== item.updated && options.resolve)
+ return fetchItem(id)
+ .then(options.resolve.bind(null, data));
+ else
+ return data;
+ }).then(send.bind(null, 'sdk-places-bookmarks-save'));
+ }
+
+ return promise;
+}
+
+/*
+ * Turns a bookmark item into a plain object,
+ * converts `tags` from Set to Array, group instance to an id
+ */
+function normalize (item) {
+ let data = merge({}, item);
+ // Circumvent prototype property of `type`
+ delete data.type;
+ data.type = item.type;
+ data.tags = [];
+ if (item.tags) {
+ data.tags = fromIterator(item.tags);
+ }
+ data.group = getId(data.group) || exports.UNSORTED.id;
+
+ return data;
+}
+
+/*
+ * Takes a data object and constructs a BookmarkItem instance
+ * of it, recursively generating parent instances as well.
+ *
+ * Pass in a `cache` Map to reuse instances of
+ * bookmark items to reduce overhead;
+ * The cache object is a map of id to a deferred with a
+ * promise that resolves to the bookmark item.
+ */
+function construct (object, cache, forced) {
+ let item = instantiate(object);
+ let deferred = defer();
+
+ // Item could not be instantiated
+ if (!item)
+ return resolve(null);
+
+ // Return promise for item if found in the cache,
+ // and not `forced`. `forced` indicates that this is the construct
+ // call that should not read from cache, but should actually perform
+ // the construction, as it was set before several async calls
+ if (cache.has(item.id) && !forced)
+ return cache.get(item.id).promise;
+ else if (cache.has(item.id))
+ deferred = cache.get(item.id);
+ else
+ cache.set(item.id, deferred);
+
+ // When parent group is found in cache, use
+ // the same deferred value
+ if (item.group && cache.has(item.group)) {
+ cache.get(item.group).promise.then(group => {
+ item.group = group;
+ deferred.resolve(item);
+ });
+
+ // If not in the cache, and a root group, return
+ // the premade instance
+ } else if (rootGroups.get(item.group)) {
+ item.group = rootGroups.get(item.group);
+ deferred.resolve(item);
+
+ // If not in the cache or a root group, fetch the parent
+ } else {
+ cache.set(item.group, defer());
+ fetchItem(item.group).then(group => {
+ return construct(group, cache, true);
+ }).then(group => {
+ item.group = group;
+ deferred.resolve(item);
+ }, deferred.reject);
+ }
+
+ return deferred.promise;
+}
+
+function instantiate (object) {
+ if (object.type === 'bookmark')
+ return Bookmark(object);
+ if (object.type === 'group')
+ return Group(object);
+ if (object.type === 'separator')
+ return Separator(object);
+ return null;
+}
+
+/**
+ * Validates a bookmark item; will throw an error if ininvalid,
+ * to be used with `promised`. As bookmark items check on their class,
+ * this only checks ducktypes
+ */
+function validate (object) {
+ if (!isDuckType(object)) return true;
+ let contract = object.type === 'bookmark' ? bookmarkContract :
+ object.type === 'group' ? groupContract :
+ object.type === 'separator' ? separatorContract :
+ null;
+ if (!contract) {
+ throw Error('No type specified');
+ }
+
+ // If object has a property set, and undefined,
+ // manually override with default as it'll fail otherwise
+ let withDefaults = Object.keys(defaults).reduce((obj, prop) => {
+ if (obj[prop] == null) obj[prop] = defaults[prop];
+ return obj;
+ }, extend(object));
+
+ contract(withDefaults);
+}
+
+function isDuckType (item) {
+ return !(item instanceof Bookmark) &&
+ !(item instanceof Group) &&
+ !(item instanceof Separator);
+}
+
+function saveId (unsaved, id) {
+ itemMap.set(unsaved, id);
+}
+
+// Fetches an item's ID from itself, or from the mapped items
+function getId (item) {
+ return typeof item === 'number' ? item :
+ item ? item.id || itemMap.get(item) :
+ null;
+}
+
+/*
+ * Set up the default, root groups
+ */
+
+var defaultGroupMap = {
+ MENU: bmsrv.bookmarksMenuFolder,
+ TOOLBAR: bmsrv.toolbarFolder,
+ UNSORTED: bmsrv.unfiledBookmarksFolder
+};
+
+var rootGroups = new Map();
+
+for (let i in defaultGroupMap) {
+ let group = Object.freeze(Group({ title: i, id: defaultGroupMap[i] }));
+ rootGroups.set(defaultGroupMap[i], group);
+ exports[i] = group;
+}
+
+var defaults = {
+ group: exports.UNSORTED,
+ index: -1
+};
diff --git a/toolkit/jetpack/sdk/places/contract.js b/toolkit/jetpack/sdk/places/contract.js
new file mode 100644
index 000000000..a3541c34d
--- /dev/null
+++ b/toolkit/jetpack/sdk/places/contract.js
@@ -0,0 +1,73 @@
+/* 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": "unstable"
+};
+
+const { Cc, Ci } = require('chrome');
+const { isValidURI, URL } = require('../url');
+const { contract } = require('../util/contract');
+const { extend } = require('../util/object');
+
+// map of property validations
+const validItem = {
+ id: {
+ is: ['number', 'undefined', 'null'],
+ },
+ group: {
+ is: ['object', 'number', 'undefined', 'null'],
+ ok: function (value) {
+ return value &&
+ (value.toString && value.toString() === '[object Group]') ||
+ typeof value === 'number' ||
+ value.type === 'group';
+ },
+ msg: 'The `group` property must be a valid Group object'
+ },
+ index: {
+ is: ['undefined', 'null', 'number'],
+ map: value => value == null ? -1 : value,
+ msg: 'The `index` property must be a number.'
+ },
+ updated: {
+ is: ['number', 'undefined']
+ }
+};
+
+const validTitle = {
+ title: {
+ is: ['string'],
+ msg: 'The `title` property must be defined.'
+ }
+};
+
+const validURL = {
+ url: {
+ is: ['string'],
+ ok: isValidURI,
+ msg: 'The `url` property must be a valid URL.'
+ }
+};
+
+const validTags = {
+ tags: {
+ is: ['object'],
+ ok: tags => tags instanceof Set,
+ map: function (tags) {
+ if (Array.isArray(tags))
+ return new Set(tags);
+ if (tags == null)
+ return new Set();
+ return tags;
+ },
+ msg: 'The `tags` property must be a Set, or an array'
+ }
+};
+
+exports.bookmarkContract = contract(
+ extend(validItem, validTitle, validURL, validTags));
+exports.separatorContract = contract(validItem);
+exports.groupContract = contract(extend(validItem, validTitle));
diff --git a/toolkit/jetpack/sdk/places/events.js b/toolkit/jetpack/sdk/places/events.js
new file mode 100644
index 000000000..a3f95ee03
--- /dev/null
+++ b/toolkit/jetpack/sdk/places/events.js
@@ -0,0 +1,128 @@
+/* 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 { Unknown } = require('../platform/xpcom');
+const { Class } = require('../core/heritage');
+const { merge } = require('../util/object');
+const bookmarkService = Cc['@mozilla.org/browser/nav-bookmarks-service;1']
+ .getService(Ci.nsINavBookmarksService);
+const historyService = Cc['@mozilla.org/browser/nav-history-service;1']
+ .getService(Ci.nsINavHistoryService);
+const { mapBookmarkItemType } = require('./utils');
+const { EventTarget } = require('../event/target');
+const { emit } = require('../event/core');
+const { when } = require('../system/unload');
+
+const emitter = EventTarget();
+
+var HISTORY_ARGS = {
+ onBeginUpdateBatch: [],
+ onEndUpdateBatch: [],
+ onClearHistory: [],
+ onDeleteURI: ['url'],
+ onDeleteVisits: ['url', 'visitTime'],
+ onPageChanged: ['url', 'property', 'value'],
+ onTitleChanged: ['url', 'title'],
+ onVisit: [
+ 'url', 'visitId', 'time', 'sessionId', 'referringId', 'transitionType'
+ ]
+};
+
+var HISTORY_EVENTS = {
+ onBeginUpdateBatch: 'history-start-batch',
+ onEndUpdateBatch: 'history-end-batch',
+ onClearHistory: 'history-start-clear',
+ onDeleteURI: 'history-delete-url',
+ onDeleteVisits: 'history-delete-visits',
+ onPageChanged: 'history-page-changed',
+ onTitleChanged: 'history-title-changed',
+ onVisit: 'history-visit'
+};
+
+var BOOKMARK_ARGS = {
+ onItemAdded: [
+ 'id', 'parentId', 'index', 'type', 'url', 'title', 'dateAdded'
+ ],
+ onItemChanged: [
+ 'id', 'property', null, 'value', 'lastModified', 'type', 'parentId'
+ ],
+ onItemMoved: [
+ 'id', 'previousParentId', 'previousIndex', 'currentParentId',
+ 'currentIndex', 'type'
+ ],
+ onItemRemoved: ['id', 'parentId', 'index', 'type', 'url'],
+ onItemVisited: ['id', 'visitId', 'time', 'transitionType', 'url', 'parentId']
+};
+
+var BOOKMARK_EVENTS = {
+ onItemAdded: 'bookmark-item-added',
+ onItemChanged: 'bookmark-item-changed',
+ onItemMoved: 'bookmark-item-moved',
+ onItemRemoved: 'bookmark-item-removed',
+ onItemVisited: 'bookmark-item-visited',
+};
+
+function createHandler (type, propNames) {
+ propNames = propNames || [];
+ return function (...args) {
+ let data = propNames.reduce((acc, prop, i) => {
+ if (prop)
+ acc[prop] = formatValue(prop, args[i]);
+ return acc;
+ }, {});
+
+ emit(emitter, 'data', {
+ type: type,
+ data: data
+ });
+ };
+}
+
+/*
+ * Creates an observer, creating handlers based off of
+ * the `events` names, and ordering arguments from `propNames` hash
+ */
+function createObserverInstance (events, propNames) {
+ let definition = Object.keys(events).reduce((prototype, eventName) => {
+ prototype[eventName] = createHandler(events[eventName], propNames[eventName]);
+ return prototype;
+ }, {});
+
+ return Class(merge(definition, { extends: Unknown }))();
+}
+
+/*
+ * Formats `data` based off of the value of `type`
+ */
+function formatValue (type, data) {
+ if (type === 'type')
+ return mapBookmarkItemType(data);
+ if (type === 'url' && data)
+ return data.spec;
+ return data;
+}
+
+var historyObserver = createObserverInstance(HISTORY_EVENTS, HISTORY_ARGS);
+historyService.addObserver(historyObserver, false);
+
+var bookmarkObserver = createObserverInstance(BOOKMARK_EVENTS, BOOKMARK_ARGS);
+bookmarkService.addObserver(bookmarkObserver, false);
+
+when(() => {
+ historyService.removeObserver(historyObserver);
+ bookmarkService.removeObserver(bookmarkObserver);
+});
+
+exports.events = emitter;
diff --git a/toolkit/jetpack/sdk/places/favicon.js b/toolkit/jetpack/sdk/places/favicon.js
new file mode 100644
index 000000000..05b057db1
--- /dev/null
+++ b/toolkit/jetpack/sdk/places/favicon.js
@@ -0,0 +1,49 @@
+/* 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": "unstable",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+const { Cc, Ci, Cu } = require("chrome");
+const { defer, reject } = require("../core/promise");
+const FaviconService = Cc["@mozilla.org/browser/favicon-service;1"].
+ getService(Ci.nsIFaviconService);
+const AsyncFavicons = FaviconService.QueryInterface(Ci.mozIAsyncFavicons);
+const { isValidURI } = require("../url");
+const { newURI, getURL } = require("../url/utils");
+
+/**
+ * Takes an object of several possible types and
+ * returns a promise that resolves to the page's favicon URI.
+ * @param {String|Tab} object
+ * @param {Function} (callback)
+ * @returns {Promise}
+ */
+
+function getFavicon (object, callback) {
+ let url = getURL(object);
+ let deferred = defer();
+
+ if (url && isValidURI(url)) {
+ AsyncFavicons.getFaviconURLForPage(newURI(url), function (aURI) {
+ if (aURI && aURI.spec)
+ deferred.resolve(aURI.spec.toString());
+ else
+ deferred.reject(null);
+ });
+ } else {
+ deferred.reject(null);
+ }
+
+ if (callback) deferred.promise.then(callback, callback);
+ return deferred.promise;
+}
+exports.getFavicon = getFavicon;
diff --git a/toolkit/jetpack/sdk/places/history.js b/toolkit/jetpack/sdk/places/history.js
new file mode 100644
index 000000000..b243b024c
--- /dev/null
+++ b/toolkit/jetpack/sdk/places/history.js
@@ -0,0 +1,65 @@
+/* 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": "unstable",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+/*
+ * Requiring hosts so they can subscribe to client messages
+ */
+require('./host/host-bookmarks');
+require('./host/host-tags');
+require('./host/host-query');
+
+const { Cc, Ci } = require('chrome');
+const { Class } = require('../core/heritage');
+const { events, send } = require('../addon/events');
+const { defer, reject, all } = require('../core/promise');
+const { uuid } = require('../util/uuid');
+const { flatten } = require('../util/array');
+const { has, extend, merge, pick } = require('../util/object');
+const { emit } = require('../event/core');
+const { defer: async } = require('../lang/functional');
+const { EventTarget } = require('../event/target');
+const {
+ urlQueryParser, createQuery, createQueryOptions
+} = require('./utils');
+
+/*
+ * Constant used by nsIHistoryQuery; 0 is a history query
+ * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
+ */
+const HISTORY_QUERY = 0;
+
+var search = function query (queries, options) {
+ queries = [].concat(queries);
+ let emitter = EventTarget();
+ let queryObjs = queries.map(createQuery.bind(null, HISTORY_QUERY));
+ let optionsObj = createQueryOptions(HISTORY_QUERY, options);
+
+ // Can remove after `Promise.jsm` is implemented in Bug 881047,
+ // which will guarantee next tick execution
+ async(() => {
+ send('sdk-places-query', {
+ query: queryObjs,
+ options: optionsObj
+ }).then(results => {
+ results.map(item => emit(emitter, 'data', item));
+ emit(emitter, 'end', results);
+ }, reason => {
+ emit(emitter, 'error', reason);
+ emit(emitter, 'end', []);
+ });
+ })();
+
+ return emitter;
+};
+exports.search = search;
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);
+}
diff --git a/toolkit/jetpack/sdk/places/utils.js b/toolkit/jetpack/sdk/places/utils.js
new file mode 100644
index 000000000..44366d2aa
--- /dev/null
+++ b/toolkit/jetpack/sdk/places/utils.js
@@ -0,0 +1,268 @@
+/* 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, Cu } = require('chrome');
+const { Class } = require('../core/heritage');
+const { method } = require('../lang/functional');
+const { defer, promised, all } = require('../core/promise');
+const { send } = require('../addon/events');
+const { EventTarget } = require('../event/target');
+const { merge } = require('../util/object');
+const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+Cu.importGlobalProperties(["URL"]);
+
+/*
+ * TreeNodes are used to construct dependency trees
+ * for BookmarkItems
+ */
+var TreeNode = Class({
+ initialize: function (value) {
+ this.value = value;
+ this.children = [];
+ },
+ add: function (values) {
+ [].concat(values).forEach(value => {
+ this.children.push(value instanceof TreeNode ? value : TreeNode(value));
+ });
+ },
+ get length () {
+ let count = 0;
+ this.walk(() => count++);
+ // Do not count the current node
+ return --count;
+ },
+ get: method(get),
+ walk: method(walk),
+ toString: () => '[object TreeNode]'
+});
+exports.TreeNode = TreeNode;
+
+/*
+ * Descends down from `node` applying `fn` to each in order.
+ * `fn` can return values or promises -- if promise returned,
+ * children are not processed until resolved. `fn` is passed
+ * one argument, the current node, `curr`.
+ */
+function walk (curr, fn) {
+ return promised(fn)(curr).then(val => {
+ return all(curr.children.map(child => walk(child, fn)));
+ });
+}
+
+/*
+ * Descends from the TreeNode `node`, returning
+ * the node with value `value` if found or `null`
+ * otherwise
+ */
+function get (node, value) {
+ if (node.value === value) return node;
+ for (let child of node.children) {
+ let found = get(child, value);
+ if (found) return found;
+ }
+ return null;
+}
+
+/*
+ * Constructs a tree of bookmark nodes
+ * returning the root (value: null);
+ */
+
+function constructTree (items) {
+ let root = TreeNode(null);
+ items.forEach(treeify.bind(null, root));
+
+ function treeify (root, item) {
+ // If node already exists, skip
+ let node = root.get(item);
+ if (node) return node;
+ node = TreeNode(item);
+
+ let parentNode = item.group ? treeify(root, item.group) : root;
+ parentNode.add(node);
+
+ return node;
+ }
+
+ return root;
+}
+exports.constructTree = constructTree;
+
+/*
+ * Shortcut for converting an id, or an object with an id, into
+ * an object with corresponding bookmark data
+ */
+function fetchItem (item) {
+ return send('sdk-places-bookmarks-get', { id: item.id || item });
+}
+exports.fetchItem = fetchItem;
+
+/*
+ * Takes an ID or an object with ID and checks it against
+ * the root bookmark folders
+ */
+function isRootGroup (id) {
+ id = id && id.id;
+ return ~[bmsrv.bookmarksMenuFolder, bmsrv.toolbarFolder,
+ bmsrv.unfiledBookmarksFolder
+ ].indexOf(id);
+}
+exports.isRootGroup = isRootGroup;
+
+/*
+ * Merges appropriate options into query based off of url
+ * 4 scenarios:
+ *
+ * 'moz.com' // domain: moz.com, domainIsHost: true
+ * --> 'http://moz.com', 'http://moz.com/thunderbird'
+ * '*.moz.com' // domain: moz.com, domainIsHost: false
+ * --> 'http://moz.com', 'http://moz.com/index', 'http://ff.moz.com/test'
+ * 'http://moz.com' // uri: http://moz.com/
+ * --> 'http://moz.com/'
+ * 'http://moz.com/*' // uri: http://moz.com/, domain: moz.com, domainIsHost: true
+ * --> 'http://moz.com/', 'http://moz.com/thunderbird'
+ */
+
+function urlQueryParser (query, url) {
+ if (!url) return;
+ if (/^https?:\/\//.test(url)) {
+ query.uri = url.charAt(url.length - 1) === '/' ? url : url + '/';
+ if (/\*$/.test(url)) {
+ // Wildcard searches on URIs are not supported, so try to extract a
+ // domain and filter the data later.
+ url = url.replace(/\*$/, '');
+ try {
+ query.domain = new URL(url).hostname;
+ query.domainIsHost = true;
+ // Unfortunately here we cannot use an expando to store the wildcard,
+ // cause the query is a wrapped native XPCOM object, so we reuse uri.
+ // We clearly don't want to query for both uri and domain, thus we'll
+ // have to handle this in host-query.js::execute()
+ query.uri = url;
+ } catch (ex) {
+ // Cannot extract an host cause it's not a valid uri, the query will
+ // just return nothing.
+ }
+ }
+ } else {
+ if (/^\*/.test(url)) {
+ query.domain = url.replace(/^\*\./, '');
+ query.domainIsHost = false;
+ } else {
+ query.domain = url;
+ query.domainIsHost = true;
+ }
+ }
+}
+exports.urlQueryParser = urlQueryParser;
+
+/*
+ * Takes an EventEmitter and returns a promise that
+ * aggregates results and handles a bulk resolve and reject
+ */
+
+function promisedEmitter (emitter) {
+ let { promise, resolve, reject } = defer();
+ let errors = [];
+ emitter.on('error', error => errors.push(error));
+ emitter.on('end', (items) => {
+ if (errors.length) reject(errors[0]);
+ else resolve(items);
+ });
+ return promise;
+}
+exports.promisedEmitter = promisedEmitter;
+
+
+// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
+function createQuery (type, query) {
+ query = query || {};
+ let qObj = {
+ searchTerms: query.query
+ };
+
+ urlQueryParser(qObj, query.url);
+
+ // 0 === history
+ if (type === 0) {
+ // PRTime used by query is in microseconds, not milliseconds
+ qObj.beginTime = (query.from || 0) * 1000;
+ qObj.endTime = (query.to || new Date()) * 1000;
+
+ // Set reference time to Epoch
+ qObj.beginTimeReference = 0;
+ qObj.endTimeReference = 0;
+ }
+ // 1 === bookmarks
+ else if (type === 1) {
+ qObj.tags = query.tags;
+ qObj.folder = query.group && query.group.id;
+ }
+ // 2 === unified (not implemented on platform)
+ else if (type === 2) {
+
+ }
+
+ return qObj;
+}
+exports.createQuery = createQuery;
+
+// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
+
+const SORT_MAP = {
+ title: 1,
+ date: 3, // sort by visit date
+ url: 5,
+ visitCount: 7,
+ // keywords currently unsupported
+ // keyword: 9,
+ dateAdded: 11, // bookmarks only
+ lastModified: 13 // bookmarks only
+};
+
+function createQueryOptions (type, options) {
+ options = options || {};
+ let oObj = {};
+ oObj.sortingMode = SORT_MAP[options.sort] || 0;
+ if (options.descending && options.sort)
+ oObj.sortingMode++;
+
+ // Resolve to default sort if ineligible based on query type
+ if (type === 0 && // history
+ (options.sort === 'dateAdded' || options.sort === 'lastModified'))
+ oObj.sortingMode = 0;
+
+ oObj.maxResults = typeof options.count === 'number' ? options.count : 0;
+
+ oObj.queryType = type;
+
+ return oObj;
+}
+exports.createQueryOptions = createQueryOptions;
+
+
+function mapBookmarkItemType (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;
+ }
+}
+exports.mapBookmarkItemType = mapBookmarkItemType;