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