michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: module.metadata = { michael@0: "stability": "experimental", michael@0: "engines": { michael@0: "Firefox": "*" michael@0: } michael@0: }; michael@0: michael@0: const { Cc, Ci } = require('chrome'); michael@0: const { defer, all, resolve } = require('../../core/promise'); michael@0: const { safeMerge, omit } = require('../../util/object'); michael@0: const historyService = Cc['@mozilla.org/browser/nav-history-service;1'] michael@0: .getService(Ci.nsINavHistoryService); michael@0: const bookmarksService = Cc['@mozilla.org/browser/nav-bookmarks-service;1'] michael@0: .getService(Ci.nsINavBookmarksService); michael@0: const { request, response } = require('../../addon/host'); michael@0: const { newURI } = require('../../url/utils'); michael@0: const { send } = require('../../addon/events'); michael@0: const { on, emit } = require('../../event/core'); michael@0: const { filter } = require('../../event/utils'); michael@0: michael@0: const ROOT_FOLDERS = [ michael@0: bookmarksService.unfiledBookmarksFolder, bookmarksService.toolbarFolder, michael@0: bookmarksService.bookmarksMenuFolder michael@0: ]; michael@0: michael@0: const EVENT_MAP = { michael@0: 'sdk-places-query': queryReceiver michael@0: }; michael@0: michael@0: // Properties that need to be manually michael@0: // copied into a nsINavHistoryQuery object michael@0: const MANUAL_QUERY_PROPERTIES = [ michael@0: 'uri', 'folder', 'tags', 'url', 'folder' michael@0: ]; michael@0: michael@0: const PLACES_PROPERTIES = [ michael@0: 'uri', 'title', 'accessCount', 'time' michael@0: ]; michael@0: michael@0: function execute (queries, options) { michael@0: let deferred = defer(); michael@0: let root = historyService michael@0: .executeQueries(queries, queries.length, options).root; michael@0: michael@0: let items = collect([], root); michael@0: deferred.resolve(items); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function collect (acc, node) { michael@0: node.containerOpen = true; michael@0: for (let i = 0; i < node.childCount; i++) { michael@0: let child = node.getChild(i); michael@0: acc.push(child); michael@0: if (child.type === child.RESULT_TYPE_FOLDER) { michael@0: let container = child.QueryInterface(Ci.nsINavHistoryContainerResultNode); michael@0: collect(acc, container); michael@0: } michael@0: } michael@0: node.containerOpen = false; michael@0: return acc; michael@0: } michael@0: michael@0: function query (queries, options) { michael@0: queries = queries || []; michael@0: options = options || {}; michael@0: let deferred = defer(); michael@0: let optionsObj, queryObjs; michael@0: michael@0: try { michael@0: optionsObj = historyService.getNewQueryOptions(); michael@0: queryObjs = [].concat(queries).map(createQuery); michael@0: if (!queryObjs.length) { michael@0: queryObjs = [historyService.getNewQuery()]; michael@0: } michael@0: safeMerge(optionsObj, options); michael@0: } catch (e) { michael@0: deferred.reject(e); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /* michael@0: * Currently `places:` queries are not supported michael@0: */ michael@0: optionsObj.excludeQueries = true; michael@0: michael@0: execute(queryObjs, optionsObj).then(function (results) { michael@0: if (optionsObj.queryType === 0) { michael@0: return results.map(normalize); michael@0: } else if (optionsObj.queryType === 1) { michael@0: // Formats query results into more standard michael@0: // data structures for returning michael@0: return all(results.map(({itemId}) => michael@0: send('sdk-places-bookmarks-get', { id: itemId }))); michael@0: } michael@0: }).then(deferred.resolve, deferred.reject); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: exports.query = query; michael@0: michael@0: function createQuery (query) { michael@0: query = query || {}; michael@0: let queryObj = historyService.getNewQuery(); michael@0: michael@0: safeMerge(queryObj, omit(query, MANUAL_QUERY_PROPERTIES)); michael@0: michael@0: if (query.tags && Array.isArray(query.tags)) michael@0: queryObj.tags = query.tags; michael@0: if (query.uri || query.url) michael@0: queryObj.uri = newURI(query.uri || query.url); michael@0: if (query.folder) michael@0: queryObj.setFolders([query.folder], 1); michael@0: return queryObj; michael@0: } michael@0: michael@0: function queryReceiver (message) { michael@0: let queries = message.data.queries || message.data.query; michael@0: let options = message.data.options; michael@0: let resData = { michael@0: id: message.id, michael@0: event: message.event michael@0: }; michael@0: michael@0: query(queries, options).then(results => { michael@0: resData.data = results; michael@0: respond(resData); michael@0: }, reason => { michael@0: resData.error = reason; michael@0: respond(resData); michael@0: }); michael@0: } michael@0: michael@0: /* michael@0: * Converts a nsINavHistoryResultNode into a plain object michael@0: * michael@0: * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode michael@0: */ michael@0: function normalize (historyObj) { michael@0: return PLACES_PROPERTIES.reduce((obj, prop) => { michael@0: if (prop === 'uri') michael@0: obj.url = historyObj.uri; michael@0: else if (prop === 'time') { michael@0: // Cast from microseconds to milliseconds michael@0: obj.time = Math.floor(historyObj.time / 1000) michael@0: } else if (prop === 'accessCount') michael@0: obj.visitCount = historyObj[prop]; michael@0: else michael@0: obj[prop] = historyObj[prop]; michael@0: return obj; michael@0: }, {}); michael@0: } michael@0: michael@0: /* michael@0: * Hook into host michael@0: */ michael@0: michael@0: let reqStream = filter(request, function (data) /sdk-places-query/.test(data.event)); michael@0: on(reqStream, 'data', function (e) { michael@0: if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e); michael@0: }); michael@0: michael@0: function respond (data) { michael@0: emit(response, 'data', data); michael@0: }