diff options
Diffstat (limited to 'toolkit/jetpack/sdk/places/host')
-rw-r--r-- | toolkit/jetpack/sdk/places/host/host-bookmarks.js | 238 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/places/host/host-query.js | 179 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/places/host/host-tags.js | 92 |
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); +} |