toolkit/components/places/BookmarkJSONUtils.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/places/BookmarkJSONUtils.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,513 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +this.EXPORTED_SYMBOLS = [ "BookmarkJSONUtils" ];
     1.9 +
    1.10 +const Ci = Components.interfaces;
    1.11 +const Cc = Components.classes;
    1.12 +const Cu = Components.utils;
    1.13 +const Cr = Components.results;
    1.14 +
    1.15 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.16 +Cu.import("resource://gre/modules/Services.jsm");
    1.17 +Cu.import("resource://gre/modules/NetUtil.jsm");
    1.18 +Cu.import("resource://gre/modules/osfile.jsm");
    1.19 +Cu.import("resource://gre/modules/PlacesUtils.jsm");
    1.20 +Cu.import("resource://gre/modules/Promise.jsm");
    1.21 +Cu.import("resource://gre/modules/Task.jsm");
    1.22 +
    1.23 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
    1.24 +  "resource://gre/modules/PlacesBackups.jsm");
    1.25 +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
    1.26 +  "resource://gre/modules/Deprecated.jsm");
    1.27 +
    1.28 +XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder());
    1.29 +XPCOMUtils.defineLazyGetter(this, "gTextEncoder", () => new TextEncoder());
    1.30 +
    1.31 +/**
    1.32 + * Generates an hash for the given string.
    1.33 + *
    1.34 + * @note The generated hash is returned in base64 form.  Mind the fact base64
    1.35 + * is case-sensitive if you are going to reuse this code.
    1.36 + */
    1.37 +function generateHash(aString) {
    1.38 +  let cryptoHash = Cc["@mozilla.org/security/hash;1"]
    1.39 +                     .createInstance(Ci.nsICryptoHash);
    1.40 +  cryptoHash.init(Ci.nsICryptoHash.MD5);
    1.41 +  let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
    1.42 +                       .createInstance(Ci.nsIStringInputStream);
    1.43 +  stringStream.data = aString;
    1.44 +  cryptoHash.updateFromStream(stringStream, -1);
    1.45 +  // base64 allows the '/' char, but we can't use it for filenames.
    1.46 +  return cryptoHash.finish(true).replace("/", "-", "g");
    1.47 +}
    1.48 +
    1.49 +this.BookmarkJSONUtils = Object.freeze({
    1.50 +  /**
    1.51 +   * Import bookmarks from a url.
    1.52 +   *
    1.53 +   * @param aSpec
    1.54 +   *        url of the bookmark data.
    1.55 +   * @param aReplace
    1.56 +   *        Boolean if true, replace existing bookmarks, else merge.
    1.57 +   *
    1.58 +   * @return {Promise}
    1.59 +   * @resolves When the new bookmarks have been created.
    1.60 +   * @rejects JavaScript exception.
    1.61 +   */
    1.62 +  importFromURL: function BJU_importFromURL(aSpec, aReplace) {
    1.63 +    return Task.spawn(function* () {
    1.64 +      notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN);
    1.65 +      try {
    1.66 +        let importer = new BookmarkImporter(aReplace);
    1.67 +        yield importer.importFromURL(aSpec);
    1.68 +
    1.69 +        notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS);
    1.70 +      } catch(ex) {
    1.71 +        Cu.reportError("Failed to restore bookmarks from " + aSpec + ": " + ex);
    1.72 +        notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
    1.73 +      }
    1.74 +    });
    1.75 +  },
    1.76 +
    1.77 +  /**
    1.78 +   * Restores bookmarks and tags from a JSON file.
    1.79 +   * @note any item annotated with "places/excludeFromBackup" won't be removed
    1.80 +   *       before executing the restore.
    1.81 +   *
    1.82 +   * @param aFilePath
    1.83 +   *        OS.File path string of bookmarks in JSON format to be restored.
    1.84 +   * @param aReplace
    1.85 +   *        Boolean if true, replace existing bookmarks, else merge.
    1.86 +   *
    1.87 +   * @return {Promise}
    1.88 +   * @resolves When the new bookmarks have been created.
    1.89 +   * @rejects JavaScript exception.
    1.90 +   * @deprecated passing an nsIFile is deprecated
    1.91 +   */
    1.92 +  importFromFile: function BJU_importFromFile(aFilePath, aReplace) {
    1.93 +    if (aFilePath instanceof Ci.nsIFile) {
    1.94 +      Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.importFromFile " +
    1.95 +                         "is deprecated. Please use an OS.File path string instead.",
    1.96 +                         "https://developer.mozilla.org/docs/JavaScript_OS.File");
    1.97 +      aFilePath = aFilePath.path;
    1.98 +    }
    1.99 +
   1.100 +    return Task.spawn(function* () {
   1.101 +      notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN);
   1.102 +      try {
   1.103 +        if (!(yield OS.File.exists(aFilePath)))
   1.104 +          throw new Error("Cannot restore from nonexisting json file");
   1.105 +
   1.106 +        let importer = new BookmarkImporter(aReplace);
   1.107 +        yield importer.importFromURL(OS.Path.toFileURI(aFilePath));
   1.108 +
   1.109 +        notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS);
   1.110 +      } catch(ex) {
   1.111 +        Cu.reportError("Failed to restore bookmarks from " + aFilePath + ": " + ex);
   1.112 +        notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
   1.113 +        throw ex;
   1.114 +      }
   1.115 +    });
   1.116 +  },
   1.117 +
   1.118 +  /**
   1.119 +   * Serializes bookmarks using JSON, and writes to the supplied file path.
   1.120 +   *
   1.121 +   * @param aFilePath
   1.122 +   *        OS.File path string for the "bookmarks.json" file to be created.
   1.123 +   * @param [optional] aOptions
   1.124 +   *        Object containing options for the export:
   1.125 +   *         - failIfHashIs: if the generated file would have the same hash
   1.126 +   *                         defined here, will reject with ex.becauseSameHash
   1.127 +   * @return {Promise}
   1.128 +   * @resolves once the file has been created, to an object with the
   1.129 +   *           following properties:
   1.130 +   *            - count: number of exported bookmarks
   1.131 +   *            - hash: file hash for contents comparison
   1.132 +   * @rejects JavaScript exception.
   1.133 +   * @deprecated passing an nsIFile is deprecated
   1.134 +   */
   1.135 +  exportToFile: function BJU_exportToFile(aFilePath, aOptions={}) {
   1.136 +    if (aFilePath instanceof Ci.nsIFile) {
   1.137 +      Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.exportToFile " +
   1.138 +                         "is deprecated. Please use an OS.File path string instead.",
   1.139 +                         "https://developer.mozilla.org/docs/JavaScript_OS.File");
   1.140 +      aFilePath = aFilePath.path;
   1.141 +    }
   1.142 +    return Task.spawn(function* () {
   1.143 +      let [bookmarks, count] = yield PlacesBackups.getBookmarksTree();
   1.144 +      let startTime = Date.now();
   1.145 +      let jsonString = JSON.stringify(bookmarks);
   1.146 +      // Report the time taken to convert the tree to JSON.
   1.147 +      try {
   1.148 +        Services.telemetry
   1.149 +                .getHistogramById("PLACES_BACKUPS_TOJSON_MS")
   1.150 +                .add(Date.now() - startTime);
   1.151 +      } catch (ex) {
   1.152 +        Components.utils.reportError("Unable to report telemetry.");
   1.153 +      }
   1.154 +
   1.155 +      startTime = Date.now();
   1.156 +      let hash = generateHash(jsonString);
   1.157 +      // Report the time taken to generate the hash.
   1.158 +      try {
   1.159 +        Services.telemetry
   1.160 +                .getHistogramById("PLACES_BACKUPS_HASHING_MS")
   1.161 +                .add(Date.now() - startTime);
   1.162 +      } catch (ex) {
   1.163 +        Components.utils.reportError("Unable to report telemetry.");
   1.164 +      }
   1.165 +
   1.166 +      if (hash === aOptions.failIfHashIs) {
   1.167 +        let e = new Error("Hash conflict");
   1.168 +        e.becauseSameHash = true;
   1.169 +        throw e;
   1.170 +      }
   1.171 +
   1.172 +      // Do not write to the tmp folder, otherwise if it has a different
   1.173 +      // filesystem writeAtomic will fail.  Eventual dangling .tmp files should
   1.174 +      // be cleaned up by the caller.
   1.175 +      yield OS.File.writeAtomic(aFilePath, jsonString,
   1.176 +                                { tmpPath: OS.Path.join(aFilePath + ".tmp") });
   1.177 +      return { count: count, hash: hash };
   1.178 +    });
   1.179 +  }
   1.180 +});
   1.181 +
   1.182 +function BookmarkImporter(aReplace) {
   1.183 +  this._replace = aReplace;
   1.184 +}
   1.185 +BookmarkImporter.prototype = {
   1.186 +  /**
   1.187 +   * Import bookmarks from a url.
   1.188 +   *
   1.189 +   * @param aSpec
   1.190 +   *        url of the bookmark data.
   1.191 +   *
   1.192 +   * @return {Promise}
   1.193 +   * @resolves When the new bookmarks have been created.
   1.194 +   * @rejects JavaScript exception.
   1.195 +   */
   1.196 +  importFromURL: function BI_importFromURL(aSpec) {
   1.197 +    let deferred = Promise.defer();
   1.198 +
   1.199 +    let streamObserver = {
   1.200 +      onStreamComplete: function (aLoader, aContext, aStatus, aLength,
   1.201 +                                  aResult) {
   1.202 +        let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
   1.203 +                        createInstance(Ci.nsIScriptableUnicodeConverter);
   1.204 +        converter.charset = "UTF-8";
   1.205 +
   1.206 +        try {
   1.207 +          let jsonString = converter.convertFromByteArray(aResult,
   1.208 +                                                          aResult.length);
   1.209 +          deferred.resolve(this.importFromJSON(jsonString));
   1.210 +        } catch (ex) {
   1.211 +          Cu.reportError("Failed to import from URL: " + ex);
   1.212 +          deferred.reject(ex);
   1.213 +          throw ex;
   1.214 +        }
   1.215 +      }.bind(this)
   1.216 +    };
   1.217 +
   1.218 +    try {
   1.219 +      let channel = Services.io.newChannelFromURI(NetUtil.newURI(aSpec));
   1.220 +      let streamLoader = Cc["@mozilla.org/network/stream-loader;1"].
   1.221 +                         createInstance(Ci.nsIStreamLoader);
   1.222 +
   1.223 +      streamLoader.init(streamObserver);
   1.224 +      channel.asyncOpen(streamLoader, channel);
   1.225 +    } catch (ex) {
   1.226 +      deferred.reject(ex);
   1.227 +    }
   1.228 +
   1.229 +    return deferred.promise;
   1.230 +  },
   1.231 +
   1.232 +  /**
   1.233 +   * Import bookmarks from a JSON string.
   1.234 +   *
   1.235 +   * @param aString
   1.236 +   *        JSON string of serialized bookmark data.
   1.237 +   */
   1.238 +  importFromJSON: function BI_importFromJSON(aString) {
   1.239 +    let deferred = Promise.defer();
   1.240 +    let nodes =
   1.241 +      PlacesUtils.unwrapNodes(aString, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
   1.242 +
   1.243 +    if (nodes.length == 0 || !nodes[0].children ||
   1.244 +        nodes[0].children.length == 0) {
   1.245 +      deferred.resolve(); // Nothing to restore
   1.246 +    } else {
   1.247 +      // Ensure tag folder gets processed last
   1.248 +      nodes[0].children.sort(function sortRoots(aNode, bNode) {
   1.249 +        return (aNode.root && aNode.root == "tagsFolder") ? 1 :
   1.250 +               (bNode.root && bNode.root == "tagsFolder") ? -1 : 0;
   1.251 +      });
   1.252 +
   1.253 +      let batch = {
   1.254 +        nodes: nodes[0].children,
   1.255 +        runBatched: function runBatched() {
   1.256 +          if (this._replace) {
   1.257 +            // Get roots excluded from the backup, we will not remove them
   1.258 +            // before restoring.
   1.259 +            let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
   1.260 +                                 PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
   1.261 +            // Delete existing children of the root node, excepting:
   1.262 +            // 1. special folders: delete the child nodes
   1.263 +            // 2. tags folder: untag via the tagging api
   1.264 +            let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
   1.265 +                                                   false, false).root;
   1.266 +            let childIds = [];
   1.267 +            for (let i = 0; i < root.childCount; i++) {
   1.268 +              let childId = root.getChild(i).itemId;
   1.269 +              if (excludeItems.indexOf(childId) == -1 &&
   1.270 +                  childId != PlacesUtils.tagsFolderId) {
   1.271 +                childIds.push(childId);
   1.272 +              }
   1.273 +            }
   1.274 +            root.containerOpen = false;
   1.275 +
   1.276 +            for (let i = 0; i < childIds.length; i++) {
   1.277 +              let rootItemId = childIds[i];
   1.278 +              if (PlacesUtils.isRootItem(rootItemId)) {
   1.279 +                PlacesUtils.bookmarks.removeFolderChildren(rootItemId);
   1.280 +              } else {
   1.281 +                PlacesUtils.bookmarks.removeItem(rootItemId);
   1.282 +              }
   1.283 +            }
   1.284 +          }
   1.285 +
   1.286 +          let searchIds = [];
   1.287 +          let folderIdMap = [];
   1.288 +
   1.289 +          for (let node of batch.nodes) {
   1.290 +            if (!node.children || node.children.length == 0)
   1.291 +              continue; // Nothing to restore for this root
   1.292 +
   1.293 +            if (node.root) {
   1.294 +              let container = PlacesUtils.placesRootId; // Default to places root
   1.295 +              switch (node.root) {
   1.296 +                case "bookmarksMenuFolder":
   1.297 +                  container = PlacesUtils.bookmarksMenuFolderId;
   1.298 +                  break;
   1.299 +                case "tagsFolder":
   1.300 +                  container = PlacesUtils.tagsFolderId;
   1.301 +                  break;
   1.302 +                case "unfiledBookmarksFolder":
   1.303 +                  container = PlacesUtils.unfiledBookmarksFolderId;
   1.304 +                  break;
   1.305 +                case "toolbarFolder":
   1.306 +                  container = PlacesUtils.toolbarFolderId;
   1.307 +                  break;
   1.308 +              }
   1.309 +
   1.310 +              // Insert the data into the db
   1.311 +              for (let child of node.children) {
   1.312 +                let index = child.index;
   1.313 +                let [folders, searches] =
   1.314 +                  this.importJSONNode(child, container, index, 0);
   1.315 +                for (let i = 0; i < folders.length; i++) {
   1.316 +                  if (folders[i])
   1.317 +                    folderIdMap[i] = folders[i];
   1.318 +                }
   1.319 +                searchIds = searchIds.concat(searches);
   1.320 +              }
   1.321 +            } else {
   1.322 +              this.importJSONNode(
   1.323 +                node, PlacesUtils.placesRootId, node.index, 0);
   1.324 +            }
   1.325 +          }
   1.326 +
   1.327 +          // Fixup imported place: uris that contain folders
   1.328 +          searchIds.forEach(function(aId) {
   1.329 +            let oldURI = PlacesUtils.bookmarks.getBookmarkURI(aId);
   1.330 +            let uri = fixupQuery(oldURI, folderIdMap);
   1.331 +            if (!uri.equals(oldURI)) {
   1.332 +              PlacesUtils.bookmarks.changeBookmarkURI(aId, uri);
   1.333 +            }
   1.334 +          });
   1.335 +
   1.336 +          deferred.resolve();
   1.337 +        }.bind(this)
   1.338 +      };
   1.339 +
   1.340 +      PlacesUtils.bookmarks.runInBatchMode(batch, null);
   1.341 +    }
   1.342 +    return deferred.promise;
   1.343 +  },
   1.344 +
   1.345 +  /**
   1.346 +   * Takes a JSON-serialized node and inserts it into the db.
   1.347 +   *
   1.348 +   * @param aData
   1.349 +   *        The unwrapped data blob of dropped or pasted data.
   1.350 +   * @param aContainer
   1.351 +   *        The container the data was dropped or pasted into
   1.352 +   * @param aIndex
   1.353 +   *        The index within the container the item was dropped or pasted at
   1.354 +   * @return an array containing of maps of old folder ids to new folder ids,
   1.355 +   *         and an array of saved search ids that need to be fixed up.
   1.356 +   *         eg: [[[oldFolder1, newFolder1]], [search1]]
   1.357 +   */
   1.358 +  importJSONNode: function BI_importJSONNode(aData, aContainer, aIndex,
   1.359 +                                             aGrandParentId) {
   1.360 +    let folderIdMap = [];
   1.361 +    let searchIds = [];
   1.362 +    let id = -1;
   1.363 +    switch (aData.type) {
   1.364 +      case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
   1.365 +        if (aContainer == PlacesUtils.tagsFolderId) {
   1.366 +          // Node is a tag
   1.367 +          if (aData.children) {
   1.368 +            aData.children.forEach(function(aChild) {
   1.369 +              try {
   1.370 +                PlacesUtils.tagging.tagURI(
   1.371 +                  NetUtil.newURI(aChild.uri), [aData.title]);
   1.372 +              } catch (ex) {
   1.373 +                // Invalid tag child, skip it
   1.374 +              }
   1.375 +            });
   1.376 +            return [folderIdMap, searchIds];
   1.377 +          }
   1.378 +        } else if (aData.annos &&
   1.379 +                   aData.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
   1.380 +          // Node is a livemark
   1.381 +          let feedURI = null;
   1.382 +          let siteURI = null;
   1.383 +          aData.annos = aData.annos.filter(function(aAnno) {
   1.384 +            switch (aAnno.name) {
   1.385 +              case PlacesUtils.LMANNO_FEEDURI:
   1.386 +                feedURI = NetUtil.newURI(aAnno.value);
   1.387 +                return false;
   1.388 +              case PlacesUtils.LMANNO_SITEURI:
   1.389 +                siteURI = NetUtil.newURI(aAnno.value);
   1.390 +                return false;
   1.391 +              default:
   1.392 +                return true;
   1.393 +            }
   1.394 +          });
   1.395 +
   1.396 +          if (feedURI) {
   1.397 +            PlacesUtils.livemarks.addLivemark({
   1.398 +              title: aData.title,
   1.399 +              feedURI: feedURI,
   1.400 +              parentId: aContainer,
   1.401 +              index: aIndex,
   1.402 +              lastModified: aData.lastModified,
   1.403 +              siteURI: siteURI
   1.404 +            }).then(function (aLivemark) {
   1.405 +              let id = aLivemark.id;
   1.406 +              if (aData.dateAdded)
   1.407 +                PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded);
   1.408 +              if (aData.annos && aData.annos.length)
   1.409 +                PlacesUtils.setAnnotationsForItem(id, aData.annos);
   1.410 +            }, Cu.reportError);
   1.411 +          }
   1.412 +        } else {
   1.413 +          id = PlacesUtils.bookmarks.createFolder(
   1.414 +                 aContainer, aData.title, aIndex);
   1.415 +          folderIdMap[aData.id] = id;
   1.416 +          // Process children
   1.417 +          if (aData.children) {
   1.418 +            for (let i = 0; i < aData.children.length; i++) {
   1.419 +              let child = aData.children[i];
   1.420 +              let [folders, searches] =
   1.421 +                this.importJSONNode(child, id, i, aContainer);
   1.422 +              for (let j = 0; j < folders.length; j++) {
   1.423 +                if (folders[j])
   1.424 +                  folderIdMap[j] = folders[j];
   1.425 +              }
   1.426 +              searchIds = searchIds.concat(searches);
   1.427 +            }
   1.428 +          }
   1.429 +        }
   1.430 +        break;
   1.431 +      case PlacesUtils.TYPE_X_MOZ_PLACE:
   1.432 +        id = PlacesUtils.bookmarks.insertBookmark(
   1.433 +               aContainer, NetUtil.newURI(aData.uri), aIndex, aData.title);
   1.434 +        if (aData.keyword)
   1.435 +          PlacesUtils.bookmarks.setKeywordForBookmark(id, aData.keyword);
   1.436 +        if (aData.tags) {
   1.437 +          // TODO (bug 967196) the tagging service should trim by itself.
   1.438 +          let tags = aData.tags.split(",").map(tag => tag.trim());
   1.439 +          if (tags.length)
   1.440 +            PlacesUtils.tagging.tagURI(NetUtil.newURI(aData.uri), tags);
   1.441 +        }
   1.442 +        if (aData.charset) {
   1.443 +          PlacesUtils.annotations.setPageAnnotation(
   1.444 +            NetUtil.newURI(aData.uri), PlacesUtils.CHARSET_ANNO, aData.charset,
   1.445 +            0, Ci.nsIAnnotationService.EXPIRE_NEVER);
   1.446 +        }
   1.447 +        if (aData.uri.substr(0, 6) == "place:")
   1.448 +          searchIds.push(id);
   1.449 +        if (aData.icon) {
   1.450 +          try {
   1.451 +            // Create a fake faviconURI to use (FIXME: bug 523932)
   1.452 +            let faviconURI = NetUtil.newURI("fake-favicon-uri:" + aData.uri);
   1.453 +            PlacesUtils.favicons.replaceFaviconDataFromDataURL(
   1.454 +              faviconURI, aData.icon, 0);
   1.455 +            PlacesUtils.favicons.setAndFetchFaviconForPage(
   1.456 +              NetUtil.newURI(aData.uri), faviconURI, false,
   1.457 +              PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
   1.458 +          } catch (ex) {
   1.459 +            Components.utils.reportError("Failed to import favicon data:" + ex);
   1.460 +          }
   1.461 +        }
   1.462 +        if (aData.iconUri) {
   1.463 +          try {
   1.464 +            PlacesUtils.favicons.setAndFetchFaviconForPage(
   1.465 +              NetUtil.newURI(aData.uri), NetUtil.newURI(aData.iconUri), false,
   1.466 +              PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
   1.467 +          } catch (ex) {
   1.468 +            Components.utils.reportError("Failed to import favicon URI:" + ex);
   1.469 +          }
   1.470 +        }
   1.471 +        break;
   1.472 +      case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
   1.473 +        id = PlacesUtils.bookmarks.insertSeparator(aContainer, aIndex);
   1.474 +        break;
   1.475 +      default:
   1.476 +        // Unknown node type
   1.477 +    }
   1.478 +
   1.479 +    // Set generic properties, valid for all nodes
   1.480 +    if (id != -1 && aContainer != PlacesUtils.tagsFolderId &&
   1.481 +        aGrandParentId != PlacesUtils.tagsFolderId) {
   1.482 +      if (aData.dateAdded)
   1.483 +        PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded);
   1.484 +      if (aData.lastModified)
   1.485 +        PlacesUtils.bookmarks.setItemLastModified(id, aData.lastModified);
   1.486 +      if (aData.annos && aData.annos.length)
   1.487 +        PlacesUtils.setAnnotationsForItem(id, aData.annos);
   1.488 +    }
   1.489 +
   1.490 +    return [folderIdMap, searchIds];
   1.491 +  }
   1.492 +}
   1.493 +
   1.494 +function notifyObservers(topic) {
   1.495 +  Services.obs.notifyObservers(null, topic, "json");
   1.496 +}
   1.497 +
   1.498 +/**
   1.499 + * Replaces imported folder ids with their local counterparts in a place: URI.
   1.500 + *
   1.501 + * @param   aURI
   1.502 + *          A place: URI with folder ids.
   1.503 + * @param   aFolderIdMap
   1.504 + *          An array mapping old folder id to new folder ids.
   1.505 + * @returns the fixed up URI if all matched. If some matched, it returns
   1.506 + *          the URI with only the matching folders included. If none matched
   1.507 + *          it returns the input URI unchanged.
   1.508 + */
   1.509 +function fixupQuery(aQueryURI, aFolderIdMap) {
   1.510 +  let convert = function(str, p1, offset, s) {
   1.511 +    return "folder=" + aFolderIdMap[p1];
   1.512 +  }
   1.513 +  let stringURI = aQueryURI.spec.replace(/folder=([0-9]+)/g, convert);
   1.514 +
   1.515 +  return NetUtil.newURI(stringURI);
   1.516 +}

mercurial