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 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | this.EXPORTED_SYMBOLS = [ "BookmarkJSONUtils" ]; |
michael@0 | 6 | |
michael@0 | 7 | const Ci = Components.interfaces; |
michael@0 | 8 | const Cc = Components.classes; |
michael@0 | 9 | const Cu = Components.utils; |
michael@0 | 10 | const Cr = Components.results; |
michael@0 | 11 | |
michael@0 | 12 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 13 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 14 | Cu.import("resource://gre/modules/NetUtil.jsm"); |
michael@0 | 15 | Cu.import("resource://gre/modules/osfile.jsm"); |
michael@0 | 16 | Cu.import("resource://gre/modules/PlacesUtils.jsm"); |
michael@0 | 17 | Cu.import("resource://gre/modules/Promise.jsm"); |
michael@0 | 18 | Cu.import("resource://gre/modules/Task.jsm"); |
michael@0 | 19 | |
michael@0 | 20 | XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", |
michael@0 | 21 | "resource://gre/modules/PlacesBackups.jsm"); |
michael@0 | 22 | XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", |
michael@0 | 23 | "resource://gre/modules/Deprecated.jsm"); |
michael@0 | 24 | |
michael@0 | 25 | XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder()); |
michael@0 | 26 | XPCOMUtils.defineLazyGetter(this, "gTextEncoder", () => new TextEncoder()); |
michael@0 | 27 | |
michael@0 | 28 | /** |
michael@0 | 29 | * Generates an hash for the given string. |
michael@0 | 30 | * |
michael@0 | 31 | * @note The generated hash is returned in base64 form. Mind the fact base64 |
michael@0 | 32 | * is case-sensitive if you are going to reuse this code. |
michael@0 | 33 | */ |
michael@0 | 34 | function generateHash(aString) { |
michael@0 | 35 | let cryptoHash = Cc["@mozilla.org/security/hash;1"] |
michael@0 | 36 | .createInstance(Ci.nsICryptoHash); |
michael@0 | 37 | cryptoHash.init(Ci.nsICryptoHash.MD5); |
michael@0 | 38 | let stringStream = Cc["@mozilla.org/io/string-input-stream;1"] |
michael@0 | 39 | .createInstance(Ci.nsIStringInputStream); |
michael@0 | 40 | stringStream.data = aString; |
michael@0 | 41 | cryptoHash.updateFromStream(stringStream, -1); |
michael@0 | 42 | // base64 allows the '/' char, but we can't use it for filenames. |
michael@0 | 43 | return cryptoHash.finish(true).replace("/", "-", "g"); |
michael@0 | 44 | } |
michael@0 | 45 | |
michael@0 | 46 | this.BookmarkJSONUtils = Object.freeze({ |
michael@0 | 47 | /** |
michael@0 | 48 | * Import bookmarks from a url. |
michael@0 | 49 | * |
michael@0 | 50 | * @param aSpec |
michael@0 | 51 | * url of the bookmark data. |
michael@0 | 52 | * @param aReplace |
michael@0 | 53 | * Boolean if true, replace existing bookmarks, else merge. |
michael@0 | 54 | * |
michael@0 | 55 | * @return {Promise} |
michael@0 | 56 | * @resolves When the new bookmarks have been created. |
michael@0 | 57 | * @rejects JavaScript exception. |
michael@0 | 58 | */ |
michael@0 | 59 | importFromURL: function BJU_importFromURL(aSpec, aReplace) { |
michael@0 | 60 | return Task.spawn(function* () { |
michael@0 | 61 | notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN); |
michael@0 | 62 | try { |
michael@0 | 63 | let importer = new BookmarkImporter(aReplace); |
michael@0 | 64 | yield importer.importFromURL(aSpec); |
michael@0 | 65 | |
michael@0 | 66 | notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS); |
michael@0 | 67 | } catch(ex) { |
michael@0 | 68 | Cu.reportError("Failed to restore bookmarks from " + aSpec + ": " + ex); |
michael@0 | 69 | notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED); |
michael@0 | 70 | } |
michael@0 | 71 | }); |
michael@0 | 72 | }, |
michael@0 | 73 | |
michael@0 | 74 | /** |
michael@0 | 75 | * Restores bookmarks and tags from a JSON file. |
michael@0 | 76 | * @note any item annotated with "places/excludeFromBackup" won't be removed |
michael@0 | 77 | * before executing the restore. |
michael@0 | 78 | * |
michael@0 | 79 | * @param aFilePath |
michael@0 | 80 | * OS.File path string of bookmarks in JSON format to be restored. |
michael@0 | 81 | * @param aReplace |
michael@0 | 82 | * Boolean if true, replace existing bookmarks, else merge. |
michael@0 | 83 | * |
michael@0 | 84 | * @return {Promise} |
michael@0 | 85 | * @resolves When the new bookmarks have been created. |
michael@0 | 86 | * @rejects JavaScript exception. |
michael@0 | 87 | * @deprecated passing an nsIFile is deprecated |
michael@0 | 88 | */ |
michael@0 | 89 | importFromFile: function BJU_importFromFile(aFilePath, aReplace) { |
michael@0 | 90 | if (aFilePath instanceof Ci.nsIFile) { |
michael@0 | 91 | Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.importFromFile " + |
michael@0 | 92 | "is deprecated. Please use an OS.File path string instead.", |
michael@0 | 93 | "https://developer.mozilla.org/docs/JavaScript_OS.File"); |
michael@0 | 94 | aFilePath = aFilePath.path; |
michael@0 | 95 | } |
michael@0 | 96 | |
michael@0 | 97 | return Task.spawn(function* () { |
michael@0 | 98 | notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN); |
michael@0 | 99 | try { |
michael@0 | 100 | if (!(yield OS.File.exists(aFilePath))) |
michael@0 | 101 | throw new Error("Cannot restore from nonexisting json file"); |
michael@0 | 102 | |
michael@0 | 103 | let importer = new BookmarkImporter(aReplace); |
michael@0 | 104 | yield importer.importFromURL(OS.Path.toFileURI(aFilePath)); |
michael@0 | 105 | |
michael@0 | 106 | notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS); |
michael@0 | 107 | } catch(ex) { |
michael@0 | 108 | Cu.reportError("Failed to restore bookmarks from " + aFilePath + ": " + ex); |
michael@0 | 109 | notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED); |
michael@0 | 110 | throw ex; |
michael@0 | 111 | } |
michael@0 | 112 | }); |
michael@0 | 113 | }, |
michael@0 | 114 | |
michael@0 | 115 | /** |
michael@0 | 116 | * Serializes bookmarks using JSON, and writes to the supplied file path. |
michael@0 | 117 | * |
michael@0 | 118 | * @param aFilePath |
michael@0 | 119 | * OS.File path string for the "bookmarks.json" file to be created. |
michael@0 | 120 | * @param [optional] aOptions |
michael@0 | 121 | * Object containing options for the export: |
michael@0 | 122 | * - failIfHashIs: if the generated file would have the same hash |
michael@0 | 123 | * defined here, will reject with ex.becauseSameHash |
michael@0 | 124 | * @return {Promise} |
michael@0 | 125 | * @resolves once the file has been created, to an object with the |
michael@0 | 126 | * following properties: |
michael@0 | 127 | * - count: number of exported bookmarks |
michael@0 | 128 | * - hash: file hash for contents comparison |
michael@0 | 129 | * @rejects JavaScript exception. |
michael@0 | 130 | * @deprecated passing an nsIFile is deprecated |
michael@0 | 131 | */ |
michael@0 | 132 | exportToFile: function BJU_exportToFile(aFilePath, aOptions={}) { |
michael@0 | 133 | if (aFilePath instanceof Ci.nsIFile) { |
michael@0 | 134 | Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.exportToFile " + |
michael@0 | 135 | "is deprecated. Please use an OS.File path string instead.", |
michael@0 | 136 | "https://developer.mozilla.org/docs/JavaScript_OS.File"); |
michael@0 | 137 | aFilePath = aFilePath.path; |
michael@0 | 138 | } |
michael@0 | 139 | return Task.spawn(function* () { |
michael@0 | 140 | let [bookmarks, count] = yield PlacesBackups.getBookmarksTree(); |
michael@0 | 141 | let startTime = Date.now(); |
michael@0 | 142 | let jsonString = JSON.stringify(bookmarks); |
michael@0 | 143 | // Report the time taken to convert the tree to JSON. |
michael@0 | 144 | try { |
michael@0 | 145 | Services.telemetry |
michael@0 | 146 | .getHistogramById("PLACES_BACKUPS_TOJSON_MS") |
michael@0 | 147 | .add(Date.now() - startTime); |
michael@0 | 148 | } catch (ex) { |
michael@0 | 149 | Components.utils.reportError("Unable to report telemetry."); |
michael@0 | 150 | } |
michael@0 | 151 | |
michael@0 | 152 | startTime = Date.now(); |
michael@0 | 153 | let hash = generateHash(jsonString); |
michael@0 | 154 | // Report the time taken to generate the hash. |
michael@0 | 155 | try { |
michael@0 | 156 | Services.telemetry |
michael@0 | 157 | .getHistogramById("PLACES_BACKUPS_HASHING_MS") |
michael@0 | 158 | .add(Date.now() - startTime); |
michael@0 | 159 | } catch (ex) { |
michael@0 | 160 | Components.utils.reportError("Unable to report telemetry."); |
michael@0 | 161 | } |
michael@0 | 162 | |
michael@0 | 163 | if (hash === aOptions.failIfHashIs) { |
michael@0 | 164 | let e = new Error("Hash conflict"); |
michael@0 | 165 | e.becauseSameHash = true; |
michael@0 | 166 | throw e; |
michael@0 | 167 | } |
michael@0 | 168 | |
michael@0 | 169 | // Do not write to the tmp folder, otherwise if it has a different |
michael@0 | 170 | // filesystem writeAtomic will fail. Eventual dangling .tmp files should |
michael@0 | 171 | // be cleaned up by the caller. |
michael@0 | 172 | yield OS.File.writeAtomic(aFilePath, jsonString, |
michael@0 | 173 | { tmpPath: OS.Path.join(aFilePath + ".tmp") }); |
michael@0 | 174 | return { count: count, hash: hash }; |
michael@0 | 175 | }); |
michael@0 | 176 | } |
michael@0 | 177 | }); |
michael@0 | 178 | |
michael@0 | 179 | function BookmarkImporter(aReplace) { |
michael@0 | 180 | this._replace = aReplace; |
michael@0 | 181 | } |
michael@0 | 182 | BookmarkImporter.prototype = { |
michael@0 | 183 | /** |
michael@0 | 184 | * Import bookmarks from a url. |
michael@0 | 185 | * |
michael@0 | 186 | * @param aSpec |
michael@0 | 187 | * url of the bookmark data. |
michael@0 | 188 | * |
michael@0 | 189 | * @return {Promise} |
michael@0 | 190 | * @resolves When the new bookmarks have been created. |
michael@0 | 191 | * @rejects JavaScript exception. |
michael@0 | 192 | */ |
michael@0 | 193 | importFromURL: function BI_importFromURL(aSpec) { |
michael@0 | 194 | let deferred = Promise.defer(); |
michael@0 | 195 | |
michael@0 | 196 | let streamObserver = { |
michael@0 | 197 | onStreamComplete: function (aLoader, aContext, aStatus, aLength, |
michael@0 | 198 | aResult) { |
michael@0 | 199 | let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. |
michael@0 | 200 | createInstance(Ci.nsIScriptableUnicodeConverter); |
michael@0 | 201 | converter.charset = "UTF-8"; |
michael@0 | 202 | |
michael@0 | 203 | try { |
michael@0 | 204 | let jsonString = converter.convertFromByteArray(aResult, |
michael@0 | 205 | aResult.length); |
michael@0 | 206 | deferred.resolve(this.importFromJSON(jsonString)); |
michael@0 | 207 | } catch (ex) { |
michael@0 | 208 | Cu.reportError("Failed to import from URL: " + ex); |
michael@0 | 209 | deferred.reject(ex); |
michael@0 | 210 | throw ex; |
michael@0 | 211 | } |
michael@0 | 212 | }.bind(this) |
michael@0 | 213 | }; |
michael@0 | 214 | |
michael@0 | 215 | try { |
michael@0 | 216 | let channel = Services.io.newChannelFromURI(NetUtil.newURI(aSpec)); |
michael@0 | 217 | let streamLoader = Cc["@mozilla.org/network/stream-loader;1"]. |
michael@0 | 218 | createInstance(Ci.nsIStreamLoader); |
michael@0 | 219 | |
michael@0 | 220 | streamLoader.init(streamObserver); |
michael@0 | 221 | channel.asyncOpen(streamLoader, channel); |
michael@0 | 222 | } catch (ex) { |
michael@0 | 223 | deferred.reject(ex); |
michael@0 | 224 | } |
michael@0 | 225 | |
michael@0 | 226 | return deferred.promise; |
michael@0 | 227 | }, |
michael@0 | 228 | |
michael@0 | 229 | /** |
michael@0 | 230 | * Import bookmarks from a JSON string. |
michael@0 | 231 | * |
michael@0 | 232 | * @param aString |
michael@0 | 233 | * JSON string of serialized bookmark data. |
michael@0 | 234 | */ |
michael@0 | 235 | importFromJSON: function BI_importFromJSON(aString) { |
michael@0 | 236 | let deferred = Promise.defer(); |
michael@0 | 237 | let nodes = |
michael@0 | 238 | PlacesUtils.unwrapNodes(aString, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER); |
michael@0 | 239 | |
michael@0 | 240 | if (nodes.length == 0 || !nodes[0].children || |
michael@0 | 241 | nodes[0].children.length == 0) { |
michael@0 | 242 | deferred.resolve(); // Nothing to restore |
michael@0 | 243 | } else { |
michael@0 | 244 | // Ensure tag folder gets processed last |
michael@0 | 245 | nodes[0].children.sort(function sortRoots(aNode, bNode) { |
michael@0 | 246 | return (aNode.root && aNode.root == "tagsFolder") ? 1 : |
michael@0 | 247 | (bNode.root && bNode.root == "tagsFolder") ? -1 : 0; |
michael@0 | 248 | }); |
michael@0 | 249 | |
michael@0 | 250 | let batch = { |
michael@0 | 251 | nodes: nodes[0].children, |
michael@0 | 252 | runBatched: function runBatched() { |
michael@0 | 253 | if (this._replace) { |
michael@0 | 254 | // Get roots excluded from the backup, we will not remove them |
michael@0 | 255 | // before restoring. |
michael@0 | 256 | let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation( |
michael@0 | 257 | PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO); |
michael@0 | 258 | // Delete existing children of the root node, excepting: |
michael@0 | 259 | // 1. special folders: delete the child nodes |
michael@0 | 260 | // 2. tags folder: untag via the tagging api |
michael@0 | 261 | let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, |
michael@0 | 262 | false, false).root; |
michael@0 | 263 | let childIds = []; |
michael@0 | 264 | for (let i = 0; i < root.childCount; i++) { |
michael@0 | 265 | let childId = root.getChild(i).itemId; |
michael@0 | 266 | if (excludeItems.indexOf(childId) == -1 && |
michael@0 | 267 | childId != PlacesUtils.tagsFolderId) { |
michael@0 | 268 | childIds.push(childId); |
michael@0 | 269 | } |
michael@0 | 270 | } |
michael@0 | 271 | root.containerOpen = false; |
michael@0 | 272 | |
michael@0 | 273 | for (let i = 0; i < childIds.length; i++) { |
michael@0 | 274 | let rootItemId = childIds[i]; |
michael@0 | 275 | if (PlacesUtils.isRootItem(rootItemId)) { |
michael@0 | 276 | PlacesUtils.bookmarks.removeFolderChildren(rootItemId); |
michael@0 | 277 | } else { |
michael@0 | 278 | PlacesUtils.bookmarks.removeItem(rootItemId); |
michael@0 | 279 | } |
michael@0 | 280 | } |
michael@0 | 281 | } |
michael@0 | 282 | |
michael@0 | 283 | let searchIds = []; |
michael@0 | 284 | let folderIdMap = []; |
michael@0 | 285 | |
michael@0 | 286 | for (let node of batch.nodes) { |
michael@0 | 287 | if (!node.children || node.children.length == 0) |
michael@0 | 288 | continue; // Nothing to restore for this root |
michael@0 | 289 | |
michael@0 | 290 | if (node.root) { |
michael@0 | 291 | let container = PlacesUtils.placesRootId; // Default to places root |
michael@0 | 292 | switch (node.root) { |
michael@0 | 293 | case "bookmarksMenuFolder": |
michael@0 | 294 | container = PlacesUtils.bookmarksMenuFolderId; |
michael@0 | 295 | break; |
michael@0 | 296 | case "tagsFolder": |
michael@0 | 297 | container = PlacesUtils.tagsFolderId; |
michael@0 | 298 | break; |
michael@0 | 299 | case "unfiledBookmarksFolder": |
michael@0 | 300 | container = PlacesUtils.unfiledBookmarksFolderId; |
michael@0 | 301 | break; |
michael@0 | 302 | case "toolbarFolder": |
michael@0 | 303 | container = PlacesUtils.toolbarFolderId; |
michael@0 | 304 | break; |
michael@0 | 305 | } |
michael@0 | 306 | |
michael@0 | 307 | // Insert the data into the db |
michael@0 | 308 | for (let child of node.children) { |
michael@0 | 309 | let index = child.index; |
michael@0 | 310 | let [folders, searches] = |
michael@0 | 311 | this.importJSONNode(child, container, index, 0); |
michael@0 | 312 | for (let i = 0; i < folders.length; i++) { |
michael@0 | 313 | if (folders[i]) |
michael@0 | 314 | folderIdMap[i] = folders[i]; |
michael@0 | 315 | } |
michael@0 | 316 | searchIds = searchIds.concat(searches); |
michael@0 | 317 | } |
michael@0 | 318 | } else { |
michael@0 | 319 | this.importJSONNode( |
michael@0 | 320 | node, PlacesUtils.placesRootId, node.index, 0); |
michael@0 | 321 | } |
michael@0 | 322 | } |
michael@0 | 323 | |
michael@0 | 324 | // Fixup imported place: uris that contain folders |
michael@0 | 325 | searchIds.forEach(function(aId) { |
michael@0 | 326 | let oldURI = PlacesUtils.bookmarks.getBookmarkURI(aId); |
michael@0 | 327 | let uri = fixupQuery(oldURI, folderIdMap); |
michael@0 | 328 | if (!uri.equals(oldURI)) { |
michael@0 | 329 | PlacesUtils.bookmarks.changeBookmarkURI(aId, uri); |
michael@0 | 330 | } |
michael@0 | 331 | }); |
michael@0 | 332 | |
michael@0 | 333 | deferred.resolve(); |
michael@0 | 334 | }.bind(this) |
michael@0 | 335 | }; |
michael@0 | 336 | |
michael@0 | 337 | PlacesUtils.bookmarks.runInBatchMode(batch, null); |
michael@0 | 338 | } |
michael@0 | 339 | return deferred.promise; |
michael@0 | 340 | }, |
michael@0 | 341 | |
michael@0 | 342 | /** |
michael@0 | 343 | * Takes a JSON-serialized node and inserts it into the db. |
michael@0 | 344 | * |
michael@0 | 345 | * @param aData |
michael@0 | 346 | * The unwrapped data blob of dropped or pasted data. |
michael@0 | 347 | * @param aContainer |
michael@0 | 348 | * The container the data was dropped or pasted into |
michael@0 | 349 | * @param aIndex |
michael@0 | 350 | * The index within the container the item was dropped or pasted at |
michael@0 | 351 | * @return an array containing of maps of old folder ids to new folder ids, |
michael@0 | 352 | * and an array of saved search ids that need to be fixed up. |
michael@0 | 353 | * eg: [[[oldFolder1, newFolder1]], [search1]] |
michael@0 | 354 | */ |
michael@0 | 355 | importJSONNode: function BI_importJSONNode(aData, aContainer, aIndex, |
michael@0 | 356 | aGrandParentId) { |
michael@0 | 357 | let folderIdMap = []; |
michael@0 | 358 | let searchIds = []; |
michael@0 | 359 | let id = -1; |
michael@0 | 360 | switch (aData.type) { |
michael@0 | 361 | case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER: |
michael@0 | 362 | if (aContainer == PlacesUtils.tagsFolderId) { |
michael@0 | 363 | // Node is a tag |
michael@0 | 364 | if (aData.children) { |
michael@0 | 365 | aData.children.forEach(function(aChild) { |
michael@0 | 366 | try { |
michael@0 | 367 | PlacesUtils.tagging.tagURI( |
michael@0 | 368 | NetUtil.newURI(aChild.uri), [aData.title]); |
michael@0 | 369 | } catch (ex) { |
michael@0 | 370 | // Invalid tag child, skip it |
michael@0 | 371 | } |
michael@0 | 372 | }); |
michael@0 | 373 | return [folderIdMap, searchIds]; |
michael@0 | 374 | } |
michael@0 | 375 | } else if (aData.annos && |
michael@0 | 376 | aData.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) { |
michael@0 | 377 | // Node is a livemark |
michael@0 | 378 | let feedURI = null; |
michael@0 | 379 | let siteURI = null; |
michael@0 | 380 | aData.annos = aData.annos.filter(function(aAnno) { |
michael@0 | 381 | switch (aAnno.name) { |
michael@0 | 382 | case PlacesUtils.LMANNO_FEEDURI: |
michael@0 | 383 | feedURI = NetUtil.newURI(aAnno.value); |
michael@0 | 384 | return false; |
michael@0 | 385 | case PlacesUtils.LMANNO_SITEURI: |
michael@0 | 386 | siteURI = NetUtil.newURI(aAnno.value); |
michael@0 | 387 | return false; |
michael@0 | 388 | default: |
michael@0 | 389 | return true; |
michael@0 | 390 | } |
michael@0 | 391 | }); |
michael@0 | 392 | |
michael@0 | 393 | if (feedURI) { |
michael@0 | 394 | PlacesUtils.livemarks.addLivemark({ |
michael@0 | 395 | title: aData.title, |
michael@0 | 396 | feedURI: feedURI, |
michael@0 | 397 | parentId: aContainer, |
michael@0 | 398 | index: aIndex, |
michael@0 | 399 | lastModified: aData.lastModified, |
michael@0 | 400 | siteURI: siteURI |
michael@0 | 401 | }).then(function (aLivemark) { |
michael@0 | 402 | let id = aLivemark.id; |
michael@0 | 403 | if (aData.dateAdded) |
michael@0 | 404 | PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded); |
michael@0 | 405 | if (aData.annos && aData.annos.length) |
michael@0 | 406 | PlacesUtils.setAnnotationsForItem(id, aData.annos); |
michael@0 | 407 | }, Cu.reportError); |
michael@0 | 408 | } |
michael@0 | 409 | } else { |
michael@0 | 410 | id = PlacesUtils.bookmarks.createFolder( |
michael@0 | 411 | aContainer, aData.title, aIndex); |
michael@0 | 412 | folderIdMap[aData.id] = id; |
michael@0 | 413 | // Process children |
michael@0 | 414 | if (aData.children) { |
michael@0 | 415 | for (let i = 0; i < aData.children.length; i++) { |
michael@0 | 416 | let child = aData.children[i]; |
michael@0 | 417 | let [folders, searches] = |
michael@0 | 418 | this.importJSONNode(child, id, i, aContainer); |
michael@0 | 419 | for (let j = 0; j < folders.length; j++) { |
michael@0 | 420 | if (folders[j]) |
michael@0 | 421 | folderIdMap[j] = folders[j]; |
michael@0 | 422 | } |
michael@0 | 423 | searchIds = searchIds.concat(searches); |
michael@0 | 424 | } |
michael@0 | 425 | } |
michael@0 | 426 | } |
michael@0 | 427 | break; |
michael@0 | 428 | case PlacesUtils.TYPE_X_MOZ_PLACE: |
michael@0 | 429 | id = PlacesUtils.bookmarks.insertBookmark( |
michael@0 | 430 | aContainer, NetUtil.newURI(aData.uri), aIndex, aData.title); |
michael@0 | 431 | if (aData.keyword) |
michael@0 | 432 | PlacesUtils.bookmarks.setKeywordForBookmark(id, aData.keyword); |
michael@0 | 433 | if (aData.tags) { |
michael@0 | 434 | // TODO (bug 967196) the tagging service should trim by itself. |
michael@0 | 435 | let tags = aData.tags.split(",").map(tag => tag.trim()); |
michael@0 | 436 | if (tags.length) |
michael@0 | 437 | PlacesUtils.tagging.tagURI(NetUtil.newURI(aData.uri), tags); |
michael@0 | 438 | } |
michael@0 | 439 | if (aData.charset) { |
michael@0 | 440 | PlacesUtils.annotations.setPageAnnotation( |
michael@0 | 441 | NetUtil.newURI(aData.uri), PlacesUtils.CHARSET_ANNO, aData.charset, |
michael@0 | 442 | 0, Ci.nsIAnnotationService.EXPIRE_NEVER); |
michael@0 | 443 | } |
michael@0 | 444 | if (aData.uri.substr(0, 6) == "place:") |
michael@0 | 445 | searchIds.push(id); |
michael@0 | 446 | if (aData.icon) { |
michael@0 | 447 | try { |
michael@0 | 448 | // Create a fake faviconURI to use (FIXME: bug 523932) |
michael@0 | 449 | let faviconURI = NetUtil.newURI("fake-favicon-uri:" + aData.uri); |
michael@0 | 450 | PlacesUtils.favicons.replaceFaviconDataFromDataURL( |
michael@0 | 451 | faviconURI, aData.icon, 0); |
michael@0 | 452 | PlacesUtils.favicons.setAndFetchFaviconForPage( |
michael@0 | 453 | NetUtil.newURI(aData.uri), faviconURI, false, |
michael@0 | 454 | PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE); |
michael@0 | 455 | } catch (ex) { |
michael@0 | 456 | Components.utils.reportError("Failed to import favicon data:" + ex); |
michael@0 | 457 | } |
michael@0 | 458 | } |
michael@0 | 459 | if (aData.iconUri) { |
michael@0 | 460 | try { |
michael@0 | 461 | PlacesUtils.favicons.setAndFetchFaviconForPage( |
michael@0 | 462 | NetUtil.newURI(aData.uri), NetUtil.newURI(aData.iconUri), false, |
michael@0 | 463 | PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE); |
michael@0 | 464 | } catch (ex) { |
michael@0 | 465 | Components.utils.reportError("Failed to import favicon URI:" + ex); |
michael@0 | 466 | } |
michael@0 | 467 | } |
michael@0 | 468 | break; |
michael@0 | 469 | case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR: |
michael@0 | 470 | id = PlacesUtils.bookmarks.insertSeparator(aContainer, aIndex); |
michael@0 | 471 | break; |
michael@0 | 472 | default: |
michael@0 | 473 | // Unknown node type |
michael@0 | 474 | } |
michael@0 | 475 | |
michael@0 | 476 | // Set generic properties, valid for all nodes |
michael@0 | 477 | if (id != -1 && aContainer != PlacesUtils.tagsFolderId && |
michael@0 | 478 | aGrandParentId != PlacesUtils.tagsFolderId) { |
michael@0 | 479 | if (aData.dateAdded) |
michael@0 | 480 | PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded); |
michael@0 | 481 | if (aData.lastModified) |
michael@0 | 482 | PlacesUtils.bookmarks.setItemLastModified(id, aData.lastModified); |
michael@0 | 483 | if (aData.annos && aData.annos.length) |
michael@0 | 484 | PlacesUtils.setAnnotationsForItem(id, aData.annos); |
michael@0 | 485 | } |
michael@0 | 486 | |
michael@0 | 487 | return [folderIdMap, searchIds]; |
michael@0 | 488 | } |
michael@0 | 489 | } |
michael@0 | 490 | |
michael@0 | 491 | function notifyObservers(topic) { |
michael@0 | 492 | Services.obs.notifyObservers(null, topic, "json"); |
michael@0 | 493 | } |
michael@0 | 494 | |
michael@0 | 495 | /** |
michael@0 | 496 | * Replaces imported folder ids with their local counterparts in a place: URI. |
michael@0 | 497 | * |
michael@0 | 498 | * @param aURI |
michael@0 | 499 | * A place: URI with folder ids. |
michael@0 | 500 | * @param aFolderIdMap |
michael@0 | 501 | * An array mapping old folder id to new folder ids. |
michael@0 | 502 | * @returns the fixed up URI if all matched. If some matched, it returns |
michael@0 | 503 | * the URI with only the matching folders included. If none matched |
michael@0 | 504 | * it returns the input URI unchanged. |
michael@0 | 505 | */ |
michael@0 | 506 | function fixupQuery(aQueryURI, aFolderIdMap) { |
michael@0 | 507 | let convert = function(str, p1, offset, s) { |
michael@0 | 508 | return "folder=" + aFolderIdMap[p1]; |
michael@0 | 509 | } |
michael@0 | 510 | let stringURI = aQueryURI.spec.replace(/folder=([0-9]+)/g, convert); |
michael@0 | 511 | |
michael@0 | 512 | return NetUtil.newURI(stringURI); |
michael@0 | 513 | } |