Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
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/. */
5 "use strict";
7 module.metadata = {
8 "stability": "unstable",
9 "engines": {
10 "Firefox": "*"
11 }
12 };
14 /*
15 * Requiring hosts so they can subscribe to client messages
16 */
17 require('./host/host-bookmarks');
18 require('./host/host-tags');
19 require('./host/host-query');
21 const { Cc, Ci } = require('chrome');
22 const { Class } = require('../core/heritage');
23 const { send } = require('../addon/events');
24 const { defer, reject, all, resolve, promised } = require('../core/promise');
25 const { EventTarget } = require('../event/target');
26 const { emit } = require('../event/core');
27 const { identity, defer:async } = require('../lang/functional');
28 const { extend, merge } = require('../util/object');
29 const { fromIterator } = require('../util/array');
30 const {
31 constructTree, fetchItem, createQuery,
32 isRootGroup, createQueryOptions
33 } = require('./utils');
34 const {
35 bookmarkContract, groupContract, separatorContract
36 } = require('./contract');
37 const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
38 getService(Ci.nsINavBookmarksService);
40 /*
41 * Mapping of uncreated bookmarks with their created
42 * counterparts
43 */
44 const itemMap = new WeakMap();
46 /*
47 * Constant used by nsIHistoryQuery; 1 is a bookmark query
48 * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
49 */
50 const BOOKMARK_QUERY = 1;
52 /*
53 * Bookmark Item classes
54 */
56 const Bookmark = Class({
57 extends: [
58 bookmarkContract.properties(identity)
59 ],
60 initialize: function initialize (options) {
61 merge(this, bookmarkContract(extend(defaults, options)));
62 },
63 type: 'bookmark',
64 toString: function () '[object Bookmark]'
65 });
66 exports.Bookmark = Bookmark;
68 const Group = Class({
69 extends: [
70 groupContract.properties(identity)
71 ],
72 initialize: function initialize (options) {
73 // Don't validate if root group
74 if (isRootGroup(options))
75 merge(this, options);
76 else
77 merge(this, groupContract(extend(defaults, options)));
78 },
79 type: 'group',
80 toString: function () '[object Group]'
81 });
82 exports.Group = Group;
84 const Separator = Class({
85 extends: [
86 separatorContract.properties(identity)
87 ],
88 initialize: function initialize (options) {
89 merge(this, separatorContract(extend(defaults, options)));
90 },
91 type: 'separator',
92 toString: function () '[object Separator]'
93 });
94 exports.Separator = Separator;
96 /*
97 * Functions
98 */
100 function save (items, options) {
101 items = [].concat(items);
102 options = options || {};
103 let emitter = EventTarget();
104 let results = [];
105 let errors = [];
106 let root = constructTree(items);
107 let cache = new Map();
109 let isExplicitSave = item => !!~items.indexOf(item);
110 // `walk` returns an aggregate promise indicating the completion
111 // of the `commitItem` on each node, not whether or not that
112 // commit was successful
114 // Force this to be async, as if a ducktype fails validation,
115 // the promise implementation will fire an error event, which will
116 // not trigger the handler as it's not yet bound
117 //
118 // Can remove after `Promise.jsm` is implemented in Bug 881047,
119 // which will guarantee next tick execution
120 async(() => root.walk(preCommitItem).then(commitComplete))();
122 function preCommitItem ({value:item}) {
123 // Do nothing if tree root, default group (unsavable),
124 // or if it's a dependency and not explicitly saved (in the list
125 // of items to be saved), and not needed to be saved
126 if (item === null || // node is the tree root
127 isRootGroup(item) ||
128 (getId(item) && !isExplicitSave(item)))
129 return;
131 return promised(validate)(item)
132 .then(() => commitItem(item, options))
133 .then(data => construct(data, cache))
134 .then(savedItem => {
135 // If item was just created, make a map between
136 // the creation object and created object,
137 // so we can reference the item that doesn't have an id
138 if (!getId(item))
139 saveId(item, savedItem.id);
141 // Emit both the processed item, and original item
142 // so a mapping can be understood in handler
143 emit(emitter, 'data', savedItem, item);
145 // Push to results iff item was explicitly saved
146 if (isExplicitSave(item))
147 results[items.indexOf(item)] = savedItem;
148 }, reason => {
149 // Force reason to be a string for consistency
150 reason = reason + '';
151 // Emit both the reason, and original item
152 // so a mapping can be understood in handler
153 emit(emitter, 'error', reason + '', item);
154 // Store unsaved item in results list
155 results[items.indexOf(item)] = item;
156 errors.push(reason);
157 });
158 }
160 // Called when traversal of the node tree is completed and all
161 // items have been committed
162 function commitComplete () {
163 emit(emitter, 'end', results);
164 }
166 return emitter;
167 }
168 exports.save = save;
170 function search (queries, options) {
171 queries = [].concat(queries);
172 let emitter = EventTarget();
173 let cache = new Map();
174 let queryObjs = queries.map(createQuery.bind(null, BOOKMARK_QUERY));
175 let optionsObj = createQueryOptions(BOOKMARK_QUERY, options);
177 // Can remove after `Promise.jsm` is implemented in Bug 881047,
178 // which will guarantee next tick execution
179 async(() => {
180 send('sdk-places-query', { queries: queryObjs, options: optionsObj })
181 .then(handleQueryResponse);
182 })();
184 function handleQueryResponse (data) {
185 let deferreds = data.map(item => {
186 return construct(item, cache).then(bookmark => {
187 emit(emitter, 'data', bookmark);
188 return bookmark;
189 }, reason => {
190 emit(emitter, 'error', reason);
191 errors.push(reason);
192 });
193 });
195 all(deferreds).then(data => {
196 emit(emitter, 'end', data);
197 }, () => emit(emitter, 'end', []));
198 }
200 return emitter;
201 }
202 exports.search = search;
204 function remove (items) {
205 return [].concat(items).map(item => {
206 item.remove = true;
207 return item;
208 });
209 }
211 exports.remove = remove;
213 /*
214 * Internal Utilities
215 */
217 function commitItem (item, options) {
218 // Get the item's ID, or getId it's saved version if it exists
219 let id = getId(item);
220 let data = normalize(item);
221 let promise;
223 data.id = id;
225 if (!id) {
226 promise = send('sdk-places-bookmarks-create', data);
227 } else if (item.remove) {
228 promise = send('sdk-places-bookmarks-remove', { id: id });
229 } else {
230 promise = send('sdk-places-bookmarks-last-updated', {
231 id: id
232 }).then(function (updated) {
233 // If attempting to save an item that is not the
234 // latest snapshot of a bookmark item, execute
235 // the resolution function
236 if (updated !== item.updated && options.resolve)
237 return fetchItem(id)
238 .then(options.resolve.bind(null, data));
239 else
240 return data;
241 }).then(send.bind(null, 'sdk-places-bookmarks-save'));
242 }
244 return promise;
245 }
247 /*
248 * Turns a bookmark item into a plain object,
249 * converts `tags` from Set to Array, group instance to an id
250 */
251 function normalize (item) {
252 let data = merge({}, item);
253 // Circumvent prototype property of `type`
254 delete data.type;
255 data.type = item.type;
256 data.tags = [];
257 if (item.tags) {
258 data.tags = fromIterator(item.tags);
259 }
260 data.group = getId(data.group) || exports.UNSORTED.id;
262 return data;
263 }
265 /*
266 * Takes a data object and constructs a BookmarkItem instance
267 * of it, recursively generating parent instances as well.
268 *
269 * Pass in a `cache` Map to reuse instances of
270 * bookmark items to reduce overhead;
271 * The cache object is a map of id to a deferred with a
272 * promise that resolves to the bookmark item.
273 */
274 function construct (object, cache, forced) {
275 let item = instantiate(object);
276 let deferred = defer();
278 // Item could not be instantiated
279 if (!item)
280 return resolve(null);
282 // Return promise for item if found in the cache,
283 // and not `forced`. `forced` indicates that this is the construct
284 // call that should not read from cache, but should actually perform
285 // the construction, as it was set before several async calls
286 if (cache.has(item.id) && !forced)
287 return cache.get(item.id).promise;
288 else if (cache.has(item.id))
289 deferred = cache.get(item.id);
290 else
291 cache.set(item.id, deferred);
293 // When parent group is found in cache, use
294 // the same deferred value
295 if (item.group && cache.has(item.group)) {
296 cache.get(item.group).promise.then(group => {
297 item.group = group;
298 deferred.resolve(item);
299 });
301 // If not in the cache, and a root group, return
302 // the premade instance
303 } else if (rootGroups.get(item.group)) {
304 item.group = rootGroups.get(item.group);
305 deferred.resolve(item);
307 // If not in the cache or a root group, fetch the parent
308 } else {
309 cache.set(item.group, defer());
310 fetchItem(item.group).then(group => {
311 return construct(group, cache, true);
312 }).then(group => {
313 item.group = group;
314 deferred.resolve(item);
315 }, deferred.reject);
316 }
318 return deferred.promise;
319 }
321 function instantiate (object) {
322 if (object.type === 'bookmark')
323 return Bookmark(object);
324 if (object.type === 'group')
325 return Group(object);
326 if (object.type === 'separator')
327 return Separator(object);
328 return null;
329 }
331 /**
332 * Validates a bookmark item; will throw an error if ininvalid,
333 * to be used with `promised`. As bookmark items check on their class,
334 * this only checks ducktypes
335 */
336 function validate (object) {
337 if (!isDuckType(object)) return true;
338 let contract = object.type === 'bookmark' ? bookmarkContract :
339 object.type === 'group' ? groupContract :
340 object.type === 'separator' ? separatorContract :
341 null;
342 if (!contract) {
343 throw Error('No type specified');
344 }
346 // If object has a property set, and undefined,
347 // manually override with default as it'll fail otherwise
348 let withDefaults = Object.keys(defaults).reduce((obj, prop) => {
349 if (obj[prop] == null) obj[prop] = defaults[prop];
350 return obj;
351 }, extend(object));
353 contract(withDefaults);
354 }
356 function isDuckType (item) {
357 return !(item instanceof Bookmark) &&
358 !(item instanceof Group) &&
359 !(item instanceof Separator);
360 }
362 function saveId (unsaved, id) {
363 itemMap.set(unsaved, id);
364 }
366 // Fetches an item's ID from itself, or from the mapped items
367 function getId (item) {
368 return typeof item === 'number' ? item :
369 item ? item.id || itemMap.get(item) :
370 null;
371 }
373 /*
374 * Set up the default, root groups
375 */
377 let defaultGroupMap = {
378 MENU: bmsrv.bookmarksMenuFolder,
379 TOOLBAR: bmsrv.toolbarFolder,
380 UNSORTED: bmsrv.unfiledBookmarksFolder
381 };
383 let rootGroups = new Map();
385 for (let i in defaultGroupMap) {
386 let group = Object.freeze(Group({ title: i, id: defaultGroupMap[i] }));
387 rootGroups.set(defaultGroupMap[i], group);
388 exports[i] = group;
389 }
391 let defaults = {
392 group: exports.UNSORTED,
393 index: -1
394 };