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