host-query.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. module.metadata = {
  6. "stability": "experimental",
  7. "engines": {
  8. "Firefox": "*"
  9. }
  10. };
  11. const { Cc, Ci } = require('chrome');
  12. const { defer, all, resolve } = require('../../core/promise');
  13. const { safeMerge, omit } = require('../../util/object');
  14. const historyService = Cc['@mozilla.org/browser/nav-history-service;1']
  15. .getService(Ci.nsINavHistoryService);
  16. const bookmarksService = Cc['@mozilla.org/browser/nav-bookmarks-service;1']
  17. .getService(Ci.nsINavBookmarksService);
  18. const { request, response } = require('../../addon/host');
  19. const { newURI } = require('../../url/utils');
  20. const { send } = require('../../addon/events');
  21. const { on, emit } = require('../../event/core');
  22. const { filter } = require('../../event/utils');
  23. const ROOT_FOLDERS = [
  24. bookmarksService.unfiledBookmarksFolder, bookmarksService.toolbarFolder,
  25. bookmarksService.bookmarksMenuFolder
  26. ];
  27. const EVENT_MAP = {
  28. 'sdk-places-query': queryReceiver
  29. };
  30. // Properties that need to be manually
  31. // copied into a nsINavHistoryQuery object
  32. const MANUAL_QUERY_PROPERTIES = [
  33. 'uri', 'folder', 'tags', 'url', 'folder'
  34. ];
  35. const PLACES_PROPERTIES = [
  36. 'uri', 'title', 'accessCount', 'time'
  37. ];
  38. function execute (queries, options) {
  39. let deferred = defer();
  40. let root = historyService
  41. .executeQueries(queries, queries.length, options).root;
  42. let items = collect([], root);
  43. deferred.resolve(items);
  44. return deferred.promise;
  45. }
  46. function collect (acc, node) {
  47. node.containerOpen = true;
  48. for (let i = 0; i < node.childCount; i++) {
  49. let child = node.getChild(i);
  50. acc.push(child);
  51. if (child.type === child.RESULT_TYPE_FOLDER) {
  52. let container = child.QueryInterface(Ci.nsINavHistoryContainerResultNode);
  53. collect(acc, container);
  54. }
  55. }
  56. node.containerOpen = false;
  57. return acc;
  58. }
  59. function query (queries, options) {
  60. queries = queries || [];
  61. options = options || {};
  62. let deferred = defer();
  63. let optionsObj, queryObjs;
  64. try {
  65. optionsObj = historyService.getNewQueryOptions();
  66. queryObjs = [].concat(queries).map(createQuery);
  67. if (!queryObjs.length) {
  68. queryObjs = [historyService.getNewQuery()];
  69. }
  70. safeMerge(optionsObj, options);
  71. } catch (e) {
  72. deferred.reject(e);
  73. return deferred.promise;
  74. }
  75. /*
  76. * Currently `places:` queries are not supported
  77. */
  78. optionsObj.excludeQueries = true;
  79. execute(queryObjs, optionsObj).then(function (results) {
  80. if (optionsObj.queryType === 0) {
  81. return results.map(normalize);
  82. } else if (optionsObj.queryType === 1) {
  83. // Formats query results into more standard
  84. // data structures for returning
  85. return all(results.map(({itemId}) =>
  86. send('sdk-places-bookmarks-get', { id: itemId })));
  87. }
  88. }).then(deferred.resolve, deferred.reject);
  89. return deferred.promise;
  90. }
  91. exports.query = query;
  92. function createQuery (query) {
  93. query = query || {};
  94. let queryObj = historyService.getNewQuery();
  95. safeMerge(queryObj, omit(query, MANUAL_QUERY_PROPERTIES));
  96. if (query.tags && Array.isArray(query.tags))
  97. queryObj.tags = query.tags;
  98. if (query.uri || query.url)
  99. queryObj.uri = newURI(query.uri || query.url);
  100. if (query.folder)
  101. queryObj.setFolders([query.folder], 1);
  102. return queryObj;
  103. }
  104. function queryReceiver (message) {
  105. let queries = message.data.queries || message.data.query;
  106. let options = message.data.options;
  107. let resData = {
  108. id: message.id,
  109. event: message.event
  110. };
  111. query(queries, options).then(results => {
  112. resData.data = results;
  113. respond(resData);
  114. }, reason => {
  115. resData.error = reason;
  116. respond(resData);
  117. });
  118. }
  119. /*
  120. * Converts a nsINavHistoryResultNode into a plain object
  121. *
  122. * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
  123. */
  124. function normalize (historyObj) {
  125. return PLACES_PROPERTIES.reduce((obj, prop) => {
  126. if (prop === 'uri')
  127. obj.url = historyObj.uri;
  128. else if (prop === 'time') {
  129. // Cast from microseconds to milliseconds
  130. obj.time = Math.floor(historyObj.time / 1000)
  131. } else if (prop === 'accessCount')
  132. obj.visitCount = historyObj[prop];
  133. else
  134. obj[prop] = historyObj[prop];
  135. return obj;
  136. }, {});
  137. }
  138. /*
  139. * Hook into host
  140. */
  141. let reqStream = filter(request, function (data) /sdk-places-query/.test(data.event));
  142. on(reqStream, 'data', function (e) {
  143. if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e);
  144. });
  145. function respond (data) {
  146. emit(response, 'data', data);
  147. }