|
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/. */ |
|
5 |
|
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 ]; |
|
27 |
|
28 const Ci = Components.interfaces; |
|
29 const Cc = Components.classes; |
|
30 const Cr = Components.results; |
|
31 const Cu = Components.utils; |
|
32 |
|
33 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
34 |
|
35 XPCOMUtils.defineLazyModuleGetter(this, "Services", |
|
36 "resource://gre/modules/Services.jsm"); |
|
37 |
|
38 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
|
39 "resource://gre/modules/NetUtil.jsm"); |
|
40 |
|
41 XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
42 "resource://gre/modules/Task.jsm"); |
|
43 |
|
44 XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
45 "resource://gre/modules/Promise.jsm"); |
|
46 |
|
47 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", |
|
48 "resource://gre/modules/Deprecated.jsm"); |
|
49 |
|
50 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils", |
|
51 "resource://gre/modules/BookmarkJSONUtils.jsm"); |
|
52 |
|
53 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", |
|
54 "resource://gre/modules/PlacesBackups.jsm"); |
|
55 |
|
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; |
|
60 |
|
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 |
|
69 |
|
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); |
|
81 |
|
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", |
|
97 |
|
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", |
|
104 |
|
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", |
|
115 |
|
116 asContainer: function(aNode) asContainer(aNode), |
|
117 asQuery: function(aNode) asQuery(aNode), |
|
118 |
|
119 endl: NEWLINE, |
|
120 |
|
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 }, |
|
130 |
|
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 }, |
|
143 |
|
144 getFormattedString: function PU_getFormattedString(key, params) { |
|
145 return bundle.formatStringFromName(key, params, params.length); |
|
146 }, |
|
147 |
|
148 getString: function PU_getString(key) { |
|
149 return bundle.GetStringFromName(key); |
|
150 }, |
|
151 |
|
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 }, |
|
162 |
|
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 }, |
|
173 |
|
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 }, |
|
183 |
|
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 }, |
|
193 |
|
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 }, |
|
203 |
|
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 }, |
|
216 |
|
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 }); |
|
237 |
|
238 var readOnly = this.annotations.getItemsWithAnnotation(this.READ_ONLY_ANNO); |
|
239 this.__defineGetter__("_readOnly", function() readOnly); |
|
240 return this._readOnly; |
|
241 }, |
|
242 |
|
243 QueryInterface: XPCOMUtils.generateQI([ |
|
244 Ci.nsIAnnotationObserver |
|
245 , Ci.nsIObserver |
|
246 , Ci.nsITransactionListener |
|
247 ]), |
|
248 |
|
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 }, |
|
258 |
|
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 }, |
|
283 |
|
284 ////////////////////////////////////////////////////////////////////////////// |
|
285 //// nsIAnnotationObserver |
|
286 |
|
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 }, |
|
293 |
|
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 }, |
|
301 |
|
302 onPageAnnotationSet: function() {}, |
|
303 onPageAnnotationRemoved: function() {}, |
|
304 |
|
305 |
|
306 ////////////////////////////////////////////////////////////////////////////// |
|
307 //// nsITransactionListener |
|
308 |
|
309 didDo: function PU_didDo(aManager, aTransaction, aDoResult) |
|
310 { |
|
311 updateCommandsOnActiveWindow(); |
|
312 }, |
|
313 |
|
314 didUndo: function PU_didUndo(aManager, aTransaction, aUndoResult) |
|
315 { |
|
316 updateCommandsOnActiveWindow(); |
|
317 }, |
|
318 |
|
319 didRedo: function PU_didRedo(aManager, aTransaction, aRedoResult) |
|
320 { |
|
321 updateCommandsOnActiveWindow(); |
|
322 }, |
|
323 |
|
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 }, |
|
341 |
|
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() {}, |
|
350 |
|
351 |
|
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 } |
|
364 |
|
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 }, |
|
371 |
|
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 }, |
|
384 |
|
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 }, |
|
399 |
|
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 }, |
|
411 |
|
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 }, |
|
424 |
|
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 }, |
|
441 |
|
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 }, |
|
452 |
|
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 }, |
|
469 |
|
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 } |
|
495 |
|
496 // If we didn't create our own query, do not alter the node's open state. |
|
497 return [cNode, false]; |
|
498 } |
|
499 |
|
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 } |
|
511 |
|
512 function isLivemark(aNode) { |
|
513 return PlacesUtils.nodeIsFolder(aNode) && |
|
514 PlacesUtils.annotations |
|
515 .itemHasAnnotation(aNode.itemId, |
|
516 PlacesUtils.LMANNO_FEEDURI); |
|
517 } |
|
518 |
|
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 }; |
|
529 |
|
530 let [node, shouldClose] = convertNode(aNode); |
|
531 this._serializeNodeAsJSONToOutputStream(node, writer); |
|
532 if (shouldClose) |
|
533 node.containerOpen = false; |
|
534 |
|
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 } |
|
542 |
|
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 } |
|
548 |
|
549 let [node, shouldClose] = convertNode(aNode); |
|
550 let dataUrl = gatherDataUrl(node); |
|
551 if (shouldClose) |
|
552 node.containerOpen = false; |
|
553 |
|
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) : ""; |
|
568 |
|
569 if (isLivemark(bNode)) { |
|
570 return "<A HREF=\"" + gatherLivemarkUrl(bNode) + "\">" + escapedTitle + "</A>" + NEWLINE; |
|
571 } |
|
572 |
|
573 if (PlacesUtils.nodeIsContainer(bNode)) { |
|
574 asContainer(bNode); |
|
575 let wasOpen = bNode.containerOpen; |
|
576 if (!wasOpen) |
|
577 bNode.containerOpen = true; |
|
578 |
|
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 } |
|
596 |
|
597 let [node, shouldClose] = convertNode(aNode); |
|
598 let dataHtml = gatherDataHtml(node); |
|
599 if (shouldClose) |
|
600 node.containerOpen = false; |
|
601 |
|
602 return dataHtml; |
|
603 } |
|
604 } |
|
605 |
|
606 // Otherwise, we wrap as TYPE_UNICODE. |
|
607 function gatherDataText(bNode) { |
|
608 if (isLivemark(bNode)) { |
|
609 return gatherLivemarkUrl(bNode); |
|
610 } |
|
611 |
|
612 if (PlacesUtils.nodeIsContainer(bNode)) { |
|
613 asContainer(bNode); |
|
614 let wasOpen = bNode.containerOpen; |
|
615 if (!wasOpen) |
|
616 bNode.containerOpen = true; |
|
617 |
|
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 } |
|
634 |
|
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; |
|
640 |
|
641 return dataText; |
|
642 }, |
|
643 |
|
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 }, |
|
710 |
|
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; |
|
732 |
|
733 var result = this.history.executeQuery(query, options); |
|
734 result.root.containerOpen = true; |
|
735 return result; |
|
736 }, |
|
737 |
|
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 }, |
|
761 |
|
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 }, |
|
786 |
|
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 }, |
|
810 |
|
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; |
|
822 |
|
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 }, |
|
836 |
|
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 }, |
|
844 |
|
845 get bookmarksMenuFolderId() { |
|
846 delete this.bookmarksMenuFolderId; |
|
847 return this.bookmarksMenuFolderId = this.bookmarks.bookmarksMenuFolder; |
|
848 }, |
|
849 |
|
850 get toolbarFolderId() { |
|
851 delete this.toolbarFolderId; |
|
852 return this.toolbarFolderId = this.bookmarks.toolbarFolder; |
|
853 }, |
|
854 |
|
855 get tagsFolderId() { |
|
856 delete this.tagsFolderId; |
|
857 return this.tagsFolderId = this.bookmarks.tagsFolder; |
|
858 }, |
|
859 |
|
860 get unfiledBookmarksFolderId() { |
|
861 delete this.unfiledBookmarksFolderId; |
|
862 return this.unfiledBookmarksFolderId = this.bookmarks.unfiledBookmarksFolder; |
|
863 }, |
|
864 |
|
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 }, |
|
879 |
|
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 }, |
|
894 |
|
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); |
|
904 |
|
905 return null; |
|
906 }, |
|
907 |
|
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 }, |
|
932 |
|
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); |
|
939 |
|
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 }, |
|
950 |
|
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; |
|
972 |
|
973 var grandparentId = this.bookmarks.getFolderIdForItem(parentId); |
|
974 if (grandparentId != this.tagsFolderId) |
|
975 return itemId; |
|
976 } |
|
977 return -1; |
|
978 }, |
|
979 |
|
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; |
|
1001 |
|
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; |
|
1008 |
|
1009 // If our options are exactly what we expect, directly return the node. |
|
1010 if (excludeItems == aExcludeItems && expandQueries == aExpandQueries) |
|
1011 return aNode; |
|
1012 |
|
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 }, |
|
1022 |
|
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; |
|
1033 |
|
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; |
|
1042 |
|
1043 root.containerOpen = true; |
|
1044 } |
|
1045 |
|
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 } |
|
1052 |
|
1053 if (!wasOpen) { |
|
1054 root.containerOpen = false; |
|
1055 if (!didSuppressNotifications) |
|
1056 result.suppressNotifications = false; |
|
1057 } |
|
1058 return found; |
|
1059 }, |
|
1060 |
|
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; |
|
1073 |
|
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; |
|
1082 |
|
1083 root.containerOpen = true; |
|
1084 } |
|
1085 |
|
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 } |
|
1091 |
|
1092 if (!wasOpen) { |
|
1093 root.containerOpen = false; |
|
1094 if (!didSuppressNotifications) |
|
1095 result.suppressNotifications = false; |
|
1096 } |
|
1097 return urls; |
|
1098 }, |
|
1099 |
|
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; |
|
1127 |
|
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 } |
|
1146 |
|
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 } |
|
1156 |
|
1157 if (aPlacesNode.tags) |
|
1158 aJSNode.tags = aPlacesNode.tags; |
|
1159 |
|
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 } |
|
1168 |
|
1169 function addSeparatorProperties(aPlacesNode, aJSNode) { |
|
1170 aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR; |
|
1171 } |
|
1172 |
|
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; |
|
1186 |
|
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 } |
|
1206 |
|
1207 function appendConvertedComplexNode(aNode, aSourceNode, aArray) { |
|
1208 var repr = {}; |
|
1209 |
|
1210 for (let [name, value] in Iterator(aNode)) |
|
1211 repr[name] = value; |
|
1212 |
|
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 } |
|
1228 |
|
1229 aArray.push(repr); |
|
1230 return true; |
|
1231 } |
|
1232 |
|
1233 function appendConvertedNode(bNode, aIndex, aArray) { |
|
1234 var node = {}; |
|
1235 |
|
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; |
|
1241 |
|
1242 addGenericProperties(bNode, node); |
|
1243 |
|
1244 var parent = bNode.parent; |
|
1245 var grandParent = parent ? parent.parent : null; |
|
1246 if (grandParent) |
|
1247 node.grandParentId = grandParent.itemId; |
|
1248 |
|
1249 if (PlacesUtils.nodeIsURI(bNode)) { |
|
1250 // Tag root accept only folder nodes |
|
1251 if (parent && parent.itemId == PlacesUtils.tagsFolderId) |
|
1252 return false; |
|
1253 |
|
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 } |
|
1262 |
|
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; |
|
1269 |
|
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; |
|
1278 |
|
1279 addSeparatorProperties(bNode, node); |
|
1280 } |
|
1281 |
|
1282 if (!node.feedURI && node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) |
|
1283 return appendConvertedComplexNode(node, bNode, aArray); |
|
1284 |
|
1285 aArray.push(node); |
|
1286 return true; |
|
1287 } |
|
1288 |
|
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 }, |
|
1299 |
|
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 } |
|
1329 |
|
1330 let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; |
|
1331 this._asyncGetBookmarksStmt.params.url = url; |
|
1332 |
|
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 }, |
|
1352 |
|
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 }, |
|
1378 |
|
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 }, |
|
1401 |
|
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(); |
|
1411 |
|
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); |
|
1425 |
|
1426 return deferred.promise; |
|
1427 }, |
|
1428 |
|
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(); |
|
1438 |
|
1439 Services.tm.mainThread.dispatch(function() { |
|
1440 let charset = null; |
|
1441 |
|
1442 try { |
|
1443 charset = PlacesUtils.annotations.getPageAnnotation(aURI, |
|
1444 PlacesUtils.CHARSET_ANNO); |
|
1445 } catch (ex) { } |
|
1446 |
|
1447 deferred.resolve(charset); |
|
1448 }, Ci.nsIThread.DISPATCH_NORMAL); |
|
1449 |
|
1450 return deferred.promise; |
|
1451 }, |
|
1452 |
|
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 }); |
|
1474 |
|
1475 return deferred.promise; |
|
1476 }, |
|
1477 |
|
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 }); |
|
1499 |
|
1500 return deferred.promise; |
|
1501 }, |
|
1502 |
|
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 }, |
|
1526 |
|
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), |
|
1538 |
|
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 }; |
|
1551 |
|
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 } |
|
1586 |
|
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 }); |
|
1593 |
|
1594 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "asyncHistory", |
|
1595 "@mozilla.org/browser/history;1", |
|
1596 "mozIAsyncHistory"); |
|
1597 |
|
1598 XPCOMUtils.defineLazyGetter(PlacesUtils, "bhistory", function() { |
|
1599 return PlacesUtils.history; |
|
1600 }); |
|
1601 |
|
1602 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "favicons", |
|
1603 "@mozilla.org/browser/favicon-service;1", |
|
1604 "mozIAsyncFavicons"); |
|
1605 |
|
1606 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "bookmarks", |
|
1607 "@mozilla.org/browser/nav-bookmarks-service;1", |
|
1608 "nsINavBookmarksService"); |
|
1609 |
|
1610 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "annotations", |
|
1611 "@mozilla.org/browser/annotation-service;1", |
|
1612 "nsIAnnotationService"); |
|
1613 |
|
1614 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "tagging", |
|
1615 "@mozilla.org/browser/tagging-service;1", |
|
1616 "nsITaggingService"); |
|
1617 |
|
1618 XPCOMUtils.defineLazyGetter(PlacesUtils, "livemarks", function() { |
|
1619 return Cc["@mozilla.org/browser/livemark-service;2"]. |
|
1620 getService(Ci.mozIAsyncLivemarks); |
|
1621 }); |
|
1622 |
|
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 }); |
|
1633 |
|
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 }); |
|
1654 |
|
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 }); |
|
1661 |
|
1662 XPCOMUtils.defineLazyServiceGetter(this, "focusManager", |
|
1663 "@mozilla.org/focus-manager;1", |
|
1664 "nsIFocusManager"); |
|
1665 |
|
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(), |
|
1685 |
|
1686 getItemId: function (aGUID) { |
|
1687 if (this.idsForGUIDs.has(aGUID)) |
|
1688 return Promise.resolve(this.idsForGUIDs.get(aGUID)); |
|
1689 |
|
1690 let deferred = Promise.defer(); |
|
1691 let itemId = -1; |
|
1692 |
|
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); |
|
1704 |
|
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 }); |
|
1715 |
|
1716 return deferred.promise; |
|
1717 }, |
|
1718 |
|
1719 getItemGUID: function (aItemId) { |
|
1720 if (this.GUIDsForIds.has(aItemId)) |
|
1721 return Promise.resolve(this.GUIDsForIds.get(aItemId)); |
|
1722 |
|
1723 let deferred = Promise.defer(); |
|
1724 let guid = ""; |
|
1725 |
|
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); |
|
1738 |
|
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 }); |
|
1749 |
|
1750 return deferred.promise; |
|
1751 }, |
|
1752 |
|
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 }, |
|
1774 |
|
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 }); |
|
1797 |
|
1798 //////////////////////////////////////////////////////////////////////////////// |
|
1799 //// Transactions handlers. |
|
1800 |
|
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 } |
|
1813 |
|
1814 |
|
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 } |
|
1824 |
|
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 }; |
|
1866 |
|
1867 |
|
1868 /** |
|
1869 * Base transaction implementation. |
|
1870 * |
|
1871 * @note used internally, DO NOT EXPORT. |
|
1872 */ |
|
1873 function BaseTransaction() |
|
1874 { |
|
1875 } |
|
1876 |
|
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 }; |
|
1892 |
|
1893 |
|
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(); |
|
1912 |
|
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 } |
|
1926 |
|
1927 let txnCount = countTransactions(this.childTransactions, 0); |
|
1928 this._useBatch = txnCount >= MIN_TRANSACTIONS_FOR_BATCH; |
|
1929 } |
|
1930 |
|
1931 PlacesAggregatedTransaction.prototype = { |
|
1932 __proto__: BaseTransaction.prototype, |
|
1933 |
|
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 }, |
|
1942 |
|
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 }, |
|
1951 |
|
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 }; |
|
1970 |
|
1971 |
|
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 } |
|
1999 |
|
2000 PlacesCreateFolderTransaction.prototype = { |
|
2001 __proto__: BaseTransaction.prototype, |
|
2002 |
|
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); |
|
2010 |
|
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 } |
|
2016 |
|
2017 let txn = new PlacesAggregatedTransaction("Create folder childTxn", |
|
2018 this.childTransactions); |
|
2019 txn.doTransaction(); |
|
2020 } |
|
2021 }, |
|
2022 |
|
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 } |
|
2030 |
|
2031 // Remove item only after all child transactions have been reverted. |
|
2032 PlacesUtils.bookmarks.removeItem(this.item.id); |
|
2033 } |
|
2034 }; |
|
2035 |
|
2036 |
|
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 } |
|
2076 |
|
2077 PlacesCreateBookmarkTransaction.prototype = { |
|
2078 __proto__: BaseTransaction.prototype, |
|
2079 |
|
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); |
|
2092 |
|
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 }, |
|
2103 |
|
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 } |
|
2112 |
|
2113 // Remove item only after all child transactions have been reverted. |
|
2114 PlacesUtils.bookmarks.removeItem(this.item.id); |
|
2115 } |
|
2116 }; |
|
2117 |
|
2118 |
|
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 } |
|
2136 |
|
2137 PlacesCreateSeparatorTransaction.prototype = { |
|
2138 __proto__: BaseTransaction.prototype, |
|
2139 |
|
2140 doTransaction: function CSTXN_doTransaction() |
|
2141 { |
|
2142 this.item.id = |
|
2143 PlacesUtils.bookmarks.insertSeparator(this.item.parentId, this.item.index); |
|
2144 }, |
|
2145 |
|
2146 undoTransaction: function CSTXN_undoTransaction() |
|
2147 { |
|
2148 PlacesUtils.bookmarks.removeItem(this.item.id); |
|
2149 } |
|
2150 }; |
|
2151 |
|
2152 |
|
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 } |
|
2185 |
|
2186 PlacesCreateLivemarkTransaction.prototype = { |
|
2187 __proto__: BaseTransaction.prototype, |
|
2188 |
|
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 }, |
|
2205 |
|
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 }; |
|
2216 |
|
2217 |
|
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); |
|
2233 |
|
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 } |
|
2245 |
|
2246 PlacesRemoveLivemarkTransaction.prototype = { |
|
2247 __proto__: BaseTransaction.prototype, |
|
2248 |
|
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 }, |
|
2258 |
|
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 }; |
|
2282 |
|
2283 |
|
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 } |
|
2306 |
|
2307 PlacesMoveItemTransaction.prototype = { |
|
2308 __proto__: BaseTransaction.prototype, |
|
2309 |
|
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 }, |
|
2317 |
|
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 }; |
|
2333 |
|
2334 |
|
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; |
|
2348 |
|
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 } |
|
2356 |
|
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); |
|
2361 |
|
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 } |
|
2376 |
|
2377 if (this.item.itemType != Ci.nsINavBookmarksService.TYPE_SEPARATOR) |
|
2378 this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id); |
|
2379 |
|
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 } |
|
2386 |
|
2387 PlacesRemoveItemTransaction.prototype = { |
|
2388 __proto__: BaseTransaction.prototype, |
|
2389 |
|
2390 doTransaction: function RITXN_doTransaction() |
|
2391 { |
|
2392 this.item.index = PlacesUtils.bookmarks.getItemIndex(this.item.id); |
|
2393 |
|
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; |
|
2403 |
|
2404 PlacesUtils.bookmarks.removeItem(this.item.id); |
|
2405 |
|
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 }, |
|
2413 |
|
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 } |
|
2437 |
|
2438 if (this.item.annotations && this.item.annotations.length > 0) |
|
2439 PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations); |
|
2440 |
|
2441 PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.item.dateAdded); |
|
2442 PlacesUtils.bookmarks.setItemLastModified(this.item.id, |
|
2443 this.item.lastModified); |
|
2444 }, |
|
2445 |
|
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 }; |
|
2465 |
|
2466 |
|
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 } |
|
2485 |
|
2486 PlacesEditItemTitleTransaction.prototype = { |
|
2487 __proto__: BaseTransaction.prototype, |
|
2488 |
|
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 }, |
|
2494 |
|
2495 undoTransaction: function EITTXN_undoTransaction() |
|
2496 { |
|
2497 PlacesUtils.bookmarks.setItemTitle(this.item.id, this.item.title); |
|
2498 } |
|
2499 }; |
|
2500 |
|
2501 |
|
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 } |
|
2519 |
|
2520 PlacesEditBookmarkURITransaction.prototype = { |
|
2521 __proto__: BaseTransaction.prototype, |
|
2522 |
|
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 }, |
|
2536 |
|
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 }; |
|
2549 |
|
2550 |
|
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 } |
|
2571 |
|
2572 PlacesSetItemAnnotationTransaction.prototype = { |
|
2573 __proto__: BaseTransaction.prototype, |
|
2574 |
|
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 } |
|
2598 |
|
2599 PlacesUtils.setAnnotationsForItem(this.item.id, this.new.annotations); |
|
2600 }, |
|
2601 |
|
2602 undoTransaction: function SIATXN_undoTransaction() |
|
2603 { |
|
2604 PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations); |
|
2605 } |
|
2606 }; |
|
2607 |
|
2608 |
|
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 } |
|
2629 |
|
2630 PlacesSetPageAnnotationTransaction.prototype = { |
|
2631 __proto__: BaseTransaction.prototype, |
|
2632 |
|
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 } |
|
2656 |
|
2657 PlacesUtils.setAnnotationsForURI(this.item.uri, this.new.annotations); |
|
2658 }, |
|
2659 |
|
2660 undoTransaction: function SPATXN_undoTransaction() |
|
2661 { |
|
2662 PlacesUtils.setAnnotationsForURI(this.item.uri, this.item.annotations); |
|
2663 } |
|
2664 }; |
|
2665 |
|
2666 |
|
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 } |
|
2685 |
|
2686 PlacesEditBookmarkKeywordTransaction.prototype = { |
|
2687 __proto__: BaseTransaction.prototype, |
|
2688 |
|
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 }, |
|
2694 |
|
2695 undoTransaction: function EBKTXN_undoTransaction() |
|
2696 { |
|
2697 PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.item.keyword); |
|
2698 } |
|
2699 }; |
|
2700 |
|
2701 |
|
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 } |
|
2720 |
|
2721 PlacesEditBookmarkPostDataTransaction.prototype = { |
|
2722 __proto__: BaseTransaction.prototype, |
|
2723 |
|
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 }, |
|
2729 |
|
2730 undoTransaction: function EBPDTXN_undoTransaction() |
|
2731 { |
|
2732 PlacesUtils.setPostDataForBookmark(this.item.id, this.item.postData); |
|
2733 } |
|
2734 }; |
|
2735 |
|
2736 |
|
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 } |
|
2755 |
|
2756 PlacesEditItemDateAddedTransaction.prototype = { |
|
2757 __proto__: BaseTransaction.prototype, |
|
2758 |
|
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 }, |
|
2768 |
|
2769 undoTransaction: function EIDATXN_undoTransaction() |
|
2770 { |
|
2771 PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.item.dateAdded); |
|
2772 } |
|
2773 }; |
|
2774 |
|
2775 |
|
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 } |
|
2794 |
|
2795 PlacesEditItemLastModifiedTransaction.prototype = { |
|
2796 __proto__: BaseTransaction.prototype, |
|
2797 |
|
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 }, |
|
2809 |
|
2810 undoTransaction: |
|
2811 function EILMTXN_undoTransaction() |
|
2812 { |
|
2813 PlacesUtils.bookmarks.setItemLastModified(this.item.id, |
|
2814 this.item.lastModified); |
|
2815 } |
|
2816 }; |
|
2817 |
|
2818 |
|
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 } |
|
2833 |
|
2834 PlacesSortFolderByNameTransaction.prototype = { |
|
2835 __proto__: BaseTransaction.prototype, |
|
2836 |
|
2837 doTransaction: function SFBNTXN_doTransaction() |
|
2838 { |
|
2839 this._oldOrder = []; |
|
2840 |
|
2841 let contents = |
|
2842 PlacesUtils.getFolderContents(this.item.id, false, false).root; |
|
2843 let count = contents.childCount; |
|
2844 |
|
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 }; |
|
2856 |
|
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; |
|
2872 |
|
2873 if (preSep.length > 0) { |
|
2874 preSep.sort(sortingMethod); |
|
2875 newOrder = newOrder.concat(preSep); |
|
2876 } |
|
2877 |
|
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 }, |
|
2888 |
|
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 }; |
|
2901 |
|
2902 |
|
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 } |
|
2921 |
|
2922 PlacesTagURITransaction.prototype = { |
|
2923 __proto__: BaseTransaction.prototype, |
|
2924 |
|
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 }, |
|
2939 |
|
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 }; |
|
2949 |
|
2950 |
|
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 } |
|
2981 |
|
2982 PlacesUntagURITransaction.prototype = { |
|
2983 __proto__: BaseTransaction.prototype, |
|
2984 |
|
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 }, |
|
2995 |
|
2996 undoTransaction: function UTUTXN_undoTransaction() |
|
2997 { |
|
2998 PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags); |
|
2999 } |
|
3000 }; |