Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 this.EXPORTED_SYMBOLS = [
7 "PlacesUtils"
8 , "PlacesAggregatedTransaction"
9 , "PlacesCreateFolderTransaction"
10 , "PlacesCreateBookmarkTransaction"
11 , "PlacesCreateSeparatorTransaction"
12 , "PlacesCreateLivemarkTransaction"
13 , "PlacesMoveItemTransaction"
14 , "PlacesRemoveItemTransaction"
15 , "PlacesEditItemTitleTransaction"
16 , "PlacesEditBookmarkURITransaction"
17 , "PlacesSetItemAnnotationTransaction"
18 , "PlacesSetPageAnnotationTransaction"
19 , "PlacesEditBookmarkKeywordTransaction"
20 , "PlacesEditBookmarkPostDataTransaction"
21 , "PlacesEditItemDateAddedTransaction"
22 , "PlacesEditItemLastModifiedTransaction"
23 , "PlacesSortFolderByNameTransaction"
24 , "PlacesTagURITransaction"
25 , "PlacesUntagURITransaction"
26 ];
28 const Ci = Components.interfaces;
29 const Cc = Components.classes;
30 const Cr = Components.results;
31 const Cu = Components.utils;
33 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
35 XPCOMUtils.defineLazyModuleGetter(this, "Services",
36 "resource://gre/modules/Services.jsm");
38 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
39 "resource://gre/modules/NetUtil.jsm");
41 XPCOMUtils.defineLazyModuleGetter(this, "Task",
42 "resource://gre/modules/Task.jsm");
44 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
45 "resource://gre/modules/Promise.jsm");
47 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
48 "resource://gre/modules/Deprecated.jsm");
50 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
51 "resource://gre/modules/BookmarkJSONUtils.jsm");
53 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
54 "resource://gre/modules/PlacesBackups.jsm");
56 // The minimum amount of transactions before starting a batch. Usually we do
57 // do incremental updates, a batch will cause views to completely
58 // refresh instead.
59 const MIN_TRANSACTIONS_FOR_BATCH = 5;
61 #ifdef XP_MACOSX
62 // On Mac OSX, the transferable system converts "\r\n" to "\n\n", where we
63 // really just want "\n".
64 const NEWLINE= "\n";
65 #else
66 // On other platforms, the transferable system converts "\r\n" to "\n".
67 const NEWLINE = "\r\n";
68 #endif
70 function QI_node(aNode, aIID) {
71 var result = null;
72 try {
73 result = aNode.QueryInterface(aIID);
74 }
75 catch (e) {
76 }
77 return result;
78 }
79 function asContainer(aNode) QI_node(aNode, Ci.nsINavHistoryContainerResultNode);
80 function asQuery(aNode) QI_node(aNode, Ci.nsINavHistoryQueryResultNode);
82 this.PlacesUtils = {
83 // Place entries that are containers, e.g. bookmark folders or queries.
84 TYPE_X_MOZ_PLACE_CONTAINER: "text/x-moz-place-container",
85 // Place entries that are bookmark separators.
86 TYPE_X_MOZ_PLACE_SEPARATOR: "text/x-moz-place-separator",
87 // Place entries that are not containers or separators
88 TYPE_X_MOZ_PLACE: "text/x-moz-place",
89 // Place entries in shortcut url format (url\ntitle)
90 TYPE_X_MOZ_URL: "text/x-moz-url",
91 // Place entries formatted as HTML anchors
92 TYPE_HTML: "text/html",
93 // Place entries as raw URL text
94 TYPE_UNICODE: "text/unicode",
95 // Used to track the action that populated the clipboard.
96 TYPE_X_MOZ_PLACE_ACTION: "text/x-moz-place-action",
98 EXCLUDE_FROM_BACKUP_ANNO: "places/excludeFromBackup",
99 LMANNO_FEEDURI: "livemark/feedURI",
100 LMANNO_SITEURI: "livemark/siteURI",
101 POST_DATA_ANNO: "bookmarkProperties/POSTData",
102 READ_ONLY_ANNO: "placesInternal/READ_ONLY",
103 CHARSET_ANNO: "URIProperties/characterSet",
105 TOPIC_SHUTDOWN: "places-shutdown",
106 TOPIC_INIT_COMPLETE: "places-init-complete",
107 TOPIC_DATABASE_LOCKED: "places-database-locked",
108 TOPIC_EXPIRATION_FINISHED: "places-expiration-finished",
109 TOPIC_FEEDBACK_UPDATED: "places-autocomplete-feedback-updated",
110 TOPIC_FAVICONS_EXPIRED: "places-favicons-expired",
111 TOPIC_VACUUM_STARTING: "places-vacuum-starting",
112 TOPIC_BOOKMARKS_RESTORE_BEGIN: "bookmarks-restore-begin",
113 TOPIC_BOOKMARKS_RESTORE_SUCCESS: "bookmarks-restore-success",
114 TOPIC_BOOKMARKS_RESTORE_FAILED: "bookmarks-restore-failed",
116 asContainer: function(aNode) asContainer(aNode),
117 asQuery: function(aNode) asQuery(aNode),
119 endl: NEWLINE,
121 /**
122 * Makes a URI from a spec.
123 * @param aSpec
124 * The string spec of the URI
125 * @returns A URI object for the spec.
126 */
127 _uri: function PU__uri(aSpec) {
128 return NetUtil.newURI(aSpec);
129 },
131 /**
132 * Wraps a string in a nsISupportsString wrapper.
133 * @param aString
134 * The string to wrap.
135 * @returns A nsISupportsString object containing a string.
136 */
137 toISupportsString: function PU_toISupportsString(aString) {
138 let s = Cc["@mozilla.org/supports-string;1"].
139 createInstance(Ci.nsISupportsString);
140 s.data = aString;
141 return s;
142 },
144 getFormattedString: function PU_getFormattedString(key, params) {
145 return bundle.formatStringFromName(key, params, params.length);
146 },
148 getString: function PU_getString(key) {
149 return bundle.GetStringFromName(key);
150 },
152 /**
153 * Determines whether or not a ResultNode is a Bookmark folder.
154 * @param aNode
155 * A result node
156 * @returns true if the node is a Bookmark folder, false otherwise
157 */
158 nodeIsFolder: function PU_nodeIsFolder(aNode) {
159 return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
160 aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT);
161 },
163 /**
164 * Determines whether or not a ResultNode represents a bookmarked URI.
165 * @param aNode
166 * A result node
167 * @returns true if the node represents a bookmarked URI, false otherwise
168 */
169 nodeIsBookmark: function PU_nodeIsBookmark(aNode) {
170 return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI &&
171 aNode.itemId != -1;
172 },
174 /**
175 * Determines whether or not a ResultNode is a Bookmark separator.
176 * @param aNode
177 * A result node
178 * @returns true if the node is a Bookmark separator, false otherwise
179 */
180 nodeIsSeparator: function PU_nodeIsSeparator(aNode) {
181 return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR;
182 },
184 /**
185 * Determines whether or not a ResultNode is a URL item.
186 * @param aNode
187 * A result node
188 * @returns true if the node is a URL item, false otherwise
189 */
190 nodeIsURI: function PU_nodeIsURI(aNode) {
191 return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
192 },
194 /**
195 * Determines whether or not a ResultNode is a Query item.
196 * @param aNode
197 * A result node
198 * @returns true if the node is a Query item, false otherwise
199 */
200 nodeIsQuery: function PU_nodeIsQuery(aNode) {
201 return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
202 },
204 /**
205 * Generator for a node's ancestors.
206 * @param aNode
207 * A result node
208 */
209 nodeAncestors: function PU_nodeAncestors(aNode) {
210 let node = aNode.parent;
211 while (node) {
212 yield node;
213 node = node.parent;
214 }
215 },
217 /**
218 * Cache array of read-only item IDs.
219 *
220 * The first time this property is called:
221 * - the cache is filled with all ids with the RO annotation
222 * - an annotation observer is added
223 * - a shutdown observer is added
224 *
225 * When the annotation observer detects annotations added or
226 * removed that are the RO annotation name, it adds/removes
227 * the ids from the cache.
228 *
229 * At shutdown, the annotation and shutdown observers are removed.
230 */
231 get _readOnly() {
232 // Add annotations observer.
233 this.annotations.addObserver(this, false);
234 this.registerShutdownFunction(function () {
235 this.annotations.removeObserver(this);
236 });
238 var readOnly = this.annotations.getItemsWithAnnotation(this.READ_ONLY_ANNO);
239 this.__defineGetter__("_readOnly", function() readOnly);
240 return this._readOnly;
241 },
243 QueryInterface: XPCOMUtils.generateQI([
244 Ci.nsIAnnotationObserver
245 , Ci.nsIObserver
246 , Ci.nsITransactionListener
247 ]),
249 _shutdownFunctions: [],
250 registerShutdownFunction: function PU_registerShutdownFunction(aFunc)
251 {
252 // If this is the first registered function, add the shutdown observer.
253 if (this._shutdownFunctions.length == 0) {
254 Services.obs.addObserver(this, this.TOPIC_SHUTDOWN, false);
255 }
256 this._shutdownFunctions.push(aFunc);
257 },
259 //////////////////////////////////////////////////////////////////////////////
260 //// nsIObserver
261 observe: function PU_observe(aSubject, aTopic, aData)
262 {
263 switch (aTopic) {
264 case this.TOPIC_SHUTDOWN:
265 Services.obs.removeObserver(this, this.TOPIC_SHUTDOWN);
266 while (this._shutdownFunctions.length > 0) {
267 this._shutdownFunctions.shift().apply(this);
268 }
269 if (this._bookmarksServiceObserversQueue.length > 0) {
270 // Since we are shutting down, there's no reason to add the observers.
271 this._bookmarksServiceObserversQueue.length = 0;
272 }
273 break;
274 case "bookmarks-service-ready":
275 this._bookmarksServiceReady = true;
276 while (this._bookmarksServiceObserversQueue.length > 0) {
277 let observerInfo = this._bookmarksServiceObserversQueue.shift();
278 this.bookmarks.addObserver(observerInfo.observer, observerInfo.weak);
279 }
280 break;
281 }
282 },
284 //////////////////////////////////////////////////////////////////////////////
285 //// nsIAnnotationObserver
287 onItemAnnotationSet: function PU_onItemAnnotationSet(aItemId, aAnnotationName)
288 {
289 if (aAnnotationName == this.READ_ONLY_ANNO &&
290 this._readOnly.indexOf(aItemId) == -1)
291 this._readOnly.push(aItemId);
292 },
294 onItemAnnotationRemoved:
295 function PU_onItemAnnotationRemoved(aItemId, aAnnotationName)
296 {
297 var index = this._readOnly.indexOf(aItemId);
298 if (aAnnotationName == this.READ_ONLY_ANNO && index > -1)
299 delete this._readOnly[index];
300 },
302 onPageAnnotationSet: function() {},
303 onPageAnnotationRemoved: function() {},
306 //////////////////////////////////////////////////////////////////////////////
307 //// nsITransactionListener
309 didDo: function PU_didDo(aManager, aTransaction, aDoResult)
310 {
311 updateCommandsOnActiveWindow();
312 },
314 didUndo: function PU_didUndo(aManager, aTransaction, aUndoResult)
315 {
316 updateCommandsOnActiveWindow();
317 },
319 didRedo: function PU_didRedo(aManager, aTransaction, aRedoResult)
320 {
321 updateCommandsOnActiveWindow();
322 },
324 didBeginBatch: function PU_didBeginBatch(aManager, aResult)
325 {
326 // A no-op transaction is pushed to the stack, in order to make safe and
327 // easy to implement "Undo" an unknown number of transactions (including 0),
328 // "above" beginBatch and endBatch. Otherwise,implementing Undo that way
329 // head to dataloss: for example, if no changes were done in the
330 // edit-item panel, the last transaction on the undo stack would be the
331 // initial createItem transaction, or even worse, the batched editing of
332 // some other item.
333 // DO NOT MOVE this to the window scope, that would leak (bug 490068)!
334 this.transactionManager.doTransaction({ doTransaction: function() {},
335 undoTransaction: function() {},
336 redoTransaction: function() {},
337 isTransient: false,
338 merge: function() { return false; }
339 });
340 },
342 willDo: function PU_willDo() {},
343 willUndo: function PU_willUndo() {},
344 willRedo: function PU_willRedo() {},
345 willBeginBatch: function PU_willBeginBatch() {},
346 willEndBatch: function PU_willEndBatch() {},
347 didEndBatch: function PU_didEndBatch() {},
348 willMerge: function PU_willMerge() {},
349 didMerge: function PU_didMerge() {},
352 /**
353 * Determines if a node is read only (children cannot be inserted, sometimes
354 * they cannot be removed depending on the circumstance)
355 * @param aNode
356 * A result node
357 * @returns true if the node is readonly, false otherwise
358 */
359 nodeIsReadOnly: function PU_nodeIsReadOnly(aNode) {
360 let itemId = aNode.itemId;
361 if (itemId != -1) {
362 return this._readOnly.indexOf(itemId) != -1;
363 }
365 if (this.nodeIsQuery(aNode) &&
366 asQuery(aNode).queryOptions.resultType !=
367 Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS)
368 return aNode.childrenReadOnly;
369 return false;
370 },
372 /**
373 * Determines whether or not a ResultNode is a host container.
374 * @param aNode
375 * A result node
376 * @returns true if the node is a host container, false otherwise
377 */
378 nodeIsHost: function PU_nodeIsHost(aNode) {
379 return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
380 aNode.parent &&
381 asQuery(aNode.parent).queryOptions.resultType ==
382 Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY;
383 },
385 /**
386 * Determines whether or not a ResultNode is a day container.
387 * @param node
388 * A NavHistoryResultNode
389 * @returns true if the node is a day container, false otherwise
390 */
391 nodeIsDay: function PU_nodeIsDay(aNode) {
392 var resultType;
393 return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
394 aNode.parent &&
395 ((resultType = asQuery(aNode.parent).queryOptions.resultType) ==
396 Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
397 resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY);
398 },
400 /**
401 * Determines whether or not a result-node is a tag container.
402 * @param aNode
403 * A result-node
404 * @returns true if the node is a tag container, false otherwise
405 */
406 nodeIsTagQuery: function PU_nodeIsTagQuery(aNode) {
407 return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
408 asQuery(aNode).queryOptions.resultType ==
409 Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS;
410 },
412 /**
413 * Determines whether or not a ResultNode is a container.
414 * @param aNode
415 * A result node
416 * @returns true if the node is a container item, false otherwise
417 */
418 containerTypes: [Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
419 Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
420 Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY],
421 nodeIsContainer: function PU_nodeIsContainer(aNode) {
422 return this.containerTypes.indexOf(aNode.type) != -1;
423 },
425 /**
426 * Determines whether or not a ResultNode is an history related container.
427 * @param node
428 * A result node
429 * @returns true if the node is an history related container, false otherwise
430 */
431 nodeIsHistoryContainer: function PU_nodeIsHistoryContainer(aNode) {
432 var resultType;
433 return this.nodeIsQuery(aNode) &&
434 ((resultType = asQuery(aNode).queryOptions.resultType) ==
435 Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY ||
436 resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
437 resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY ||
438 this.nodeIsDay(aNode) ||
439 this.nodeIsHost(aNode));
440 },
442 /**
443 * Determines whether or not a node is a readonly folder.
444 * @param aNode
445 * The node to test.
446 * @returns true if the node is a readonly folder.
447 */
448 isReadonlyFolder: function(aNode) {
449 return this.nodeIsFolder(aNode) &&
450 this._readOnly.indexOf(asQuery(aNode).folderItemId) != -1;
451 },
453 /**
454 * Gets the concrete item-id for the given node. Generally, this is just
455 * node.itemId, but for folder-shortcuts that's node.folderItemId.
456 */
457 getConcreteItemId: function PU_getConcreteItemId(aNode) {
458 if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT)
459 return asQuery(aNode).folderItemId;
460 else if (PlacesUtils.nodeIsTagQuery(aNode)) {
461 // RESULTS_AS_TAG_CONTENTS queries are similar to folder shortcuts
462 // so we can still get the concrete itemId for them.
463 var queries = aNode.getQueries();
464 var folders = queries[0].getFolders();
465 return folders[0];
466 }
467 return aNode.itemId;
468 },
470 /**
471 * String-wraps a result node according to the rules of the specified
472 * content type.
473 * @param aNode
474 * The Result node to wrap (serialize)
475 * @param aType
476 * The content type to serialize as
477 * @param [optional] aOverrideURI
478 * Used instead of the node's URI if provided.
479 * This is useful for wrapping a container as TYPE_X_MOZ_URL,
480 * TYPE_HTML or TYPE_UNICODE.
481 * @return A string serialization of the node
482 */
483 wrapNode: function PU_wrapNode(aNode, aType, aOverrideURI) {
484 // when wrapping a node, we want all the items, even if the original
485 // query options are excluding them.
486 // this can happen when copying from the left hand pane of the bookmarks
487 // organizer
488 // @return [node, shouldClose]
489 function convertNode(cNode) {
490 if (PlacesUtils.nodeIsFolder(cNode) &&
491 cNode.type != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT &&
492 asQuery(cNode).queryOptions.excludeItems) {
493 return [PlacesUtils.getFolderContents(cNode.itemId, false, true).root, true];
494 }
496 // If we didn't create our own query, do not alter the node's open state.
497 return [cNode, false];
498 }
500 function gatherLivemarkUrl(aNode) {
501 try {
502 return PlacesUtils.annotations
503 .getItemAnnotation(aNode.itemId,
504 PlacesUtils.LMANNO_SITEURI);
505 } catch (ex) {
506 return PlacesUtils.annotations
507 .getItemAnnotation(aNode.itemId,
508 PlacesUtils.LMANNO_FEEDURI);
509 }
510 }
512 function isLivemark(aNode) {
513 return PlacesUtils.nodeIsFolder(aNode) &&
514 PlacesUtils.annotations
515 .itemHasAnnotation(aNode.itemId,
516 PlacesUtils.LMANNO_FEEDURI);
517 }
519 switch (aType) {
520 case this.TYPE_X_MOZ_PLACE:
521 case this.TYPE_X_MOZ_PLACE_SEPARATOR:
522 case this.TYPE_X_MOZ_PLACE_CONTAINER: {
523 let writer = {
524 value: "",
525 write: function PU_wrapNode__write(aStr, aLen) {
526 this.value += aStr;
527 }
528 };
530 let [node, shouldClose] = convertNode(aNode);
531 this._serializeNodeAsJSONToOutputStream(node, writer);
532 if (shouldClose)
533 node.containerOpen = false;
535 return writer.value;
536 }
537 case this.TYPE_X_MOZ_URL: {
538 function gatherDataUrl(bNode) {
539 if (isLivemark(bNode)) {
540 return gatherLivemarkUrl(bNode) + NEWLINE + bNode.title;
541 }
543 if (PlacesUtils.nodeIsURI(bNode))
544 return (aOverrideURI || bNode.uri) + NEWLINE + bNode.title;
545 // ignore containers and separators - items without valid URIs
546 return "";
547 }
549 let [node, shouldClose] = convertNode(aNode);
550 let dataUrl = gatherDataUrl(node);
551 if (shouldClose)
552 node.containerOpen = false;
554 return dataUrl;
555 }
556 case this.TYPE_HTML: {
557 function gatherDataHtml(bNode) {
558 function htmlEscape(s) {
559 s = s.replace(/&/g, "&");
560 s = s.replace(/>/g, ">");
561 s = s.replace(/</g, "<");
562 s = s.replace(/"/g, """);
563 s = s.replace(/'/g, "'");
564 return s;
565 }
566 // escape out potential HTML in the title
567 let escapedTitle = bNode.title ? htmlEscape(bNode.title) : "";
569 if (isLivemark(bNode)) {
570 return "<A HREF=\"" + gatherLivemarkUrl(bNode) + "\">" + escapedTitle + "</A>" + NEWLINE;
571 }
573 if (PlacesUtils.nodeIsContainer(bNode)) {
574 asContainer(bNode);
575 let wasOpen = bNode.containerOpen;
576 if (!wasOpen)
577 bNode.containerOpen = true;
579 let childString = "<DL><DT>" + escapedTitle + "</DT>" + NEWLINE;
580 let cc = bNode.childCount;
581 for (let i = 0; i < cc; ++i)
582 childString += "<DD>"
583 + NEWLINE
584 + gatherDataHtml(bNode.getChild(i))
585 + "</DD>"
586 + NEWLINE;
587 bNode.containerOpen = wasOpen;
588 return childString + "</DL>" + NEWLINE;
589 }
590 if (PlacesUtils.nodeIsURI(bNode))
591 return "<A HREF=\"" + bNode.uri + "\">" + escapedTitle + "</A>" + NEWLINE;
592 if (PlacesUtils.nodeIsSeparator(bNode))
593 return "<HR>" + NEWLINE;
594 return "";
595 }
597 let [node, shouldClose] = convertNode(aNode);
598 let dataHtml = gatherDataHtml(node);
599 if (shouldClose)
600 node.containerOpen = false;
602 return dataHtml;
603 }
604 }
606 // Otherwise, we wrap as TYPE_UNICODE.
607 function gatherDataText(bNode) {
608 if (isLivemark(bNode)) {
609 return gatherLivemarkUrl(bNode);
610 }
612 if (PlacesUtils.nodeIsContainer(bNode)) {
613 asContainer(bNode);
614 let wasOpen = bNode.containerOpen;
615 if (!wasOpen)
616 bNode.containerOpen = true;
618 let childString = bNode.title + NEWLINE;
619 let cc = bNode.childCount;
620 for (let i = 0; i < cc; ++i) {
621 let child = bNode.getChild(i);
622 let suffix = i < (cc - 1) ? NEWLINE : "";
623 childString += gatherDataText(child) + suffix;
624 }
625 bNode.containerOpen = wasOpen;
626 return childString;
627 }
628 if (PlacesUtils.nodeIsURI(bNode))
629 return (aOverrideURI || bNode.uri);
630 if (PlacesUtils.nodeIsSeparator(bNode))
631 return "--------------------";
632 return "";
633 }
635 let [node, shouldClose] = convertNode(aNode);
636 let dataText = gatherDataText(node);
637 // Convert node could pass an open container node.
638 if (shouldClose)
639 node.containerOpen = false;
641 return dataText;
642 },
644 /**
645 * Unwraps data from the Clipboard or the current Drag Session.
646 * @param blob
647 * A blob (string) of data, in some format we potentially know how
648 * to parse.
649 * @param type
650 * The content type of the blob.
651 * @returns An array of objects representing each item contained by the source.
652 */
653 unwrapNodes: function PU_unwrapNodes(blob, type) {
654 // We split on "\n" because the transferable system converts "\r\n" to "\n"
655 var nodes = [];
656 switch(type) {
657 case this.TYPE_X_MOZ_PLACE:
658 case this.TYPE_X_MOZ_PLACE_SEPARATOR:
659 case this.TYPE_X_MOZ_PLACE_CONTAINER:
660 nodes = JSON.parse("[" + blob + "]");
661 break;
662 case this.TYPE_X_MOZ_URL:
663 var parts = blob.split("\n");
664 // data in this type has 2 parts per entry, so if there are fewer
665 // than 2 parts left, the blob is malformed and we should stop
666 // but drag and drop of files from the shell has parts.length = 1
667 if (parts.length != 1 && parts.length % 2)
668 break;
669 for (var i = 0; i < parts.length; i=i+2) {
670 var uriString = parts[i];
671 var titleString = "";
672 if (parts.length > i+1)
673 titleString = parts[i+1];
674 else {
675 // for drag and drop of files, try to use the leafName as title
676 try {
677 titleString = this._uri(uriString).QueryInterface(Ci.nsIURL)
678 .fileName;
679 }
680 catch (e) {}
681 }
682 // note: this._uri() will throw if uriString is not a valid URI
683 if (this._uri(uriString)) {
684 nodes.push({ uri: uriString,
685 title: titleString ? titleString : uriString ,
686 type: this.TYPE_X_MOZ_URL });
687 }
688 }
689 break;
690 case this.TYPE_UNICODE:
691 var parts = blob.split("\n");
692 for (var i = 0; i < parts.length; i++) {
693 var uriString = parts[i];
694 // text/uri-list is converted to TYPE_UNICODE but it could contain
695 // comments line prepended by #, we should skip them
696 if (uriString.substr(0, 1) == '\x23')
697 continue;
698 // note: this._uri() will throw if uriString is not a valid URI
699 if (uriString != "" && this._uri(uriString))
700 nodes.push({ uri: uriString,
701 title: uriString,
702 type: this.TYPE_X_MOZ_URL });
703 }
704 break;
705 default:
706 throw Cr.NS_ERROR_INVALID_ARG;
707 }
708 return nodes;
709 },
711 /**
712 * Generates a nsINavHistoryResult for the contents of a folder.
713 * @param folderId
714 * The folder to open
715 * @param [optional] excludeItems
716 * True to hide all items (individual bookmarks). This is used on
717 * the left places pane so you just get a folder hierarchy.
718 * @param [optional] expandQueries
719 * True to make query items expand as new containers. For managing,
720 * you want this to be false, for menus and such, you want this to
721 * be true.
722 * @returns A nsINavHistoryResult containing the contents of the
723 * folder. The result.root is guaranteed to be open.
724 */
725 getFolderContents:
726 function PU_getFolderContents(aFolderId, aExcludeItems, aExpandQueries) {
727 var query = this.history.getNewQuery();
728 query.setFolders([aFolderId], 1);
729 var options = this.history.getNewQueryOptions();
730 options.excludeItems = aExcludeItems;
731 options.expandQueries = aExpandQueries;
733 var result = this.history.executeQuery(query, options);
734 result.root.containerOpen = true;
735 return result;
736 },
738 /**
739 * Fetch all annotations for a URI, including all properties of each
740 * annotation which would be required to recreate it.
741 * @param aURI
742 * The URI for which annotations are to be retrieved.
743 * @return Array of objects, each containing the following properties:
744 * name, flags, expires, value
745 */
746 getAnnotationsForURI: function PU_getAnnotationsForURI(aURI) {
747 var annosvc = this.annotations;
748 var annos = [], val = null;
749 var annoNames = annosvc.getPageAnnotationNames(aURI);
750 for (var i = 0; i < annoNames.length; i++) {
751 var flags = {}, exp = {}, storageType = {};
752 annosvc.getPageAnnotationInfo(aURI, annoNames[i], flags, exp, storageType);
753 val = annosvc.getPageAnnotation(aURI, annoNames[i]);
754 annos.push({name: annoNames[i],
755 flags: flags.value,
756 expires: exp.value,
757 value: val});
758 }
759 return annos;
760 },
762 /**
763 * Fetch all annotations for an item, including all properties of each
764 * annotation which would be required to recreate it.
765 * @param aItemId
766 * The identifier of the itme for which annotations are to be
767 * retrieved.
768 * @return Array of objects, each containing the following properties:
769 * name, flags, expires, mimeType, type, value
770 */
771 getAnnotationsForItem: function PU_getAnnotationsForItem(aItemId) {
772 var annosvc = this.annotations;
773 var annos = [], val = null;
774 var annoNames = annosvc.getItemAnnotationNames(aItemId);
775 for (var i = 0; i < annoNames.length; i++) {
776 var flags = {}, exp = {}, storageType = {};
777 annosvc.getItemAnnotationInfo(aItemId, annoNames[i], flags, exp, storageType);
778 val = annosvc.getItemAnnotation(aItemId, annoNames[i]);
779 annos.push({name: annoNames[i],
780 flags: flags.value,
781 expires: exp.value,
782 value: val});
783 }
784 return annos;
785 },
787 /**
788 * Annotate a URI with a batch of annotations.
789 * @param aURI
790 * The URI for which annotations are to be set.
791 * @param aAnnotations
792 * Array of objects, each containing the following properties:
793 * name, flags, expires.
794 * If the value for an annotation is not set it will be removed.
795 */
796 setAnnotationsForURI: function PU_setAnnotationsForURI(aURI, aAnnos) {
797 var annosvc = this.annotations;
798 aAnnos.forEach(function(anno) {
799 if (anno.value === undefined || anno.value === null) {
800 annosvc.removePageAnnotation(aURI, anno.name);
801 }
802 else {
803 let flags = ("flags" in anno) ? anno.flags : 0;
804 let expires = ("expires" in anno) ?
805 anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
806 annosvc.setPageAnnotation(aURI, anno.name, anno.value, flags, expires);
807 }
808 });
809 },
811 /**
812 * Annotate an item with a batch of annotations.
813 * @param aItemId
814 * The identifier of the item for which annotations are to be set
815 * @param aAnnotations
816 * Array of objects, each containing the following properties:
817 * name, flags, expires.
818 * If the value for an annotation is not set it will be removed.
819 */
820 setAnnotationsForItem: function PU_setAnnotationsForItem(aItemId, aAnnos) {
821 var annosvc = this.annotations;
823 aAnnos.forEach(function(anno) {
824 if (anno.value === undefined || anno.value === null) {
825 annosvc.removeItemAnnotation(aItemId, anno.name);
826 }
827 else {
828 let flags = ("flags" in anno) ? anno.flags : 0;
829 let expires = ("expires" in anno) ?
830 anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
831 annosvc.setItemAnnotation(aItemId, anno.name, anno.value, flags,
832 expires);
833 }
834 });
835 },
837 // Identifier getters for special folders.
838 // You should use these everywhere PlacesUtils is available to avoid XPCOM
839 // traversal just to get roots' ids.
840 get placesRootId() {
841 delete this.placesRootId;
842 return this.placesRootId = this.bookmarks.placesRoot;
843 },
845 get bookmarksMenuFolderId() {
846 delete this.bookmarksMenuFolderId;
847 return this.bookmarksMenuFolderId = this.bookmarks.bookmarksMenuFolder;
848 },
850 get toolbarFolderId() {
851 delete this.toolbarFolderId;
852 return this.toolbarFolderId = this.bookmarks.toolbarFolder;
853 },
855 get tagsFolderId() {
856 delete this.tagsFolderId;
857 return this.tagsFolderId = this.bookmarks.tagsFolder;
858 },
860 get unfiledBookmarksFolderId() {
861 delete this.unfiledBookmarksFolderId;
862 return this.unfiledBookmarksFolderId = this.bookmarks.unfiledBookmarksFolder;
863 },
865 /**
866 * Checks if aItemId is a root.
867 *
868 * @param aItemId
869 * item id to look for.
870 * @returns true if aItemId is a root, false otherwise.
871 */
872 isRootItem: function PU_isRootItem(aItemId) {
873 return aItemId == PlacesUtils.bookmarksMenuFolderId ||
874 aItemId == PlacesUtils.toolbarFolderId ||
875 aItemId == PlacesUtils.unfiledBookmarksFolderId ||
876 aItemId == PlacesUtils.tagsFolderId ||
877 aItemId == PlacesUtils.placesRootId;
878 },
880 /**
881 * Set the POST data associated with a bookmark, if any.
882 * Used by POST keywords.
883 * @param aBookmarkId
884 * @returns string of POST data
885 */
886 setPostDataForBookmark: function PU_setPostDataForBookmark(aBookmarkId, aPostData) {
887 const annos = this.annotations;
888 if (aPostData)
889 annos.setItemAnnotation(aBookmarkId, this.POST_DATA_ANNO, aPostData,
890 0, Ci.nsIAnnotationService.EXPIRE_NEVER);
891 else if (annos.itemHasAnnotation(aBookmarkId, this.POST_DATA_ANNO))
892 annos.removeItemAnnotation(aBookmarkId, this.POST_DATA_ANNO);
893 },
895 /**
896 * Get the POST data associated with a bookmark, if any.
897 * @param aBookmarkId
898 * @returns string of POST data if set for aBookmarkId. null otherwise.
899 */
900 getPostDataForBookmark: function PU_getPostDataForBookmark(aBookmarkId) {
901 const annos = this.annotations;
902 if (annos.itemHasAnnotation(aBookmarkId, this.POST_DATA_ANNO))
903 return annos.getItemAnnotation(aBookmarkId, this.POST_DATA_ANNO);
905 return null;
906 },
908 /**
909 * Get the URI (and any associated POST data) for a given keyword.
910 * @param aKeyword string keyword
911 * @returns an array containing a string URL and a string of POST data
912 */
913 getURLAndPostDataForKeyword: function PU_getURLAndPostDataForKeyword(aKeyword) {
914 var url = null, postdata = null;
915 try {
916 var uri = this.bookmarks.getURIForKeyword(aKeyword);
917 if (uri) {
918 url = uri.spec;
919 var bookmarks = this.bookmarks.getBookmarkIdsForURI(uri);
920 for (let i = 0; i < bookmarks.length; i++) {
921 var bookmark = bookmarks[i];
922 var kw = this.bookmarks.getKeywordForBookmark(bookmark);
923 if (kw == aKeyword) {
924 postdata = this.getPostDataForBookmark(bookmark);
925 break;
926 }
927 }
928 }
929 } catch(ex) {}
930 return [url, postdata];
931 },
933 /**
934 * Get all bookmarks for a URL, excluding items under tags.
935 */
936 getBookmarksForURI:
937 function PU_getBookmarksForURI(aURI) {
938 var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI);
940 // filter the ids list
941 return bmkIds.filter(function(aID) {
942 var parentId = this.bookmarks.getFolderIdForItem(aID);
943 var grandparentId = this.bookmarks.getFolderIdForItem(parentId);
944 // item under a tag container
945 if (grandparentId == this.tagsFolderId)
946 return false;
947 return true;
948 }, this);
949 },
951 /**
952 * Get the most recently added/modified bookmark for a URL, excluding items
953 * under tags.
954 *
955 * @param aURI
956 * nsIURI of the page we will look for.
957 * @returns itemId of the found bookmark, or -1 if nothing is found.
958 */
959 getMostRecentBookmarkForURI:
960 function PU_getMostRecentBookmarkForURI(aURI) {
961 var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI);
962 for (var i = 0; i < bmkIds.length; i++) {
963 // Find the first folder which isn't a tag container
964 var itemId = bmkIds[i];
965 var parentId = this.bookmarks.getFolderIdForItem(itemId);
966 // Optimization: if this is a direct child of a root we don't need to
967 // check if its grandparent is a tag.
968 if (parentId == this.unfiledBookmarksFolderId ||
969 parentId == this.toolbarFolderId ||
970 parentId == this.bookmarksMenuFolderId)
971 return itemId;
973 var grandparentId = this.bookmarks.getFolderIdForItem(parentId);
974 if (grandparentId != this.tagsFolderId)
975 return itemId;
976 }
977 return -1;
978 },
980 /**
981 * Returns a nsNavHistoryContainerResultNode with forced excludeItems and
982 * expandQueries.
983 * @param aNode
984 * The node to convert
985 * @param [optional] excludeItems
986 * True to hide all items (individual bookmarks). This is used on
987 * the left places pane so you just get a folder hierarchy.
988 * @param [optional] expandQueries
989 * True to make query items expand as new containers. For managing,
990 * you want this to be false, for menus and such, you want this to
991 * be true.
992 * @returns A nsINavHistoryContainerResultNode containing the unfiltered
993 * contents of the container.
994 * @note The returned container node could be open or closed, we don't
995 * guarantee its status.
996 */
997 getContainerNodeWithOptions:
998 function PU_getContainerNodeWithOptions(aNode, aExcludeItems, aExpandQueries) {
999 if (!this.nodeIsContainer(aNode))
1000 throw Cr.NS_ERROR_INVALID_ARG;
1002 // excludeItems is inherited by child containers in an excludeItems view.
1003 var excludeItems = asQuery(aNode).queryOptions.excludeItems ||
1004 asQuery(aNode.parentResult.root).queryOptions.excludeItems;
1005 // expandQueries is inherited by child containers in an expandQueries view.
1006 var expandQueries = asQuery(aNode).queryOptions.expandQueries &&
1007 asQuery(aNode.parentResult.root).queryOptions.expandQueries;
1009 // If our options are exactly what we expect, directly return the node.
1010 if (excludeItems == aExcludeItems && expandQueries == aExpandQueries)
1011 return aNode;
1013 // Otherwise, get contents manually.
1014 var queries = {}, options = {};
1015 this.history.queryStringToQueries(aNode.uri, queries, {}, options);
1016 options.value.excludeItems = aExcludeItems;
1017 options.value.expandQueries = aExpandQueries;
1018 return this.history.executeQueries(queries.value,
1019 queries.value.length,
1020 options.value).root;
1021 },
1023 /**
1024 * Returns true if a container has uri nodes in its first level.
1025 * Has better performance than (getURLsForContainerNode(node).length > 0).
1026 * @param aNode
1027 * The container node to search through.
1028 * @returns true if the node contains uri nodes, false otherwise.
1029 */
1030 hasChildURIs: function PU_hasChildURIs(aNode) {
1031 if (!this.nodeIsContainer(aNode))
1032 return false;
1034 let root = this.getContainerNodeWithOptions(aNode, false, true);
1035 let result = root.parentResult;
1036 let didSuppressNotifications = false;
1037 let wasOpen = root.containerOpen;
1038 if (!wasOpen) {
1039 didSuppressNotifications = result.suppressNotifications;
1040 if (!didSuppressNotifications)
1041 result.suppressNotifications = true;
1043 root.containerOpen = true;
1044 }
1046 let found = false;
1047 for (let i = 0; i < root.childCount && !found; i++) {
1048 let child = root.getChild(i);
1049 if (this.nodeIsURI(child))
1050 found = true;
1051 }
1053 if (!wasOpen) {
1054 root.containerOpen = false;
1055 if (!didSuppressNotifications)
1056 result.suppressNotifications = false;
1057 }
1058 return found;
1059 },
1061 /**
1062 * Returns an array containing all the uris in the first level of the
1063 * passed in container.
1064 * If you only need to know if the node contains uris, use hasChildURIs.
1065 * @param aNode
1066 * The container node to search through
1067 * @returns array of uris in the first level of the container.
1068 */
1069 getURLsForContainerNode: function PU_getURLsForContainerNode(aNode) {
1070 let urls = [];
1071 if (!this.nodeIsContainer(aNode))
1072 return urls;
1074 let root = this.getContainerNodeWithOptions(aNode, false, true);
1075 let result = root.parentResult;
1076 let wasOpen = root.containerOpen;
1077 let didSuppressNotifications = false;
1078 if (!wasOpen) {
1079 didSuppressNotifications = result.suppressNotifications;
1080 if (!didSuppressNotifications)
1081 result.suppressNotifications = true;
1083 root.containerOpen = true;
1084 }
1086 for (let i = 0; i < root.childCount; ++i) {
1087 let child = root.getChild(i);
1088 if (this.nodeIsURI(child))
1089 urls.push({uri: child.uri, isBookmark: this.nodeIsBookmark(child)});
1090 }
1092 if (!wasOpen) {
1093 root.containerOpen = false;
1094 if (!didSuppressNotifications)
1095 result.suppressNotifications = false;
1096 }
1097 return urls;
1098 },
1100 /**
1101 * Serializes the given node (and all its descendents) as JSON
1102 * and writes the serialization to the given output stream.
1103 *
1104 * @param aNode
1105 * An nsINavHistoryResultNode
1106 * @param aStream
1107 * An nsIOutputStream. NOTE: it only uses the write(str, len)
1108 * method of nsIOutputStream. The caller is responsible for
1109 * closing the stream.
1110 */
1111 _serializeNodeAsJSONToOutputStream: function (aNode, aStream) {
1112 function addGenericProperties(aPlacesNode, aJSNode) {
1113 aJSNode.title = aPlacesNode.title;
1114 aJSNode.id = aPlacesNode.itemId;
1115 if (aJSNode.id != -1) {
1116 var parent = aPlacesNode.parent;
1117 if (parent) {
1118 aJSNode.parent = parent.itemId;
1119 aJSNode.parentReadOnly = PlacesUtils.nodeIsReadOnly(parent);
1120 }
1121 var dateAdded = aPlacesNode.dateAdded;
1122 if (dateAdded)
1123 aJSNode.dateAdded = dateAdded;
1124 var lastModified = aPlacesNode.lastModified;
1125 if (lastModified)
1126 aJSNode.lastModified = lastModified;
1128 // XXX need a hasAnnos api
1129 var annos = [];
1130 try {
1131 annos = PlacesUtils.getAnnotationsForItem(aJSNode.id).filter(function(anno) {
1132 // XXX should whitelist this instead, w/ a pref for
1133 // backup/restore of non-whitelisted annos
1134 // XXX causes JSON encoding errors, so utf-8 encode
1135 //anno.value = unescape(encodeURIComponent(anno.value));
1136 if (anno.name == PlacesUtils.LMANNO_FEEDURI)
1137 aJSNode.livemark = 1;
1138 return true;
1139 });
1140 } catch(ex) {}
1141 if (annos.length != 0)
1142 aJSNode.annos = annos;
1143 }
1144 // XXXdietrich - store annos for non-bookmark items
1145 }
1147 function addURIProperties(aPlacesNode, aJSNode) {
1148 aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
1149 aJSNode.uri = aPlacesNode.uri;
1150 if (aJSNode.id && aJSNode.id != -1) {
1151 // harvest bookmark-specific properties
1152 var keyword = PlacesUtils.bookmarks.getKeywordForBookmark(aJSNode.id);
1153 if (keyword)
1154 aJSNode.keyword = keyword;
1155 }
1157 if (aPlacesNode.tags)
1158 aJSNode.tags = aPlacesNode.tags;
1160 // last character-set
1161 var uri = PlacesUtils._uri(aPlacesNode.uri);
1162 try {
1163 var lastCharset = PlacesUtils.annotations.getPageAnnotation(
1164 uri, PlacesUtils.CHARSET_ANNO);
1165 aJSNode.charset = lastCharset;
1166 } catch (e) {}
1167 }
1169 function addSeparatorProperties(aPlacesNode, aJSNode) {
1170 aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
1171 }
1173 function addContainerProperties(aPlacesNode, aJSNode) {
1174 var concreteId = PlacesUtils.getConcreteItemId(aPlacesNode);
1175 if (concreteId != -1) {
1176 // This is a bookmark or a tag container.
1177 if (PlacesUtils.nodeIsQuery(aPlacesNode) ||
1178 concreteId != aPlacesNode.itemId) {
1179 aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
1180 aJSNode.uri = aPlacesNode.uri;
1181 // folder shortcut
1182 aJSNode.concreteId = concreteId;
1183 }
1184 else { // Bookmark folder or a shortcut we should convert to folder.
1185 aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
1187 // Mark root folders.
1188 if (aJSNode.id == PlacesUtils.placesRootId)
1189 aJSNode.root = "placesRoot";
1190 else if (aJSNode.id == PlacesUtils.bookmarksMenuFolderId)
1191 aJSNode.root = "bookmarksMenuFolder";
1192 else if (aJSNode.id == PlacesUtils.tagsFolderId)
1193 aJSNode.root = "tagsFolder";
1194 else if (aJSNode.id == PlacesUtils.unfiledBookmarksFolderId)
1195 aJSNode.root = "unfiledBookmarksFolder";
1196 else if (aJSNode.id == PlacesUtils.toolbarFolderId)
1197 aJSNode.root = "toolbarFolder";
1198 }
1199 }
1200 else {
1201 // This is a grouped container query, generated on the fly.
1202 aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
1203 aJSNode.uri = aPlacesNode.uri;
1204 }
1205 }
1207 function appendConvertedComplexNode(aNode, aSourceNode, aArray) {
1208 var repr = {};
1210 for (let [name, value] in Iterator(aNode))
1211 repr[name] = value;
1213 // write child nodes
1214 var children = repr.children = [];
1215 if (!aNode.livemark) {
1216 asContainer(aSourceNode);
1217 var wasOpen = aSourceNode.containerOpen;
1218 if (!wasOpen)
1219 aSourceNode.containerOpen = true;
1220 var cc = aSourceNode.childCount;
1221 for (var i = 0; i < cc; ++i) {
1222 var childNode = aSourceNode.getChild(i);
1223 appendConvertedNode(aSourceNode.getChild(i), i, children);
1224 }
1225 if (!wasOpen)
1226 aSourceNode.containerOpen = false;
1227 }
1229 aArray.push(repr);
1230 return true;
1231 }
1233 function appendConvertedNode(bNode, aIndex, aArray) {
1234 var node = {};
1236 // set index in order received
1237 // XXX handy shortcut, but are there cases where we don't want
1238 // to export using the sorting provided by the query?
1239 if (aIndex)
1240 node.index = aIndex;
1242 addGenericProperties(bNode, node);
1244 var parent = bNode.parent;
1245 var grandParent = parent ? parent.parent : null;
1246 if (grandParent)
1247 node.grandParentId = grandParent.itemId;
1249 if (PlacesUtils.nodeIsURI(bNode)) {
1250 // Tag root accept only folder nodes
1251 if (parent && parent.itemId == PlacesUtils.tagsFolderId)
1252 return false;
1254 // Check for url validity, since we can't halt while writing a backup.
1255 // This will throw if we try to serialize an invalid url and it does
1256 // not make sense saving a wrong or corrupt uri node.
1257 try {
1258 PlacesUtils._uri(bNode.uri);
1259 } catch (ex) {
1260 return false;
1261 }
1263 addURIProperties(bNode, node);
1264 }
1265 else if (PlacesUtils.nodeIsContainer(bNode)) {
1266 // Tag containers accept only uri nodes
1267 if (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId)
1268 return false;
1270 addContainerProperties(bNode, node);
1271 }
1272 else if (PlacesUtils.nodeIsSeparator(bNode)) {
1273 // Tag root accept only folder nodes
1274 // Tag containers accept only uri nodes
1275 if ((parent && parent.itemId == PlacesUtils.tagsFolderId) ||
1276 (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId))
1277 return false;
1279 addSeparatorProperties(bNode, node);
1280 }
1282 if (!node.feedURI && node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
1283 return appendConvertedComplexNode(node, bNode, aArray);
1285 aArray.push(node);
1286 return true;
1287 }
1289 // serialize to stream
1290 var array = [];
1291 if (appendConvertedNode(aNode, null, array)) {
1292 var json = JSON.stringify(array[0]);
1293 aStream.write(json, json.length);
1294 }
1295 else {
1296 throw Cr.NS_ERROR_UNEXPECTED;
1297 }
1298 },
1300 /**
1301 * Given a uri returns list of itemIds associated to it.
1302 *
1303 * @param aURI
1304 * nsIURI or spec of the page.
1305 * @param aCallback
1306 * Function to be called when done.
1307 * The function will receive an array of itemIds associated to aURI and
1308 * aURI itself.
1309 *
1310 * @return A object with a .cancel() method allowing to cancel the request.
1311 *
1312 * @note Children of live bookmarks folders are excluded. The callback function is
1313 * not invoked if the request is cancelled or hits an error.
1314 */
1315 asyncGetBookmarkIds: function PU_asyncGetBookmarkIds(aURI, aCallback)
1316 {
1317 if (!this._asyncGetBookmarksStmt) {
1318 let db = this.history.DBConnection;
1319 this._asyncGetBookmarksStmt = db.createAsyncStatement(
1320 "SELECT b.id "
1321 + "FROM moz_bookmarks b "
1322 + "JOIN moz_places h on h.id = b.fk "
1323 + "WHERE h.url = :url "
1324 );
1325 this.registerShutdownFunction(function () {
1326 this._asyncGetBookmarksStmt.finalize();
1327 });
1328 }
1330 let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
1331 this._asyncGetBookmarksStmt.params.url = url;
1333 // Storage does not guarantee that invoking cancel() on a statement
1334 // will cause a REASON_CANCELED. Thus we wrap the statement.
1335 let stmt = new AsyncStatementCancelWrapper(this._asyncGetBookmarksStmt);
1336 return stmt.executeAsync({
1337 _callback: aCallback,
1338 _itemIds: [],
1339 handleResult: function(aResultSet) {
1340 for (let row; (row = aResultSet.getNextRow());) {
1341 this._itemIds.push(row.getResultByIndex(0));
1342 }
1343 },
1344 handleCompletion: function(aReason)
1345 {
1346 if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
1347 this._callback(this._itemIds, aURI);
1348 }
1349 }
1350 });
1351 },
1353 /**
1354 * Lazily adds a bookmarks observer, waiting for the bookmarks service to be
1355 * alive before registering the observer. This is especially useful in the
1356 * startup path, to avoid initializing the service just to add an observer.
1357 *
1358 * @param aObserver
1359 * Object implementing nsINavBookmarkObserver
1360 * @param [optional]aWeakOwner
1361 * Whether to use weak ownership.
1362 *
1363 * @note Correct functionality of lazy observers relies on the fact Places
1364 * notifies categories before real observers, and uses
1365 * PlacesCategoriesStarter component to kick-off the registration.
1366 */
1367 _bookmarksServiceReady: false,
1368 _bookmarksServiceObserversQueue: [],
1369 addLazyBookmarkObserver:
1370 function PU_addLazyBookmarkObserver(aObserver, aWeakOwner) {
1371 if (this._bookmarksServiceReady) {
1372 this.bookmarks.addObserver(aObserver, aWeakOwner === true);
1373 return;
1374 }
1375 this._bookmarksServiceObserversQueue.push({ observer: aObserver,
1376 weak: aWeakOwner === true });
1377 },
1379 /**
1380 * Removes a bookmarks observer added through addLazyBookmarkObserver.
1381 *
1382 * @param aObserver
1383 * Object implementing nsINavBookmarkObserver
1384 */
1385 removeLazyBookmarkObserver:
1386 function PU_removeLazyBookmarkObserver(aObserver) {
1387 if (this._bookmarksServiceReady) {
1388 this.bookmarks.removeObserver(aObserver);
1389 return;
1390 }
1391 let index = -1;
1392 for (let i = 0;
1393 i < this._bookmarksServiceObserversQueue.length && index == -1; i++) {
1394 if (this._bookmarksServiceObserversQueue[i].observer === aObserver)
1395 index = i;
1396 }
1397 if (index != -1) {
1398 this._bookmarksServiceObserversQueue.splice(index, 1);
1399 }
1400 },
1402 /**
1403 * Sets the character-set for a URI.
1404 *
1405 * @param aURI nsIURI
1406 * @param aCharset character-set value.
1407 * @return {Promise}
1408 */
1409 setCharsetForURI: function PU_setCharsetForURI(aURI, aCharset) {
1410 let deferred = Promise.defer();
1412 // Delaying to catch issues with asynchronous behavior while waiting
1413 // to implement asynchronous annotations in bug 699844.
1414 Services.tm.mainThread.dispatch(function() {
1415 if (aCharset && aCharset.length > 0) {
1416 PlacesUtils.annotations.setPageAnnotation(
1417 aURI, PlacesUtils.CHARSET_ANNO, aCharset, 0,
1418 Ci.nsIAnnotationService.EXPIRE_NEVER);
1419 } else {
1420 PlacesUtils.annotations.removePageAnnotation(
1421 aURI, PlacesUtils.CHARSET_ANNO);
1422 }
1423 deferred.resolve();
1424 }, Ci.nsIThread.DISPATCH_NORMAL);
1426 return deferred.promise;
1427 },
1429 /**
1430 * Gets the last saved character-set for a URI.
1431 *
1432 * @param aURI nsIURI
1433 * @return {Promise}
1434 * @resolve a character-set or null.
1435 */
1436 getCharsetForURI: function PU_getCharsetForURI(aURI) {
1437 let deferred = Promise.defer();
1439 Services.tm.mainThread.dispatch(function() {
1440 let charset = null;
1442 try {
1443 charset = PlacesUtils.annotations.getPageAnnotation(aURI,
1444 PlacesUtils.CHARSET_ANNO);
1445 } catch (ex) { }
1447 deferred.resolve(charset);
1448 }, Ci.nsIThread.DISPATCH_NORMAL);
1450 return deferred.promise;
1451 },
1453 /**
1454 * Promised wrapper for mozIAsyncHistory::updatePlaces for a single place.
1455 *
1456 * @param aPlaces
1457 * a single mozIPlaceInfo object
1458 * @resolves {Promise}
1459 */
1460 promiseUpdatePlace: function PU_promiseUpdatePlaces(aPlace) {
1461 let deferred = Promise.defer();
1462 PlacesUtils.asyncHistory.updatePlaces(aPlace, {
1463 _placeInfo: null,
1464 handleResult: function handleResult(aPlaceInfo) {
1465 this._placeInfo = aPlaceInfo;
1466 },
1467 handleError: function handleError(aResultCode, aPlaceInfo) {
1468 deferred.reject(new Components.Exception("Error", aResultCode));
1469 },
1470 handleCompletion: function() {
1471 deferred.resolve(this._placeInfo);
1472 }
1473 });
1475 return deferred.promise;
1476 },
1478 /**
1479 * Promised wrapper for mozIAsyncHistory::getPlacesInfo for a single place.
1480 *
1481 * @param aPlaceIdentifier
1482 * either an nsIURI or a GUID (@see getPlacesInfo)
1483 * @resolves to the place info object handed to handleResult.
1484 */
1485 promisePlaceInfo: function PU_promisePlaceInfo(aPlaceIdentifier) {
1486 let deferred = Promise.defer();
1487 PlacesUtils.asyncHistory.getPlacesInfo(aPlaceIdentifier, {
1488 _placeInfo: null,
1489 handleResult: function handleResult(aPlaceInfo) {
1490 this._placeInfo = aPlaceInfo;
1491 },
1492 handleError: function handleError(aResultCode, aPlaceInfo) {
1493 deferred.reject(new Components.Exception("Error", aResultCode));
1494 },
1495 handleCompletion: function() {
1496 deferred.resolve(this._placeInfo);
1497 }
1498 });
1500 return deferred.promise;
1501 },
1503 /**
1504 * Gets favicon data for a given page url.
1505 *
1506 * @param aPageUrl url of the page to look favicon for.
1507 * @resolves to an object representing a favicon entry, having the following
1508 * properties: { uri, dataLen, data, mimeType }
1509 * @rejects JavaScript exception if the given url has no associated favicon.
1510 */
1511 promiseFaviconData: function (aPageUrl) {
1512 let deferred = Promise.defer();
1513 PlacesUtils.favicons.getFaviconDataForPage(NetUtil.newURI(aPageUrl),
1514 function (aURI, aDataLen, aData, aMimeType) {
1515 if (aURI) {
1516 deferred.resolve({ uri: aURI,
1517 dataLen: aDataLen,
1518 data: aData,
1519 mimeType: aMimeType });
1520 } else {
1521 deferred.reject();
1522 }
1523 });
1524 return deferred.promise;
1525 },
1527 /**
1528 * Get the unique id for an item (a bookmark, a folder or a separator) given
1529 * its item id.
1530 *
1531 * @param aItemId
1532 * an item id
1533 * @return {Promise}
1534 * @resolves to the GUID.
1535 * @rejects if aItemId is invalid.
1536 */
1537 promiseItemGUID: function (aItemId) GUIDHelper.getItemGUID(aItemId),
1539 /**
1540 * Get the item id for an item (a bookmark, a folder or a separator) given
1541 * its unique id.
1542 *
1543 * @param aGUID
1544 * an item GUID
1545 * @retrun {Promise}
1546 * @resolves to the GUID.
1547 * @rejects if there's no item for the given GUID.
1548 */
1549 promiseItemId: function (aGUID) GUIDHelper.getItemId(aGUID)
1550 };
1552 /**
1553 * Wraps the provided statement so that invoking cancel() on the pending
1554 * statement object will always cause a REASON_CANCELED.
1555 */
1556 function AsyncStatementCancelWrapper(aStmt) {
1557 this._stmt = aStmt;
1558 }
1559 AsyncStatementCancelWrapper.prototype = {
1560 _canceled: false,
1561 _cancel: function() {
1562 this._canceled = true;
1563 this._pendingStmt.cancel();
1564 },
1565 handleResult: function(aResultSet) {
1566 this._callback.handleResult(aResultSet);
1567 },
1568 handleError: function(aError) {
1569 Cu.reportError("Async statement execution returned (" + aError.result +
1570 "): " + aError.message);
1571 },
1572 handleCompletion: function(aReason)
1573 {
1574 let reason = this._canceled ?
1575 Ci.mozIStorageStatementCallback.REASON_CANCELED :
1576 aReason;
1577 this._callback.handleCompletion(reason);
1578 },
1579 executeAsync: function(aCallback) {
1580 this._pendingStmt = this._stmt.executeAsync(this);
1581 this._callback = aCallback;
1582 let self = this;
1583 return { cancel: function () { self._cancel(); } }
1584 }
1585 }
1587 XPCOMUtils.defineLazyGetter(PlacesUtils, "history", function() {
1588 return Cc["@mozilla.org/browser/nav-history-service;1"]
1589 .getService(Ci.nsINavHistoryService)
1590 .QueryInterface(Ci.nsIBrowserHistory)
1591 .QueryInterface(Ci.nsPIPlacesDatabase);
1592 });
1594 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "asyncHistory",
1595 "@mozilla.org/browser/history;1",
1596 "mozIAsyncHistory");
1598 XPCOMUtils.defineLazyGetter(PlacesUtils, "bhistory", function() {
1599 return PlacesUtils.history;
1600 });
1602 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "favicons",
1603 "@mozilla.org/browser/favicon-service;1",
1604 "mozIAsyncFavicons");
1606 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "bookmarks",
1607 "@mozilla.org/browser/nav-bookmarks-service;1",
1608 "nsINavBookmarksService");
1610 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "annotations",
1611 "@mozilla.org/browser/annotation-service;1",
1612 "nsIAnnotationService");
1614 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "tagging",
1615 "@mozilla.org/browser/tagging-service;1",
1616 "nsITaggingService");
1618 XPCOMUtils.defineLazyGetter(PlacesUtils, "livemarks", function() {
1619 return Cc["@mozilla.org/browser/livemark-service;2"].
1620 getService(Ci.mozIAsyncLivemarks);
1621 });
1623 XPCOMUtils.defineLazyGetter(PlacesUtils, "transactionManager", function() {
1624 let tm = Cc["@mozilla.org/transactionmanager;1"].
1625 createInstance(Ci.nsITransactionManager);
1626 tm.AddListener(PlacesUtils);
1627 this.registerShutdownFunction(function () {
1628 // Clear all references to local transactions in the transaction manager,
1629 // this prevents from leaking it.
1630 this.transactionManager.RemoveListener(this);
1631 this.transactionManager.clear();
1632 });
1634 // Bug 750269
1635 // The transaction manager keeps strong references to transactions, and by
1636 // that, also to the global for each transaction. A transaction, however,
1637 // could be either the transaction itself (for which the global is this
1638 // module) or some js-proxy in another global, usually a window. The later
1639 // would leak because the transaction lifetime (in the manager's stacks)
1640 // is independent of the global from which doTransaction was called.
1641 // To avoid such a leak, we hide the native doTransaction from callers,
1642 // and let each doTransaction call go through this module.
1643 // Doing so ensures that, as long as the transaction is any of the
1644 // PlacesXXXTransaction objects declared in this module, the object
1645 // referenced by the transaction manager has the module itself as global.
1646 return Object.create(tm, {
1647 "doTransaction": {
1648 value: function(aTransaction) {
1649 tm.doTransaction(aTransaction);
1650 }
1651 }
1652 });
1653 });
1655 XPCOMUtils.defineLazyGetter(this, "bundle", function() {
1656 const PLACES_STRING_BUNDLE_URI = "chrome://places/locale/places.properties";
1657 return Cc["@mozilla.org/intl/stringbundle;1"].
1658 getService(Ci.nsIStringBundleService).
1659 createBundle(PLACES_STRING_BUNDLE_URI);
1660 });
1662 XPCOMUtils.defineLazyServiceGetter(this, "focusManager",
1663 "@mozilla.org/focus-manager;1",
1664 "nsIFocusManager");
1666 // Sometime soon, likely as part of the transition to mozIAsyncBookmarks,
1667 // itemIds will be deprecated in favour of GUIDs, which play much better
1668 // with multiple undo/redo operations. Because these GUIDs are already stored,
1669 // and because we don't want to revise the transactions API once more when this
1670 // happens, transactions are set to work with GUIDs exclusively, in the sense
1671 // that they may never expose itemIds, nor do they accept them as input.
1672 // More importantly, transactions which add or remove items guarantee to
1673 // restore the guids on undo/redo, so that the following transactions that may
1674 // done or undo can assume the items they're interested in are stil accessible
1675 // through the same GUID.
1676 // The current bookmarks API, however, doesn't expose the necessary means for
1677 // working with GUIDs. So, until it does, this helper object accesses the
1678 // Places database directly in order to switch between GUIDs and itemIds, and
1679 // "restore" GUIDs on items re-created items.
1680 const REASON_FINISHED = Ci.mozIStorageStatementCallback.REASON_FINISHED;
1681 let GUIDHelper = {
1682 // Cache for guid<->itemId paris.
1683 GUIDsForIds: new Map(),
1684 idsForGUIDs: new Map(),
1686 getItemId: function (aGUID) {
1687 if (this.idsForGUIDs.has(aGUID))
1688 return Promise.resolve(this.idsForGUIDs.get(aGUID));
1690 let deferred = Promise.defer();
1691 let itemId = -1;
1693 this._getIDStatement.params.guid = aGUID;
1694 this._getIDStatement.executeAsync({
1695 handleResult: function (aResultSet) {
1696 let row = aResultSet.getNextRow();
1697 if (row)
1698 itemId = row.getResultByIndex(0);
1699 },
1700 handleCompletion: aReason => {
1701 if (aReason == REASON_FINISHED && itemId != -1) {
1702 this.ensureObservingRemovedItems();
1703 this.idsForGUIDs.set(aGUID, itemId);
1705 deferred.resolve(itemId);
1706 }
1707 else if (itemId != -1) {
1708 deferred.reject("no item found for the given guid");
1709 }
1710 else {
1711 deferred.reject("SQLite Error: " + aReason);
1712 }
1713 }
1714 });
1716 return deferred.promise;
1717 },
1719 getItemGUID: function (aItemId) {
1720 if (this.GUIDsForIds.has(aItemId))
1721 return Promise.resolve(this.GUIDsForIds.get(aItemId));
1723 let deferred = Promise.defer();
1724 let guid = "";
1726 this._getGUIDStatement.params.id = aItemId;
1727 this._getGUIDStatement.executeAsync({
1728 handleResult: function (aResultSet) {
1729 let row = aResultSet.getNextRow();
1730 if (row) {
1731 guid = row.getResultByIndex(1);
1732 }
1733 },
1734 handleCompletion: aReason => {
1735 if (aReason == REASON_FINISHED && guid) {
1736 this.ensureObservingRemovedItems();
1737 this.GUIDsForIds.set(aItemId, guid);
1739 deferred.resolve(guid);
1740 }
1741 else if (!guid) {
1742 deferred.reject("no item found for the given itemId");
1743 }
1744 else {
1745 deferred.reject("SQLite Error: " + aReason);
1746 }
1747 }
1748 });
1750 return deferred.promise;
1751 },
1753 ensureObservingRemovedItems: function () {
1754 if (!("observer" in this)) {
1755 /**
1756 * This observers serves two purposes:
1757 * (1) Invalidate cached id<->guid paris on when items are removed.
1758 * (2) Cache GUIDs given us free of charge by onItemAdded/onItemRemoved.
1759 * So, for exmaple, when the NewBookmark needs the new GUID, we already
1760 * have it cached.
1761 */
1762 this.observer = {
1763 onItemAdded: (aItemId, aParentId, aIndex, aItemType, aURI, aTitle,
1764 aDateAdded, aGUID, aParentGUID) => {
1765 this.GUIDsForIds.set(aItemId, aGUID);
1766 this.GUIDsForIds.set(aParentId, aParentGUID);
1767 },
1768 onItemRemoved:
1769 (aItemId, aParentId, aIndex, aItemTyep, aURI, aGUID, aParentGUID) => {
1770 this.GUIDsForIds.delete(aItemId);
1771 this.idsForGUIDs.delete(aGUID);
1772 this.GUIDsForIds.set(aParentId, aParentGUID);
1773 },
1775 QueryInterface: XPCOMUtils.generateQI(Ci.nsINavBookmarkObserver),
1776 __noSuchMethod__: () => {}, // Catch all all onItem* methods.
1777 };
1778 PlacesUtils.bookmarks.addObserver(this.observer, false);
1779 PlacesUtils.registerShutdownFunction(() => {
1780 PlacesUtils.bookmarks.removeObserver(this.observer);
1781 });
1782 }
1783 }
1784 };
1785 XPCOMUtils.defineLazyGetter(GUIDHelper, "_getIDStatement", () => {
1786 let s = PlacesUtils.history.DBConnection.createAsyncStatement(
1787 "SELECT b.id, b.guid from moz_bookmarks b WHERE b.guid = :guid");
1788 PlacesUtils.registerShutdownFunction( () => s.finalize() );
1789 return s;
1790 });
1791 XPCOMUtils.defineLazyGetter(GUIDHelper, "_getGUIDStatement", () => {
1792 let s = PlacesUtils.history.DBConnection.createAsyncStatement(
1793 "SELECT b.id, b.guid from moz_bookmarks b WHERE b.id = :id");
1794 PlacesUtils.registerShutdownFunction( () => s.finalize() );
1795 return s;
1796 });
1798 ////////////////////////////////////////////////////////////////////////////////
1799 //// Transactions handlers.
1801 /**
1802 * Updates commands in the undo group of the active window commands.
1803 * Inactive windows commands will be updated on focus.
1804 */
1805 function updateCommandsOnActiveWindow()
1806 {
1807 let win = focusManager.activeWindow;
1808 if (win && win instanceof Ci.nsIDOMWindow) {
1809 // Updating "undo" will cause a group update including "redo".
1810 win.updateCommands("undo");
1811 }
1812 }
1815 /**
1816 * Used to cache bookmark information in transactions.
1817 *
1818 * @note To avoid leaks any non-primitive property should be copied.
1819 * @note Used internally, DO NOT EXPORT.
1820 */
1821 function TransactionItemCache()
1822 {
1823 }
1825 TransactionItemCache.prototype = {
1826 set id(v)
1827 this._id = (parseInt(v) > 0 ? v : null),
1828 get id()
1829 this._id || -1,
1830 set parentId(v)
1831 this._parentId = (parseInt(v) > 0 ? v : null),
1832 get parentId()
1833 this._parentId || -1,
1834 keyword: null,
1835 title: null,
1836 dateAdded: null,
1837 lastModified: null,
1838 postData: null,
1839 itemType: null,
1840 set uri(v)
1841 this._uri = (v instanceof Ci.nsIURI ? v.clone() : null),
1842 get uri()
1843 this._uri || null,
1844 set feedURI(v)
1845 this._feedURI = (v instanceof Ci.nsIURI ? v.clone() : null),
1846 get feedURI()
1847 this._feedURI || null,
1848 set siteURI(v)
1849 this._siteURI = (v instanceof Ci.nsIURI ? v.clone() : null),
1850 get siteURI()
1851 this._siteURI || null,
1852 set index(v)
1853 this._index = (parseInt(v) >= 0 ? v : null),
1854 // Index can be 0.
1855 get index()
1856 this._index != null ? this._index : PlacesUtils.bookmarks.DEFAULT_INDEX,
1857 set annotations(v)
1858 this._annotations = Array.isArray(v) ? Cu.cloneInto(v, {}) : null,
1859 get annotations()
1860 this._annotations || null,
1861 set tags(v)
1862 this._tags = (v && Array.isArray(v) ? Array.slice(v) : null),
1863 get tags()
1864 this._tags || null,
1865 };
1868 /**
1869 * Base transaction implementation.
1870 *
1871 * @note used internally, DO NOT EXPORT.
1872 */
1873 function BaseTransaction()
1874 {
1875 }
1877 BaseTransaction.prototype = {
1878 name: null,
1879 set childTransactions(v)
1880 this._childTransactions = (Array.isArray(v) ? Array.slice(v) : null),
1881 get childTransactions()
1882 this._childTransactions || null,
1883 doTransaction: function BTXN_doTransaction() {},
1884 redoTransaction: function BTXN_redoTransaction() this.doTransaction(),
1885 undoTransaction: function BTXN_undoTransaction() {},
1886 merge: function BTXN_merge() false,
1887 get isTransient() false,
1888 QueryInterface: XPCOMUtils.generateQI([
1889 Ci.nsITransaction
1890 ]),
1891 };
1894 /**
1895 * Transaction for performing several Places Transactions in a single batch.
1896 *
1897 * @param aName
1898 * title of the aggregate transactions
1899 * @param aTransactions
1900 * an array of transactions to perform
1901 *
1902 * @return nsITransaction object
1903 */
1904 this.PlacesAggregatedTransaction =
1905 function PlacesAggregatedTransaction(aName, aTransactions)
1906 {
1907 // Copy the transactions array to decouple it from its prototype, which
1908 // otherwise keeps alive its associated global object.
1909 this.childTransactions = aTransactions;
1910 this.name = aName;
1911 this.item = new TransactionItemCache();
1913 // Check child transactions number. We will batch if we have more than
1914 // MIN_TRANSACTIONS_FOR_BATCH total number of transactions.
1915 let countTransactions = function(aTransactions, aTxnCount)
1916 {
1917 for (let i = 0;
1918 i < aTransactions.length && aTxnCount < MIN_TRANSACTIONS_FOR_BATCH;
1919 ++i, ++aTxnCount) {
1920 let txn = aTransactions[i];
1921 if (txn.childTransactions && txn.childTransactions.length > 0)
1922 aTxnCount = countTransactions(txn.childTransactions, aTxnCount);
1923 }
1924 return aTxnCount;
1925 }
1927 let txnCount = countTransactions(this.childTransactions, 0);
1928 this._useBatch = txnCount >= MIN_TRANSACTIONS_FOR_BATCH;
1929 }
1931 PlacesAggregatedTransaction.prototype = {
1932 __proto__: BaseTransaction.prototype,
1934 doTransaction: function ATXN_doTransaction()
1935 {
1936 this._isUndo = false;
1937 if (this._useBatch)
1938 PlacesUtils.bookmarks.runInBatchMode(this, null);
1939 else
1940 this.runBatched(false);
1941 },
1943 undoTransaction: function ATXN_undoTransaction()
1944 {
1945 this._isUndo = true;
1946 if (this._useBatch)
1947 PlacesUtils.bookmarks.runInBatchMode(this, null);
1948 else
1949 this.runBatched(true);
1950 },
1952 runBatched: function ATXN_runBatched()
1953 {
1954 // Use a copy of the transactions array, so we won't reverse the original
1955 // one on undoing.
1956 let transactions = this.childTransactions.slice(0);
1957 if (this._isUndo)
1958 transactions.reverse();
1959 for (let i = 0; i < transactions.length; ++i) {
1960 let txn = transactions[i];
1961 if (this.item.parentId != -1)
1962 txn.item.parentId = this.item.parentId;
1963 if (this._isUndo)
1964 txn.undoTransaction();
1965 else
1966 txn.doTransaction();
1967 }
1968 }
1969 };
1972 /**
1973 * Transaction for creating a new folder.
1974 *
1975 * @param aTitle
1976 * the title for the new folder
1977 * @param aParentId
1978 * the id of the parent folder in which the new folder should be added
1979 * @param [optional] aIndex
1980 * the index of the item in aParentId
1981 * @param [optional] aAnnotations
1982 * array of annotations to set for the new folder
1983 * @param [optional] aChildTransactions
1984 * array of transactions for items to be created in the new folder
1985 *
1986 * @return nsITransaction object
1987 */
1988 this.PlacesCreateFolderTransaction =
1989 function PlacesCreateFolderTransaction(aTitle, aParentId, aIndex, aAnnotations,
1990 aChildTransactions)
1991 {
1992 this.item = new TransactionItemCache();
1993 this.item.title = aTitle;
1994 this.item.parentId = aParentId;
1995 this.item.index = aIndex;
1996 this.item.annotations = aAnnotations;
1997 this.childTransactions = aChildTransactions;
1998 }
2000 PlacesCreateFolderTransaction.prototype = {
2001 __proto__: BaseTransaction.prototype,
2003 doTransaction: function CFTXN_doTransaction()
2004 {
2005 this.item.id = PlacesUtils.bookmarks.createFolder(this.item.parentId,
2006 this.item.title,
2007 this.item.index);
2008 if (this.item.annotations && this.item.annotations.length > 0)
2009 PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
2011 if (this.childTransactions && this.childTransactions.length > 0) {
2012 // Set the new parent id into child transactions.
2013 for (let i = 0; i < this.childTransactions.length; ++i) {
2014 this.childTransactions[i].item.parentId = this.item.id;
2015 }
2017 let txn = new PlacesAggregatedTransaction("Create folder childTxn",
2018 this.childTransactions);
2019 txn.doTransaction();
2020 }
2021 },
2023 undoTransaction: function CFTXN_undoTransaction()
2024 {
2025 if (this.childTransactions && this.childTransactions.length > 0) {
2026 let txn = new PlacesAggregatedTransaction("Create folder childTxn",
2027 this.childTransactions);
2028 txn.undoTransaction();
2029 }
2031 // Remove item only after all child transactions have been reverted.
2032 PlacesUtils.bookmarks.removeItem(this.item.id);
2033 }
2034 };
2037 /**
2038 * Transaction for creating a new bookmark.
2039 *
2040 * @param aURI
2041 * the nsIURI of the new bookmark
2042 * @param aParentId
2043 * the id of the folder in which the bookmark should be added.
2044 * @param [optional] aIndex
2045 * the index of the item in aParentId
2046 * @param [optional] aTitle
2047 * the title of the new bookmark
2048 * @param [optional] aKeyword
2049 * the keyword for the new bookmark
2050 * @param [optional] aAnnotations
2051 * array of annotations to set for the new bookmark
2052 * @param [optional] aChildTransactions
2053 * child transactions to commit after creating the bookmark. Prefer
2054 * using any of the arguments above if possible. In general, a child
2055 * transations should be used only if the change it does has to be
2056 * reverted manually when removing the bookmark item.
2057 * a child transaction must support setting its bookmark-item
2058 * identifier via an "id" js setter.
2059 *
2060 * @return nsITransaction object
2061 */
2062 this.PlacesCreateBookmarkTransaction =
2063 function PlacesCreateBookmarkTransaction(aURI, aParentId, aIndex, aTitle,
2064 aKeyword, aAnnotations,
2065 aChildTransactions)
2066 {
2067 this.item = new TransactionItemCache();
2068 this.item.uri = aURI;
2069 this.item.parentId = aParentId;
2070 this.item.index = aIndex;
2071 this.item.title = aTitle;
2072 this.item.keyword = aKeyword;
2073 this.item.annotations = aAnnotations;
2074 this.childTransactions = aChildTransactions;
2075 }
2077 PlacesCreateBookmarkTransaction.prototype = {
2078 __proto__: BaseTransaction.prototype,
2080 doTransaction: function CITXN_doTransaction()
2081 {
2082 this.item.id = PlacesUtils.bookmarks.insertBookmark(this.item.parentId,
2083 this.item.uri,
2084 this.item.index,
2085 this.item.title);
2086 if (this.item.keyword) {
2087 PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id,
2088 this.item.keyword);
2089 }
2090 if (this.item.annotations && this.item.annotations.length > 0)
2091 PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
2093 if (this.childTransactions && this.childTransactions.length > 0) {
2094 // Set the new item id into child transactions.
2095 for (let i = 0; i < this.childTransactions.length; ++i) {
2096 this.childTransactions[i].item.id = this.item.id;
2097 }
2098 let txn = new PlacesAggregatedTransaction("Create item childTxn",
2099 this.childTransactions);
2100 txn.doTransaction();
2101 }
2102 },
2104 undoTransaction: function CITXN_undoTransaction()
2105 {
2106 if (this.childTransactions && this.childTransactions.length > 0) {
2107 // Undo transactions should always be done in reverse order.
2108 let txn = new PlacesAggregatedTransaction("Create item childTxn",
2109 this.childTransactions);
2110 txn.undoTransaction();
2111 }
2113 // Remove item only after all child transactions have been reverted.
2114 PlacesUtils.bookmarks.removeItem(this.item.id);
2115 }
2116 };
2119 /**
2120 * Transaction for creating a new separator.
2121 *
2122 * @param aParentId
2123 * the id of the folder in which the separator should be added
2124 * @param [optional] aIndex
2125 * the index of the item in aParentId
2126 *
2127 * @return nsITransaction object
2128 */
2129 this.PlacesCreateSeparatorTransaction =
2130 function PlacesCreateSeparatorTransaction(aParentId, aIndex)
2131 {
2132 this.item = new TransactionItemCache();
2133 this.item.parentId = aParentId;
2134 this.item.index = aIndex;
2135 }
2137 PlacesCreateSeparatorTransaction.prototype = {
2138 __proto__: BaseTransaction.prototype,
2140 doTransaction: function CSTXN_doTransaction()
2141 {
2142 this.item.id =
2143 PlacesUtils.bookmarks.insertSeparator(this.item.parentId, this.item.index);
2144 },
2146 undoTransaction: function CSTXN_undoTransaction()
2147 {
2148 PlacesUtils.bookmarks.removeItem(this.item.id);
2149 }
2150 };
2153 /**
2154 * Transaction for creating a new livemark item.
2155 *
2156 * @see mozIAsyncLivemarks for documentation regarding the arguments.
2157 *
2158 * @param aFeedURI
2159 * nsIURI of the feed
2160 * @param [optional] aSiteURI
2161 * nsIURI of the page serving the feed
2162 * @param aTitle
2163 * title for the livemark
2164 * @param aParentId
2165 * the id of the folder in which the livemark should be added
2166 * @param [optional] aIndex
2167 * the index of the livemark in aParentId
2168 * @param [optional] aAnnotations
2169 * array of annotations to set for the new livemark.
2170 *
2171 * @return nsITransaction object
2172 */
2173 this.PlacesCreateLivemarkTransaction =
2174 function PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aTitle, aParentId,
2175 aIndex, aAnnotations)
2176 {
2177 this.item = new TransactionItemCache();
2178 this.item.feedURI = aFeedURI;
2179 this.item.siteURI = aSiteURI;
2180 this.item.title = aTitle;
2181 this.item.parentId = aParentId;
2182 this.item.index = aIndex;
2183 this.item.annotations = aAnnotations;
2184 }
2186 PlacesCreateLivemarkTransaction.prototype = {
2187 __proto__: BaseTransaction.prototype,
2189 doTransaction: function CLTXN_doTransaction()
2190 {
2191 PlacesUtils.livemarks.addLivemark(
2192 { title: this.item.title
2193 , feedURI: this.item.feedURI
2194 , parentId: this.item.parentId
2195 , index: this.item.index
2196 , siteURI: this.item.siteURI
2197 }).then(aLivemark => {
2198 this.item.id = aLivemark.id;
2199 if (this.item.annotations && this.item.annotations.length > 0) {
2200 PlacesUtils.setAnnotationsForItem(this.item.id,
2201 this.item.annotations);
2202 }
2203 }, Cu.reportError);
2204 },
2206 undoTransaction: function CLTXN_undoTransaction()
2207 {
2208 // The getLivemark callback may fail, but it is used just to serialize,
2209 // so it doesn't matter.
2210 PlacesUtils.livemarks.getLivemark({ id: this.item.id })
2211 .then(null, null).then( () => {
2212 PlacesUtils.bookmarks.removeItem(this.item.id);
2213 });
2214 }
2215 };
2218 /**
2219 * Transaction for removing a livemark item.
2220 *
2221 * @param aLivemarkId
2222 * the identifier of the folder for the livemark.
2223 *
2224 * @return nsITransaction object
2225 * @note used internally by PlacesRemoveItemTransaction, DO NOT EXPORT.
2226 */
2227 function PlacesRemoveLivemarkTransaction(aLivemarkId)
2228 {
2229 this.item = new TransactionItemCache();
2230 this.item.id = aLivemarkId;
2231 this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
2232 this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
2234 let annos = PlacesUtils.getAnnotationsForItem(this.item.id);
2235 // Exclude livemark service annotations, those will be recreated automatically
2236 let annosToExclude = [PlacesUtils.LMANNO_FEEDURI,
2237 PlacesUtils.LMANNO_SITEURI];
2238 this.item.annotations = annos.filter(function(aValue, aIndex, aArray) {
2239 return annosToExclude.indexOf(aValue.name) == -1;
2240 });
2241 this.item.dateAdded = PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
2242 this.item.lastModified =
2243 PlacesUtils.bookmarks.getItemLastModified(this.item.id);
2244 }
2246 PlacesRemoveLivemarkTransaction.prototype = {
2247 __proto__: BaseTransaction.prototype,
2249 doTransaction: function RLTXN_doTransaction()
2250 {
2251 PlacesUtils.livemarks.getLivemark({ id: this.item.id })
2252 .then(aLivemark => {
2253 this.item.feedURI = aLivemark.feedURI;
2254 this.item.siteURI = aLivemark.siteURI;
2255 PlacesUtils.bookmarks.removeItem(this.item.id);
2256 }, Cu.reportError);
2257 },
2259 undoTransaction: function RLTXN_undoTransaction()
2260 {
2261 // Undo work must be serialized, otherwise won't be able to know the
2262 // feedURI and siteURI of the livemark.
2263 // The getLivemark callback is expected to receive a failure status but it
2264 // is used just to serialize, so doesn't matter.
2265 PlacesUtils.livemarks.getLivemark({ id: this.item.id })
2266 .then(null, () => {
2267 PlacesUtils.livemarks.addLivemark({ parentId: this.item.parentId
2268 , title: this.item.title
2269 , siteURI: this.item.siteURI
2270 , feedURI: this.item.feedURI
2271 , index: this.item.index
2272 , lastModified: this.item.lastModified
2273 }).then(
2274 aLivemark => {
2275 let itemId = aLivemark.id;
2276 PlacesUtils.bookmarks.setItemDateAdded(itemId, this.item.dateAdded);
2277 PlacesUtils.setAnnotationsForItem(itemId, this.item.annotations);
2278 }, Cu.reportError);
2279 });
2280 }
2281 };
2284 /**
2285 * Transaction for moving an Item.
2286 *
2287 * @param aItemId
2288 * the id of the item to move
2289 * @param aNewParentId
2290 * id of the new parent to move to
2291 * @param aNewIndex
2292 * index of the new position to move to
2293 *
2294 * @return nsITransaction object
2295 */
2296 this.PlacesMoveItemTransaction =
2297 function PlacesMoveItemTransaction(aItemId, aNewParentId, aNewIndex)
2298 {
2299 this.item = new TransactionItemCache();
2300 this.item.id = aItemId;
2301 this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
2302 this.new = new TransactionItemCache();
2303 this.new.parentId = aNewParentId;
2304 this.new.index = aNewIndex;
2305 }
2307 PlacesMoveItemTransaction.prototype = {
2308 __proto__: BaseTransaction.prototype,
2310 doTransaction: function MITXN_doTransaction()
2311 {
2312 this.item.index = PlacesUtils.bookmarks.getItemIndex(this.item.id);
2313 PlacesUtils.bookmarks.moveItem(this.item.id,
2314 this.new.parentId, this.new.index);
2315 this._undoIndex = PlacesUtils.bookmarks.getItemIndex(this.item.id);
2316 },
2318 undoTransaction: function MITXN_undoTransaction()
2319 {
2320 // moving down in the same parent takes in count removal of the item
2321 // so to revert positions we must move to oldIndex + 1
2322 if (this.new.parentId == this.item.parentId &&
2323 this.item.index > this._undoIndex) {
2324 PlacesUtils.bookmarks.moveItem(this.item.id, this.item.parentId,
2325 this.item.index + 1);
2326 }
2327 else {
2328 PlacesUtils.bookmarks.moveItem(this.item.id, this.item.parentId,
2329 this.item.index);
2330 }
2331 }
2332 };
2335 /**
2336 * Transaction for removing an Item
2337 *
2338 * @param aItemId
2339 * id of the item to remove
2340 *
2341 * @return nsITransaction object
2342 */
2343 this.PlacesRemoveItemTransaction =
2344 function PlacesRemoveItemTransaction(aItemId)
2345 {
2346 if (PlacesUtils.isRootItem(aItemId))
2347 throw Cr.NS_ERROR_INVALID_ARG;
2349 // if the item lives within a tag container, use the tagging transactions
2350 let parent = PlacesUtils.bookmarks.getFolderIdForItem(aItemId);
2351 let grandparent = PlacesUtils.bookmarks.getFolderIdForItem(parent);
2352 if (grandparent == PlacesUtils.tagsFolderId) {
2353 let uri = PlacesUtils.bookmarks.getBookmarkURI(aItemId);
2354 return new PlacesUntagURITransaction(uri, [parent]);
2355 }
2357 // if the item is a livemark container we will not save its children.
2358 if (PlacesUtils.annotations.itemHasAnnotation(aItemId,
2359 PlacesUtils.LMANNO_FEEDURI))
2360 return new PlacesRemoveLivemarkTransaction(aItemId);
2362 this.item = new TransactionItemCache();
2363 this.item.id = aItemId;
2364 this.item.itemType = PlacesUtils.bookmarks.getItemType(this.item.id);
2365 if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
2366 this.childTransactions = this._getFolderContentsTransactions();
2367 // Remove this folder itself.
2368 let txn = PlacesUtils.bookmarks.getRemoveFolderTransaction(this.item.id);
2369 this.childTransactions.push(txn);
2370 }
2371 else if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
2372 this.item.uri = PlacesUtils.bookmarks.getBookmarkURI(this.item.id);
2373 this.item.keyword =
2374 PlacesUtils.bookmarks.getKeywordForBookmark(this.item.id);
2375 }
2377 if (this.item.itemType != Ci.nsINavBookmarksService.TYPE_SEPARATOR)
2378 this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
2380 this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
2381 this.item.annotations = PlacesUtils.getAnnotationsForItem(this.item.id);
2382 this.item.dateAdded = PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
2383 this.item.lastModified =
2384 PlacesUtils.bookmarks.getItemLastModified(this.item.id);
2385 }
2387 PlacesRemoveItemTransaction.prototype = {
2388 __proto__: BaseTransaction.prototype,
2390 doTransaction: function RITXN_doTransaction()
2391 {
2392 this.item.index = PlacesUtils.bookmarks.getItemIndex(this.item.id);
2394 if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
2395 let txn = new PlacesAggregatedTransaction("Remove item childTxn",
2396 this.childTransactions);
2397 txn.doTransaction();
2398 }
2399 else {
2400 // Before removing the bookmark, save its tags.
2401 let tags = this.item.uri ?
2402 PlacesUtils.tagging.getTagsForURI(this.item.uri) : null;
2404 PlacesUtils.bookmarks.removeItem(this.item.id);
2406 // If this was the last bookmark (excluding tag-items) for this url,
2407 // persist the tags.
2408 if (tags && PlacesUtils.getMostRecentBookmarkForURI(this.item.uri) == -1) {
2409 this.item.tags = tags;
2410 }
2411 }
2412 },
2414 undoTransaction: function RITXN_undoTransaction()
2415 {
2416 if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
2417 this.item.id = PlacesUtils.bookmarks.insertBookmark(this.item.parentId,
2418 this.item.uri,
2419 this.item.index,
2420 this.item.title);
2421 if (this.item.tags && this.item.tags.length > 0)
2422 PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
2423 if (this.item.keyword) {
2424 PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id,
2425 this.item.keyword);
2426 }
2427 }
2428 else if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
2429 let txn = new PlacesAggregatedTransaction("Remove item childTxn",
2430 this.childTransactions);
2431 txn.undoTransaction();
2432 }
2433 else { // TYPE_SEPARATOR
2434 this.item.id = PlacesUtils.bookmarks.insertSeparator(this.item.parentId,
2435 this.item.index);
2436 }
2438 if (this.item.annotations && this.item.annotations.length > 0)
2439 PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
2441 PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.item.dateAdded);
2442 PlacesUtils.bookmarks.setItemLastModified(this.item.id,
2443 this.item.lastModified);
2444 },
2446 /**
2447 * Returns a flat, ordered list of transactions for a depth-first recreation
2448 * of items within this folder.
2449 */
2450 _getFolderContentsTransactions:
2451 function RITXN__getFolderContentsTransactions()
2452 {
2453 let transactions = [];
2454 let contents =
2455 PlacesUtils.getFolderContents(this.item.id, false, false).root;
2456 for (let i = 0; i < contents.childCount; ++i) {
2457 let txn = new PlacesRemoveItemTransaction(contents.getChild(i).itemId);
2458 transactions.push(txn);
2459 }
2460 contents.containerOpen = false;
2461 // Reverse transactions to preserve parent-child relationship.
2462 return transactions.reverse();
2463 }
2464 };
2467 /**
2468 * Transaction for editting a bookmark's title.
2469 *
2470 * @param aItemId
2471 * id of the item to edit
2472 * @param aNewTitle
2473 * new title for the item to edit
2474 *
2475 * @return nsITransaction object
2476 */
2477 this.PlacesEditItemTitleTransaction =
2478 function PlacesEditItemTitleTransaction(aItemId, aNewTitle)
2479 {
2480 this.item = new TransactionItemCache();
2481 this.item.id = aItemId;
2482 this.new = new TransactionItemCache();
2483 this.new.title = aNewTitle;
2484 }
2486 PlacesEditItemTitleTransaction.prototype = {
2487 __proto__: BaseTransaction.prototype,
2489 doTransaction: function EITTXN_doTransaction()
2490 {
2491 this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
2492 PlacesUtils.bookmarks.setItemTitle(this.item.id, this.new.title);
2493 },
2495 undoTransaction: function EITTXN_undoTransaction()
2496 {
2497 PlacesUtils.bookmarks.setItemTitle(this.item.id, this.item.title);
2498 }
2499 };
2502 /**
2503 * Transaction for editing a bookmark's uri.
2504 *
2505 * @param aItemId
2506 * id of the bookmark to edit
2507 * @param aNewURI
2508 * new uri for the bookmark
2509 *
2510 * @return nsITransaction object
2511 */
2512 this.PlacesEditBookmarkURITransaction =
2513 function PlacesEditBookmarkURITransaction(aItemId, aNewURI) {
2514 this.item = new TransactionItemCache();
2515 this.item.id = aItemId;
2516 this.new = new TransactionItemCache();
2517 this.new.uri = aNewURI;
2518 }
2520 PlacesEditBookmarkURITransaction.prototype = {
2521 __proto__: BaseTransaction.prototype,
2523 doTransaction: function EBUTXN_doTransaction()
2524 {
2525 this.item.uri = PlacesUtils.bookmarks.getBookmarkURI(this.item.id);
2526 PlacesUtils.bookmarks.changeBookmarkURI(this.item.id, this.new.uri);
2527 // move tags from old URI to new URI
2528 this.item.tags = PlacesUtils.tagging.getTagsForURI(this.item.uri);
2529 if (this.item.tags.length != 0) {
2530 // only untag the old URI if this is the only bookmark
2531 if (PlacesUtils.getBookmarksForURI(this.item.uri, {}).length == 0)
2532 PlacesUtils.tagging.untagURI(this.item.uri, this.item.tags);
2533 PlacesUtils.tagging.tagURI(this.new.uri, this.item.tags);
2534 }
2535 },
2537 undoTransaction: function EBUTXN_undoTransaction()
2538 {
2539 PlacesUtils.bookmarks.changeBookmarkURI(this.item.id, this.item.uri);
2540 // move tags from new URI to old URI
2541 if (this.item.tags.length != 0) {
2542 // only untag the new URI if this is the only bookmark
2543 if (PlacesUtils.getBookmarksForURI(this.new.uri, {}).length == 0)
2544 PlacesUtils.tagging.untagURI(this.new.uri, this.item.tags);
2545 PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
2546 }
2547 }
2548 };
2551 /**
2552 * Transaction for setting/unsetting an item annotation
2553 *
2554 * @param aItemId
2555 * id of the item where to set annotation
2556 * @param aAnnotationObject
2557 * Object representing an annotation, containing the following
2558 * properties: name, flags, expires, value.
2559 * If value is null the annotation will be removed
2560 *
2561 * @return nsITransaction object
2562 */
2563 this.PlacesSetItemAnnotationTransaction =
2564 function PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject)
2565 {
2566 this.item = new TransactionItemCache();
2567 this.item.id = aItemId;
2568 this.new = new TransactionItemCache();
2569 this.new.annotations = [aAnnotationObject];
2570 }
2572 PlacesSetItemAnnotationTransaction.prototype = {
2573 __proto__: BaseTransaction.prototype,
2575 doTransaction: function SIATXN_doTransaction()
2576 {
2577 let annoName = this.new.annotations[0].name;
2578 if (PlacesUtils.annotations.itemHasAnnotation(this.item.id, annoName)) {
2579 // fill the old anno if it is set
2580 let flags = {}, expires = {}, type = {};
2581 PlacesUtils.annotations.getItemAnnotationInfo(this.item.id, annoName, flags,
2582 expires, type);
2583 let value = PlacesUtils.annotations.getItemAnnotation(this.item.id,
2584 annoName);
2585 this.item.annotations = [{ name: annoName,
2586 type: type.value,
2587 flags: flags.value,
2588 value: value,
2589 expires: expires.value }];
2590 }
2591 else {
2592 // create an empty old anno
2593 this.item.annotations = [{ name: annoName,
2594 flags: 0,
2595 value: null,
2596 expires: Ci.nsIAnnotationService.EXPIRE_NEVER }];
2597 }
2599 PlacesUtils.setAnnotationsForItem(this.item.id, this.new.annotations);
2600 },
2602 undoTransaction: function SIATXN_undoTransaction()
2603 {
2604 PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
2605 }
2606 };
2609 /**
2610 * Transaction for setting/unsetting a page annotation
2611 *
2612 * @param aURI
2613 * URI of the page where to set annotation
2614 * @param aAnnotationObject
2615 * Object representing an annotation, containing the following
2616 * properties: name, flags, expires, value.
2617 * If value is null the annotation will be removed
2618 *
2619 * @return nsITransaction object
2620 */
2621 this.PlacesSetPageAnnotationTransaction =
2622 function PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject)
2623 {
2624 this.item = new TransactionItemCache();
2625 this.item.uri = aURI;
2626 this.new = new TransactionItemCache();
2627 this.new.annotations = [aAnnotationObject];
2628 }
2630 PlacesSetPageAnnotationTransaction.prototype = {
2631 __proto__: BaseTransaction.prototype,
2633 doTransaction: function SPATXN_doTransaction()
2634 {
2635 let annoName = this.new.annotations[0].name;
2636 if (PlacesUtils.annotations.pageHasAnnotation(this.item.uri, annoName)) {
2637 // fill the old anno if it is set
2638 let flags = {}, expires = {}, type = {};
2639 PlacesUtils.annotations.getPageAnnotationInfo(this.item.uri, annoName, flags,
2640 expires, type);
2641 let value = PlacesUtils.annotations.getPageAnnotation(this.item.uri,
2642 annoName);
2643 this.item.annotations = [{ name: annoName,
2644 flags: flags.value,
2645 value: value,
2646 expires: expires.value }];
2647 }
2648 else {
2649 // create an empty old anno
2650 this.item.annotations = [{ name: annoName,
2651 type: Ci.nsIAnnotationService.TYPE_STRING,
2652 flags: 0,
2653 value: null,
2654 expires: Ci.nsIAnnotationService.EXPIRE_NEVER }];
2655 }
2657 PlacesUtils.setAnnotationsForURI(this.item.uri, this.new.annotations);
2658 },
2660 undoTransaction: function SPATXN_undoTransaction()
2661 {
2662 PlacesUtils.setAnnotationsForURI(this.item.uri, this.item.annotations);
2663 }
2664 };
2667 /**
2668 * Transaction for editing a bookmark's keyword.
2669 *
2670 * @param aItemId
2671 * id of the bookmark to edit
2672 * @param aNewKeyword
2673 * new keyword for the bookmark
2674 *
2675 * @return nsITransaction object
2676 */
2677 this.PlacesEditBookmarkKeywordTransaction =
2678 function PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword)
2679 {
2680 this.item = new TransactionItemCache();
2681 this.item.id = aItemId;
2682 this.new = new TransactionItemCache();
2683 this.new.keyword = aNewKeyword;
2684 }
2686 PlacesEditBookmarkKeywordTransaction.prototype = {
2687 __proto__: BaseTransaction.prototype,
2689 doTransaction: function EBKTXN_doTransaction()
2690 {
2691 this.item.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this.item.id);
2692 PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.new.keyword);
2693 },
2695 undoTransaction: function EBKTXN_undoTransaction()
2696 {
2697 PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.item.keyword);
2698 }
2699 };
2702 /**
2703 * Transaction for editing the post data associated with a bookmark.
2704 *
2705 * @param aItemId
2706 * id of the bookmark to edit
2707 * @param aPostData
2708 * post data
2709 *
2710 * @return nsITransaction object
2711 */
2712 this.PlacesEditBookmarkPostDataTransaction =
2713 function PlacesEditBookmarkPostDataTransaction(aItemId, aPostData)
2714 {
2715 this.item = new TransactionItemCache();
2716 this.item.id = aItemId;
2717 this.new = new TransactionItemCache();
2718 this.new.postData = aPostData;
2719 }
2721 PlacesEditBookmarkPostDataTransaction.prototype = {
2722 __proto__: BaseTransaction.prototype,
2724 doTransaction: function EBPDTXN_doTransaction()
2725 {
2726 this.item.postData = PlacesUtils.getPostDataForBookmark(this.item.id);
2727 PlacesUtils.setPostDataForBookmark(this.item.id, this.new.postData);
2728 },
2730 undoTransaction: function EBPDTXN_undoTransaction()
2731 {
2732 PlacesUtils.setPostDataForBookmark(this.item.id, this.item.postData);
2733 }
2734 };
2737 /**
2738 * Transaction for editing an item's date added property.
2739 *
2740 * @param aItemId
2741 * id of the item to edit
2742 * @param aNewDateAdded
2743 * new date added for the item
2744 *
2745 * @return nsITransaction object
2746 */
2747 this.PlacesEditItemDateAddedTransaction =
2748 function PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded)
2749 {
2750 this.item = new TransactionItemCache();
2751 this.item.id = aItemId;
2752 this.new = new TransactionItemCache();
2753 this.new.dateAdded = aNewDateAdded;
2754 }
2756 PlacesEditItemDateAddedTransaction.prototype = {
2757 __proto__: BaseTransaction.prototype,
2759 doTransaction: function EIDATXN_doTransaction()
2760 {
2761 // Child transactions have the id set as parentId.
2762 if (this.item.id == -1 && this.item.parentId != -1)
2763 this.item.id = this.item.parentId;
2764 this.item.dateAdded =
2765 PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
2766 PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.new.dateAdded);
2767 },
2769 undoTransaction: function EIDATXN_undoTransaction()
2770 {
2771 PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.item.dateAdded);
2772 }
2773 };
2776 /**
2777 * Transaction for editing an item's last modified time.
2778 *
2779 * @param aItemId
2780 * id of the item to edit
2781 * @param aNewLastModified
2782 * new last modified date for the item
2783 *
2784 * @return nsITransaction object
2785 */
2786 this.PlacesEditItemLastModifiedTransaction =
2787 function PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified)
2788 {
2789 this.item = new TransactionItemCache();
2790 this.item.id = aItemId;
2791 this.new = new TransactionItemCache();
2792 this.new.lastModified = aNewLastModified;
2793 }
2795 PlacesEditItemLastModifiedTransaction.prototype = {
2796 __proto__: BaseTransaction.prototype,
2798 doTransaction:
2799 function EILMTXN_doTransaction()
2800 {
2801 // Child transactions have the id set as parentId.
2802 if (this.item.id == -1 && this.item.parentId != -1)
2803 this.item.id = this.item.parentId;
2804 this.item.lastModified =
2805 PlacesUtils.bookmarks.getItemLastModified(this.item.id);
2806 PlacesUtils.bookmarks.setItemLastModified(this.item.id,
2807 this.new.lastModified);
2808 },
2810 undoTransaction:
2811 function EILMTXN_undoTransaction()
2812 {
2813 PlacesUtils.bookmarks.setItemLastModified(this.item.id,
2814 this.item.lastModified);
2815 }
2816 };
2819 /**
2820 * Transaction for sorting a folder by name
2821 *
2822 * @param aFolderId
2823 * id of the folder to sort
2824 *
2825 * @return nsITransaction object
2826 */
2827 this.PlacesSortFolderByNameTransaction =
2828 function PlacesSortFolderByNameTransaction(aFolderId)
2829 {
2830 this.item = new TransactionItemCache();
2831 this.item.id = aFolderId;
2832 }
2834 PlacesSortFolderByNameTransaction.prototype = {
2835 __proto__: BaseTransaction.prototype,
2837 doTransaction: function SFBNTXN_doTransaction()
2838 {
2839 this._oldOrder = [];
2841 let contents =
2842 PlacesUtils.getFolderContents(this.item.id, false, false).root;
2843 let count = contents.childCount;
2845 // sort between separators
2846 let newOrder = [];
2847 let preSep = []; // temporary array for sorting each group of items
2848 let sortingMethod =
2849 function (a, b) {
2850 if (PlacesUtils.nodeIsContainer(a) && !PlacesUtils.nodeIsContainer(b))
2851 return -1;
2852 if (!PlacesUtils.nodeIsContainer(a) && PlacesUtils.nodeIsContainer(b))
2853 return 1;
2854 return a.title.localeCompare(b.title);
2855 };
2857 for (let i = 0; i < count; ++i) {
2858 let item = contents.getChild(i);
2859 this._oldOrder[item.itemId] = i;
2860 if (PlacesUtils.nodeIsSeparator(item)) {
2861 if (preSep.length > 0) {
2862 preSep.sort(sortingMethod);
2863 newOrder = newOrder.concat(preSep);
2864 preSep.splice(0, preSep.length);
2865 }
2866 newOrder.push(item);
2867 }
2868 else
2869 preSep.push(item);
2870 }
2871 contents.containerOpen = false;
2873 if (preSep.length > 0) {
2874 preSep.sort(sortingMethod);
2875 newOrder = newOrder.concat(preSep);
2876 }
2878 // set the nex indexes
2879 let callback = {
2880 runBatched: function() {
2881 for (let i = 0; i < newOrder.length; ++i) {
2882 PlacesUtils.bookmarks.setItemIndex(newOrder[i].itemId, i);
2883 }
2884 }
2885 };
2886 PlacesUtils.bookmarks.runInBatchMode(callback, null);
2887 },
2889 undoTransaction: function SFBNTXN_undoTransaction()
2890 {
2891 let callback = {
2892 _self: this,
2893 runBatched: function() {
2894 for (item in this._self._oldOrder)
2895 PlacesUtils.bookmarks.setItemIndex(item, this._self._oldOrder[item]);
2896 }
2897 };
2898 PlacesUtils.bookmarks.runInBatchMode(callback, null);
2899 }
2900 };
2903 /**
2904 * Transaction for tagging a URL with the given set of tags. Current tags set
2905 * for the URL persist. It's the caller's job to check whether or not aURI
2906 * was already tagged by any of the tags in aTags, undoing this tags
2907 * transaction removes them all from aURL!
2908 *
2909 * @param aURI
2910 * the URL to tag.
2911 * @param aTags
2912 * Array of tags to set for the given URL.
2913 */
2914 this.PlacesTagURITransaction =
2915 function PlacesTagURITransaction(aURI, aTags)
2916 {
2917 this.item = new TransactionItemCache();
2918 this.item.uri = aURI;
2919 this.item.tags = aTags;
2920 }
2922 PlacesTagURITransaction.prototype = {
2923 __proto__: BaseTransaction.prototype,
2925 doTransaction: function TUTXN_doTransaction()
2926 {
2927 if (PlacesUtils.getMostRecentBookmarkForURI(this.item.uri) == -1) {
2928 // There is no bookmark for this uri, but we only allow to tag bookmarks.
2929 // Force an unfiled bookmark first.
2930 this.item.id =
2931 PlacesUtils.bookmarks
2932 .insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
2933 this.item.uri,
2934 PlacesUtils.bookmarks.DEFAULT_INDEX,
2935 PlacesUtils.history.getPageTitle(this.item.uri));
2936 }
2937 PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
2938 },
2940 undoTransaction: function TUTXN_undoTransaction()
2941 {
2942 if (this.item.id != -1) {
2943 PlacesUtils.bookmarks.removeItem(this.item.id);
2944 this.item.id = -1;
2945 }
2946 PlacesUtils.tagging.untagURI(this.item.uri, this.item.tags);
2947 }
2948 };
2951 /**
2952 * Transaction for removing tags from a URL. It's the caller's job to check
2953 * whether or not aURI isn't tagged by any of the tags in aTags, undoing this
2954 * tags transaction adds them all to aURL!
2955 *
2956 * @param aURI
2957 * the URL to un-tag.
2958 * @param aTags
2959 * Array of tags to unset. pass null to remove all tags from the given
2960 * url.
2961 */
2962 this.PlacesUntagURITransaction =
2963 function PlacesUntagURITransaction(aURI, aTags)
2964 {
2965 this.item = new TransactionItemCache();
2966 this.item.uri = aURI;
2967 if (aTags) {
2968 // Within this transaction, we cannot rely on tags given by itemId
2969 // since the tag containers may be gone after we call untagURI.
2970 // Thus, we convert each tag given by its itemId to name.
2971 let tags = [];
2972 for (let i = 0; i < aTags.length; ++i) {
2973 if (typeof(aTags[i]) == "number")
2974 tags.push(PlacesUtils.bookmarks.getItemTitle(aTags[i]));
2975 else
2976 tags.push(aTags[i]);
2977 }
2978 this.item.tags = tags;
2979 }
2980 }
2982 PlacesUntagURITransaction.prototype = {
2983 __proto__: BaseTransaction.prototype,
2985 doTransaction: function UTUTXN_doTransaction()
2986 {
2987 // Filter tags existing on the bookmark, otherwise on undo we may try to
2988 // set nonexistent tags.
2989 let tags = PlacesUtils.tagging.getTagsForURI(this.item.uri);
2990 this.item.tags = this.item.tags.filter(function (aTag) {
2991 return tags.indexOf(aTag) != -1;
2992 });
2993 PlacesUtils.tagging.untagURI(this.item.uri, this.item.tags);
2994 },
2996 undoTransaction: function UTUTXN_undoTransaction()
2997 {
2998 PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
2999 }
3000 };