toolkit/components/places/BookmarkHTMLUtils.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/places/BookmarkHTMLUtils.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1178 @@
     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 +/**
     1.9 + * This file works on the old-style "bookmarks.html" file.  It includes
    1.10 + * functions to import and export existing bookmarks to this file format.
    1.11 + *
    1.12 + * Format
    1.13 + * ------
    1.14 + *
    1.15 + * Primary heading := h1
    1.16 + *   Old version used this to set attributes on the bookmarks RDF root, such
    1.17 + *   as the last modified date. We only use H1 to check for the attribute
    1.18 + *   PLACES_ROOT, which tells us that this hierarchy root is the places root.
    1.19 + *   For backwards compatibility, if we don't find this, we assume that the
    1.20 + *   hierarchy is rooted at the bookmarks menu.
    1.21 + * Heading := any heading other than h1
    1.22 + *   Old version used this to set attributes on the current container. We only
    1.23 + *   care about the content of the heading container, which contains the title
    1.24 + *   of the bookmark container.
    1.25 + * Bookmark := a
    1.26 + *   HREF is the destination of the bookmark
    1.27 + *   FEEDURL is the URI of the RSS feed if this is a livemark.
    1.28 + *   LAST_CHARSET is stored as an annotation so that the next time we go to
    1.29 + *     that page we remember the user's preference.
    1.30 + *   WEB_PANEL is set to "true" if the bookmark should be loaded in the sidebar.
    1.31 + *   ICON will be stored in the favicon service
    1.32 + *   ICON_URI is new for places bookmarks.html, it refers to the original
    1.33 + *     URI of the favicon so we don't have to make up favicon URLs.
    1.34 + *   Text of the <a> container is the name of the bookmark
    1.35 + *   Ignored: LAST_VISIT, ID (writing out non-RDF IDs can confuse Firefox 2)
    1.36 + * Bookmark comment := dd
    1.37 + *   This affects the previosly added bookmark
    1.38 + * Separator := hr
    1.39 + *   Insert a separator into the current container
    1.40 + * The folder hierarchy is defined by <dl>/<ul>/<menu> (the old importing code
    1.41 + *     handles all these cases, when we write, use <dl>).
    1.42 + *
    1.43 + * Overall design
    1.44 + * --------------
    1.45 + *
    1.46 + * We need to emulate a recursive parser. A "Bookmark import frame" is created
    1.47 + * corresponding to each folder we encounter. These are arranged in a stack,
    1.48 + * and contain all the state we need to keep track of.
    1.49 + *
    1.50 + * A frame is created when we find a heading, which defines a new container.
    1.51 + * The frame also keeps track of the nesting of <DL>s, (in well-formed
    1.52 + * bookmarks files, these will have a 1-1 correspondence with frames, but we
    1.53 + * try to be a little more flexible here). When the nesting count decreases
    1.54 + * to 0, then we know a frame is complete and to pop back to the previous
    1.55 + * frame.
    1.56 + *
    1.57 + * Note that a lot of things happen when tags are CLOSED because we need to
    1.58 + * get the text from the content of the tag. For example, link and heading tags
    1.59 + * both require the content (= title) before actually creating it.
    1.60 + */
    1.61 +
    1.62 +this.EXPORTED_SYMBOLS = [ "BookmarkHTMLUtils" ];
    1.63 +
    1.64 +const Ci = Components.interfaces;
    1.65 +const Cc = Components.classes;
    1.66 +const Cu = Components.utils;
    1.67 +
    1.68 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.69 +Cu.import("resource://gre/modules/Services.jsm");
    1.70 +Cu.import("resource://gre/modules/NetUtil.jsm");
    1.71 +Cu.import("resource://gre/modules/osfile.jsm");
    1.72 +Cu.import("resource://gre/modules/FileUtils.jsm");
    1.73 +Cu.import("resource://gre/modules/PlacesUtils.jsm");
    1.74 +Cu.import("resource://gre/modules/Promise.jsm");
    1.75 +Cu.import("resource://gre/modules/Task.jsm");
    1.76 +
    1.77 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
    1.78 +  "resource://gre/modules/PlacesBackups.jsm");
    1.79 +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
    1.80 +  "resource://gre/modules/Deprecated.jsm");
    1.81 +
    1.82 +const Container_Normal = 0;
    1.83 +const Container_Toolbar = 1;
    1.84 +const Container_Menu = 2;
    1.85 +const Container_Unfiled = 3;
    1.86 +const Container_Places = 4;
    1.87 +
    1.88 +const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
    1.89 +const DESCRIPTION_ANNO = "bookmarkProperties/description";
    1.90 +
    1.91 +const MICROSEC_PER_SEC = 1000000;
    1.92 +
    1.93 +const EXPORT_INDENT = "    "; // four spaces
    1.94 +
    1.95 +// Counter used to build fake favicon urls.
    1.96 +let serialNumber = 0;
    1.97 +
    1.98 +function base64EncodeString(aString) {
    1.99 +  let stream = Cc["@mozilla.org/io/string-input-stream;1"]
   1.100 +                 .createInstance(Ci.nsIStringInputStream);
   1.101 +  stream.setData(aString, aString.length);
   1.102 +  let encoder = Cc["@mozilla.org/scriptablebase64encoder;1"]
   1.103 +                  .createInstance(Ci.nsIScriptableBase64Encoder);
   1.104 +  return encoder.encodeToString(stream, aString.length);
   1.105 +}
   1.106 +
   1.107 +/**
   1.108 + * Provides HTML escaping for use in HTML attributes and body of the bookmarks
   1.109 + * file, compatible with the old bookmarks system.
   1.110 + */
   1.111 +function escapeHtmlEntities(aText) {
   1.112 +  return (aText || "").replace("&", "&amp;", "g")
   1.113 +                      .replace("<", "&lt;", "g")
   1.114 +                      .replace(">", "&gt;", "g")
   1.115 +                      .replace("\"", "&quot;", "g")
   1.116 +                      .replace("'", "&#39;", "g");
   1.117 +}
   1.118 +
   1.119 +/**
   1.120 + * Provides URL escaping for use in HTML attributes of the bookmarks file,
   1.121 + * compatible with the old bookmarks system.
   1.122 + */
   1.123 +function escapeUrl(aText) {
   1.124 +  return (aText || "").replace("\"", "%22", "g");
   1.125 +}
   1.126 +
   1.127 +function notifyObservers(aTopic, aInitialImport) {
   1.128 +  Services.obs.notifyObservers(null, aTopic, aInitialImport ? "html-initial"
   1.129 +                                                            : "html");
   1.130 +}
   1.131 +
   1.132 +function promiseSoon() {
   1.133 +  let deferred = Promise.defer();
   1.134 +  Services.tm.mainThread.dispatch(deferred.resolve,
   1.135 +                                  Ci.nsIThread.DISPATCH_NORMAL);
   1.136 +  return deferred.promise;
   1.137 +}
   1.138 +
   1.139 +this.BookmarkHTMLUtils = Object.freeze({
   1.140 +  /**
   1.141 +   * Loads the current bookmarks hierarchy from a "bookmarks.html" file.
   1.142 +   *
   1.143 +   * @param aSpec
   1.144 +   *        String containing the "file:" URI for the existing "bookmarks.html"
   1.145 +   *        file to be loaded.
   1.146 +   * @param aInitialImport
   1.147 +   *        Whether this is the initial import executed on a new profile.
   1.148 +   *
   1.149 +   * @return {Promise}
   1.150 +   * @resolves When the new bookmarks have been created.
   1.151 +   * @rejects JavaScript exception.
   1.152 +   */
   1.153 +  importFromURL: function BHU_importFromURL(aSpec, aInitialImport) {
   1.154 +    return Task.spawn(function* () {
   1.155 +      notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
   1.156 +      try {
   1.157 +        let importer = new BookmarkImporter(aInitialImport);
   1.158 +        yield importer.importFromURL(aSpec);
   1.159 +
   1.160 +        notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aInitialImport);
   1.161 +      } catch(ex) {
   1.162 +        Cu.reportError("Failed to import bookmarks from " + aSpec + ": " + ex);
   1.163 +        notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED, aInitialImport);
   1.164 +        throw ex;
   1.165 +      }
   1.166 +    });
   1.167 +  },
   1.168 +
   1.169 +  /**
   1.170 +   * Loads the current bookmarks hierarchy from a "bookmarks.html" file.
   1.171 +   *
   1.172 +   * @param aFilePath
   1.173 +   *        OS.File path string of the "bookmarks.html" file to be loaded.
   1.174 +   * @param aInitialImport
   1.175 +   *        Whether this is the initial import executed on a new profile.
   1.176 +   *
   1.177 +   * @return {Promise}
   1.178 +   * @resolves When the new bookmarks have been created.
   1.179 +   * @rejects JavaScript exception.
   1.180 +   * @deprecated passing an nsIFile is deprecated
   1.181 +   */
   1.182 +  importFromFile: function BHU_importFromFile(aFilePath, aInitialImport) {
   1.183 +    if (aFilePath instanceof Ci.nsIFile) {
   1.184 +      Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.importFromFile " +
   1.185 +                         "is deprecated. Please use an OS.File path string instead.",
   1.186 +                         "https://developer.mozilla.org/docs/JavaScript_OS.File");
   1.187 +      aFilePath = aFilePath.path;
   1.188 +    }
   1.189 +
   1.190 +    return Task.spawn(function* () {
   1.191 +      notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
   1.192 +      try {
   1.193 +        if (!(yield OS.File.exists(aFilePath)))
   1.194 +          throw new Error("Cannot import from nonexisting html file");
   1.195 +
   1.196 +        let importer = new BookmarkImporter(aInitialImport);
   1.197 +        yield importer.importFromURL(OS.Path.toFileURI(aFilePath));
   1.198 +
   1.199 +        notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aInitialImport);
   1.200 +      } catch(ex) {
   1.201 +        Cu.reportError("Failed to import bookmarks from " + aFilePath + ": " + ex);
   1.202 +        notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED, aInitialImport);
   1.203 +        throw ex;
   1.204 +      }
   1.205 +    });
   1.206 +  },
   1.207 +
   1.208 +  /**
   1.209 +   * Saves the current bookmarks hierarchy to a "bookmarks.html" file.
   1.210 +   *
   1.211 +   * @param aFilePath
   1.212 +   *        OS.File path string for the "bookmarks.html" file to be created.
   1.213 +   *
   1.214 +   * @return {Promise}
   1.215 +   * @resolves To the exported bookmarks count when the file has been created.
   1.216 +   * @rejects JavaScript exception.
   1.217 +   * @deprecated passing an nsIFile is deprecated
   1.218 +   */
   1.219 +  exportToFile: function BHU_exportToFile(aFilePath) {
   1.220 +    if (aFilePath instanceof Ci.nsIFile) {
   1.221 +      Deprecated.warning("Passing an nsIFile to BookmarksHTMLUtils.exportToFile " +
   1.222 +                         "is deprecated. Please use an OS.File path string instead.",
   1.223 +                         "https://developer.mozilla.org/docs/JavaScript_OS.File");
   1.224 +      aFilePath = aFilePath.path;
   1.225 +    }
   1.226 +    return Task.spawn(function* () {
   1.227 +      let [bookmarks, count] = yield PlacesBackups.getBookmarksTree();
   1.228 +      let startTime = Date.now();
   1.229 +
   1.230 +      // Report the time taken to convert the tree to HTML.
   1.231 +      let exporter = new BookmarkExporter(bookmarks);
   1.232 +      yield exporter.exportToFile(aFilePath);
   1.233 +
   1.234 +      try {
   1.235 +        Services.telemetry
   1.236 +                .getHistogramById("PLACES_EXPORT_TOHTML_MS")
   1.237 +                .add(Date.now() - startTime);
   1.238 +      } catch (ex) {
   1.239 +        Components.utils.reportError("Unable to report telemetry.");
   1.240 +      }
   1.241 +
   1.242 +      return count;
   1.243 +    });
   1.244 +  },
   1.245 +
   1.246 +  get defaultPath() {
   1.247 +    try {
   1.248 +      return Services.prefs.getCharPref("browser.bookmarks.file");
   1.249 +    } catch (ex) {}
   1.250 +    return OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.html")
   1.251 +  }
   1.252 +});
   1.253 +
   1.254 +function Frame(aFrameId) {
   1.255 +  this.containerId = aFrameId;
   1.256 +
   1.257 +  /**
   1.258 +   * How many <dl>s have been nested. Each frame/container should start
   1.259 +   * with a heading, and is then followed by a <dl>, <ul>, or <menu>. When
   1.260 +   * that list is complete, then it is the end of this container and we need
   1.261 +   * to pop back up one level for new items. If we never get an open tag for
   1.262 +   * one of these things, we should assume that the container is empty and
   1.263 +   * that things we find should be siblings of it. Normally, these <dl>s won't
   1.264 +   * be nested so this will be 0 or 1.
   1.265 +   */
   1.266 +  this.containerNesting = 0;
   1.267 +
   1.268 +  /**
   1.269 +   * when we find a heading tag, it actually affects the title of the NEXT
   1.270 +   * container in the list. This stores that heading tag and whether it was
   1.271 +   * special. 'consumeHeading' resets this._
   1.272 +   */
   1.273 +  this.lastContainerType = Container_Normal;
   1.274 +
   1.275 +  /**
   1.276 +   * this contains the text from the last begin tag until now. It is reset
   1.277 +   * at every begin tag. We can check it when we see a </a>, or </h3>
   1.278 +   * to see what the text content of that node should be.
   1.279 +   */
   1.280 +  this.previousText = "";
   1.281 +
   1.282 +  /**
   1.283 +   * true when we hit a <dd>, which contains the description for the preceding
   1.284 +   * <a> tag. We can't just check for </dd> like we can for </a> or </h3>
   1.285 +   * because if there is a sub-folder, it is actually a child of the <dd>
   1.286 +   * because the tag is never explicitly closed. If this is true and we see a
   1.287 +   * new open tag, that means to commit the description to the previous
   1.288 +   * bookmark.
   1.289 +   *
   1.290 +   * Additional weirdness happens when the previous <dt> tag contains a <h3>:
   1.291 +   * this means there is a new folder with the given description, and whose
   1.292 +   * children are contained in the following <dl> list.
   1.293 +   *
   1.294 +   * This is handled in openContainer(), which commits previous text if
   1.295 +   * necessary.
   1.296 +   */
   1.297 +  this.inDescription = false;
   1.298 +
   1.299 +  /**
   1.300 +   * contains the URL of the previous bookmark created. This is used so that
   1.301 +   * when we encounter a <dd>, we know what bookmark to associate the text with.
   1.302 +   * This is cleared whenever we hit a <h3>, so that we know NOT to save this
   1.303 +   * with a bookmark, but to keep it until 
   1.304 +   */
   1.305 +  this.previousLink = null; // nsIURI
   1.306 +
   1.307 +  /**
   1.308 +   * contains the URL of the previous livemark, so that when the link ends,
   1.309 +   * and the livemark title is known, we can create it.
   1.310 +   */
   1.311 +  this.previousFeed = null; // nsIURI
   1.312 +
   1.313 +  /**
   1.314 +   * Contains the id of an imported, or newly created bookmark.
   1.315 +   */
   1.316 +  this.previousId = 0;
   1.317 +
   1.318 +  /**
   1.319 +   * Contains the date-added and last-modified-date of an imported item.
   1.320 +   * Used to override the values set by insertBookmark, createFolder, etc.
   1.321 +   */
   1.322 +  this.previousDateAdded = 0;
   1.323 +  this.previousLastModifiedDate = 0;
   1.324 +}
   1.325 +
   1.326 +function BookmarkImporter(aInitialImport) {
   1.327 +  this._isImportDefaults = aInitialImport;
   1.328 +  this._frames = new Array();
   1.329 +  this._frames.push(new Frame(PlacesUtils.bookmarksMenuFolderId));
   1.330 +}
   1.331 +
   1.332 +BookmarkImporter.prototype = {
   1.333 +
   1.334 +  _safeTrim: function safeTrim(aStr) {
   1.335 +    return aStr ? aStr.trim() : aStr;
   1.336 +  },
   1.337 +
   1.338 +  get _curFrame() {
   1.339 +    return this._frames[this._frames.length - 1];
   1.340 +  },
   1.341 +
   1.342 +  get _previousFrame() {
   1.343 +    return this._frames[this._frames.length - 2];
   1.344 +  },
   1.345 +
   1.346 +  /**
   1.347 +   * This is called when there is a new folder found. The folder takes the
   1.348 +   * name from the previous frame's heading.
   1.349 +   */
   1.350 +  _newFrame: function newFrame() {
   1.351 +    let containerId = -1;
   1.352 +    let frame = this._curFrame;
   1.353 +    let containerTitle = frame.previousText;
   1.354 +    frame.previousText = "";
   1.355 +    let containerType = frame.lastContainerType;
   1.356 +
   1.357 +    switch (containerType) {
   1.358 +      case Container_Normal:
   1.359 +        // append a new folder
   1.360 +        containerId = 
   1.361 +          PlacesUtils.bookmarks.createFolder(frame.containerId,
   1.362 +                                             containerTitle,
   1.363 +                                             PlacesUtils.bookmarks.DEFAULT_INDEX);
   1.364 +        break;
   1.365 +      case Container_Places:
   1.366 +        containerId = PlacesUtils.placesRootId;
   1.367 +        break;
   1.368 +      case Container_Menu:
   1.369 +        containerId = PlacesUtils.bookmarksMenuFolderId;
   1.370 +        break;
   1.371 +      case Container_Unfiled:
   1.372 +        containerId = PlacesUtils.unfiledBookmarksFolderId;
   1.373 +        break;
   1.374 +      case Container_Toolbar:
   1.375 +        containerId = PlacesUtils.toolbarFolderId;
   1.376 +        break;
   1.377 +      default:
   1.378 +        // NOT REACHED
   1.379 +        throw new Error("Unreached");
   1.380 +    }
   1.381 +
   1.382 +    if (frame.previousDateAdded > 0) {
   1.383 +      try {
   1.384 +        PlacesUtils.bookmarks.setItemDateAdded(containerId, frame.previousDateAdded);
   1.385 +      } catch(e) {
   1.386 +      }
   1.387 +      frame.previousDateAdded = 0;
   1.388 +    }
   1.389 +    if (frame.previousLastModifiedDate > 0) {
   1.390 +      try {
   1.391 +        PlacesUtils.bookmarks.setItemLastModified(containerId, frame.previousLastModifiedDate);
   1.392 +      } catch(e) {
   1.393 +      }
   1.394 +      // don't clear last-modified, in case there's a description
   1.395 +    }
   1.396 +
   1.397 +    frame.previousId = containerId;
   1.398 +
   1.399 +    this._frames.push(new Frame(containerId));
   1.400 +  },
   1.401 +
   1.402 +  /**
   1.403 +   * Handles <hr> as a separator.
   1.404 +   *
   1.405 +   * @note Separators may have a title in old html files, though Places dropped
   1.406 +   *       support for them.
   1.407 +   *       We also don't import ADD_DATE or LAST_MODIFIED for separators because
   1.408 +   *       pre-Places bookmarks did not support them.
   1.409 +   */
   1.410 +  _handleSeparator: function handleSeparator(aElt) {
   1.411 +    let frame = this._curFrame;
   1.412 +    try {
   1.413 +      frame.previousId =
   1.414 +        PlacesUtils.bookmarks.insertSeparator(frame.containerId,
   1.415 +                                              PlacesUtils.bookmarks.DEFAULT_INDEX);
   1.416 +    } catch(e) {}
   1.417 +  },
   1.418 +
   1.419 +  /**
   1.420 +   * Handles <H1>. We check for the attribute PLACES_ROOT and reset the
   1.421 +   * container id if it's found. Otherwise, the default bookmark menu
   1.422 +   * root is assumed and imported things will go into the bookmarks menu.
   1.423 +   */
   1.424 +  _handleHead1Begin: function handleHead1Begin(aElt) {
   1.425 +    if (this._frames.length > 1) {
   1.426 +      return;
   1.427 +    }
   1.428 +    if (aElt.hasAttribute("places_root")) {
   1.429 +      this._curFrame.containerId = PlacesUtils.placesRootId;
   1.430 +    }
   1.431 +  },
   1.432 +
   1.433 +  /**
   1.434 +   * Called for h2,h3,h4,h5,h6. This just stores the correct information in
   1.435 +   * the current frame; the actual new frame corresponding to the container
   1.436 +   * associated with the heading will be created when the tag has been closed
   1.437 +   * and we know the title (we don't know to create a new folder or to merge
   1.438 +   * with an existing one until we have the title).
   1.439 +   */
   1.440 +  _handleHeadBegin: function handleHeadBegin(aElt) {
   1.441 +    let frame = this._curFrame;
   1.442 +
   1.443 +    // after a heading, a previous bookmark is not applicable (for example, for
   1.444 +    // the descriptions contained in a <dd>). Neither is any previous head type
   1.445 +    frame.previousLink = null;
   1.446 +    frame.lastContainerType = Container_Normal;
   1.447 +
   1.448 +    // It is syntactically possible for a heading to appear after another heading
   1.449 +    // but before the <dl> that encloses that folder's contents.  This should not
   1.450 +    // happen in practice, as the file will contain "<dl></dl>" sequence for
   1.451 +    // empty containers.
   1.452 +    //
   1.453 +    // Just to be on the safe side, if we encounter
   1.454 +    //   <h3>FOO</h3>
   1.455 +    //   <h3>BAR</h3>
   1.456 +    //   <dl>...content 1...</dl>
   1.457 +    //   <dl>...content 2...</dl>
   1.458 +    // we'll pop the stack when we find the h3 for BAR, treating that as an
   1.459 +    // implicit ending of the FOO container. The output will be FOO and BAR as
   1.460 +    // siblings. If there's another <dl> following (as in "content 2"), those
   1.461 +    // items will be treated as further siblings of FOO and BAR
   1.462 +    // This special frame popping business, of course, only happens when our
   1.463 +    // frame array has more than one element so we can avoid situations where
   1.464 +    // we don't have a frame to parse into anymore.
   1.465 +    if (frame.containerNesting == 0 && this._frames.length > 1) {
   1.466 +      this._frames.pop();
   1.467 +    }
   1.468 +
   1.469 +    // We have to check for some attributes to see if this is a "special"
   1.470 +    // folder, which will have different creation rules when the end tag is
   1.471 +    // processed.
   1.472 +    if (aElt.hasAttribute("personal_toolbar_folder")) {
   1.473 +      if (this._isImportDefaults) {
   1.474 +        frame.lastContainerType = Container_Toolbar;
   1.475 +      }
   1.476 +    } else if (aElt.hasAttribute("bookmarks_menu")) {
   1.477 +      if (this._isImportDefaults) {
   1.478 +        frame.lastContainerType = Container_Menu;
   1.479 +      }
   1.480 +    } else if (aElt.hasAttribute("unfiled_bookmarks_folder")) {
   1.481 +      if (this._isImportDefaults) {
   1.482 +        frame.lastContainerType = Container_Unfiled;
   1.483 +      }
   1.484 +    } else if (aElt.hasAttribute("places_root")) {
   1.485 +      if (this._isImportDefaults) {
   1.486 +        frame.lastContainerType = Container_Places;
   1.487 +      }
   1.488 +    } else {
   1.489 +      let addDate = aElt.getAttribute("add_date");
   1.490 +      if (addDate) {
   1.491 +        frame.previousDateAdded =
   1.492 +          this._convertImportedDateToInternalDate(addDate);
   1.493 +      }
   1.494 +      let modDate = aElt.getAttribute("last_modified");
   1.495 +      if (modDate) {
   1.496 +        frame.previousLastModifiedDate =
   1.497 +          this._convertImportedDateToInternalDate(modDate);
   1.498 +      }
   1.499 +    }
   1.500 +    this._curFrame.previousText = "";
   1.501 +  },
   1.502 +
   1.503 +  /*
   1.504 +   * Handles "<a" tags by creating a new bookmark. The title of the bookmark
   1.505 +   * will be the text content, which will be stuffed in previousText for us
   1.506 +   * and which will be saved by handleLinkEnd
   1.507 +   */
   1.508 +  _handleLinkBegin: function handleLinkBegin(aElt) {
   1.509 +    let frame = this._curFrame;
   1.510 +
   1.511 +    // Make sure that the feed URIs from previous frames are emptied.
   1.512 +    frame.previousFeed = null;
   1.513 +    // Make sure that the bookmark id from previous frames are emptied.
   1.514 +    frame.previousId = 0;
   1.515 +    // mPreviousText will hold link text, clear it.
   1.516 +    frame.previousText = "";
   1.517 +
   1.518 +    // Get the attributes we care about.
   1.519 +    let href = this._safeTrim(aElt.getAttribute("href"));
   1.520 +    let feedUrl = this._safeTrim(aElt.getAttribute("feedurl"));
   1.521 +    let icon = this._safeTrim(aElt.getAttribute("icon"));
   1.522 +    let iconUri = this._safeTrim(aElt.getAttribute("icon_uri"));
   1.523 +    let lastCharset = this._safeTrim(aElt.getAttribute("last_charset"));
   1.524 +    let keyword = this._safeTrim(aElt.getAttribute("shortcuturl"));
   1.525 +    let postData = this._safeTrim(aElt.getAttribute("post_data"));
   1.526 +    let webPanel = this._safeTrim(aElt.getAttribute("web_panel"));
   1.527 +    let micsumGenURI = this._safeTrim(aElt.getAttribute("micsum_gen_uri"));
   1.528 +    let generatedTitle = this._safeTrim(aElt.getAttribute("generated_title"));
   1.529 +    let dateAdded = this._safeTrim(aElt.getAttribute("add_date"));
   1.530 +    let lastModified = this._safeTrim(aElt.getAttribute("last_modified"));
   1.531 +
   1.532 +    // For feeds, get the feed URL.  If it is invalid, mPreviousFeed will be
   1.533 +    // NULL and we'll create it as a normal bookmark.
   1.534 +    if (feedUrl) {
   1.535 +      frame.previousFeed = NetUtil.newURI(feedUrl);
   1.536 +    }
   1.537 +
   1.538 +    // Ignore <a> tags that have no href.
   1.539 +    if (href) {
   1.540 +      // Save the address if it's valid.  Note that we ignore errors if this is a
   1.541 +      // feed since href is optional for them.
   1.542 +      try {
   1.543 +        frame.previousLink = NetUtil.newURI(href);
   1.544 +      } catch(e) {
   1.545 +        if (!frame.previousFeed) {
   1.546 +          frame.previousLink = null;
   1.547 +          return;
   1.548 +        }
   1.549 +      }
   1.550 +    } else {
   1.551 +      frame.previousLink = null;
   1.552 +      // The exception is for feeds, where the href is an optional component
   1.553 +      // indicating the source web site.
   1.554 +      if (!frame.previousFeed) {
   1.555 +        return;
   1.556 +      }
   1.557 +    }
   1.558 +
   1.559 +    // Save bookmark's last modified date.
   1.560 +    if (lastModified) {
   1.561 +      frame.previousLastModifiedDate =
   1.562 +        this._convertImportedDateToInternalDate(lastModified);
   1.563 +    }
   1.564 +
   1.565 +    // If this is a live bookmark, we will handle it in HandleLinkEnd(), so we
   1.566 +    // can skip bookmark creation.
   1.567 +    if (frame.previousFeed) {
   1.568 +      return;
   1.569 +    }
   1.570 +
   1.571 +    // Create the bookmark.  The title is unknown for now, we will set it later.
   1.572 +    try {
   1.573 +      frame.previousId =
   1.574 +        PlacesUtils.bookmarks.insertBookmark(frame.containerId,
   1.575 +                                             frame.previousLink,
   1.576 +                                             PlacesUtils.bookmarks.DEFAULT_INDEX,
   1.577 +                                             "");
   1.578 +    } catch(e) {
   1.579 +      return;
   1.580 +    }
   1.581 +
   1.582 +    // Set the date added value, if we have it.
   1.583 +    if (dateAdded) {
   1.584 +      try {
   1.585 +        PlacesUtils.bookmarks.setItemDateAdded(frame.previousId,
   1.586 +          this._convertImportedDateToInternalDate(dateAdded));
   1.587 +      } catch(e) {
   1.588 +      }
   1.589 +    }
   1.590 +
   1.591 +    // Save the favicon.
   1.592 +    if (icon || iconUri) {
   1.593 +      let iconUriObject;
   1.594 +      try {
   1.595 +        iconUriObject = NetUtil.newURI(iconUri);
   1.596 +      } catch(e) {
   1.597 +      }
   1.598 +      if (icon || iconUriObject) {
   1.599 +        try {
   1.600 +          this._setFaviconForURI(frame.previousLink, iconUriObject, icon);
   1.601 +        } catch(e) {
   1.602 +        }
   1.603 +      }
   1.604 +    }
   1.605 +
   1.606 +    // Save the keyword.
   1.607 +    if (keyword) {
   1.608 +      try {
   1.609 +        PlacesUtils.bookmarks.setKeywordForBookmark(frame.previousId, keyword);
   1.610 +        if (postData) {
   1.611 +          PlacesUtils.annotations.setItemAnnotation(frame.previousId,
   1.612 +                                                    PlacesUtils.POST_DATA_ANNO,
   1.613 +                                                    postData,
   1.614 +                                                    0,
   1.615 +                                                    PlacesUtils.annotations.EXPIRE_NEVER);
   1.616 +        }
   1.617 +      } catch(e) {
   1.618 +      }
   1.619 +    }
   1.620 +
   1.621 +    // Set load-in-sidebar annotation for the bookmark.
   1.622 +    if (webPanel && webPanel.toLowerCase() == "true") {
   1.623 +      try {
   1.624 +        PlacesUtils.annotations.setItemAnnotation(frame.previousId,
   1.625 +                                                  LOAD_IN_SIDEBAR_ANNO,
   1.626 +                                                  1,
   1.627 +                                                  0,
   1.628 +                                                  PlacesUtils.annotations.EXPIRE_NEVER);
   1.629 +      } catch(e) {
   1.630 +      }
   1.631 +    }
   1.632 +
   1.633 +    // Import last charset.
   1.634 +    if (lastCharset) {
   1.635 +      PlacesUtils.setCharsetForURI(frame.previousLink, lastCharset);
   1.636 +    }
   1.637 +  },
   1.638 +
   1.639 +  _handleContainerBegin: function handleContainerBegin() {
   1.640 +    this._curFrame.containerNesting++;
   1.641 +  },
   1.642 +
   1.643 +  /**
   1.644 +   * Our "indent" count has decreased, and when we hit 0 that means that this
   1.645 +   * container is complete and we need to pop back to the outer frame. Never
   1.646 +   * pop the toplevel frame
   1.647 +   */
   1.648 +  _handleContainerEnd: function handleContainerEnd() {
   1.649 +    let frame = this._curFrame;
   1.650 +    if (frame.containerNesting > 0)
   1.651 +      frame.containerNesting --;
   1.652 +    if (this._frames.length > 1 && frame.containerNesting == 0) {
   1.653 +      // we also need to re-set the imported last-modified date here. Otherwise
   1.654 +      // the addition of items will override the imported field.
   1.655 +      let prevFrame = this._previousFrame;
   1.656 +      if (prevFrame.previousLastModifiedDate > 0) {
   1.657 +        PlacesUtils.bookmarks.setItemLastModified(frame.containerId,
   1.658 +                                                  prevFrame.previousLastModifiedDate);
   1.659 +      }
   1.660 +      this._frames.pop();
   1.661 +    }
   1.662 +  },
   1.663 +
   1.664 +  /**
   1.665 +   * Creates the new frame for this heading now that we know the name of the
   1.666 +   * container (tokens since the heading open tag will have been placed in
   1.667 +   * previousText).
   1.668 +   */
   1.669 +  _handleHeadEnd: function handleHeadEnd() {
   1.670 +    this._newFrame();
   1.671 +  },
   1.672 +
   1.673 +  /**
   1.674 +   * Saves the title for the given bookmark.
   1.675 +   */
   1.676 +  _handleLinkEnd: function handleLinkEnd() {
   1.677 +    let frame = this._curFrame;
   1.678 +    frame.previousText = frame.previousText.trim();
   1.679 +
   1.680 +    try {
   1.681 +      if (frame.previousFeed) {
   1.682 +        // The is a live bookmark.  We create it here since in HandleLinkBegin we
   1.683 +        // don't know the title.
   1.684 +        PlacesUtils.livemarks.addLivemark({
   1.685 +          "title": frame.previousText,
   1.686 +          "parentId": frame.containerId,
   1.687 +          "index": PlacesUtils.bookmarks.DEFAULT_INDEX,
   1.688 +          "feedURI": frame.previousFeed,
   1.689 +          "siteURI": frame.previousLink,
   1.690 +        }).then(null, Cu.reportError);
   1.691 +      } else if (frame.previousLink) {
   1.692 +        // This is a common bookmark.
   1.693 +        PlacesUtils.bookmarks.setItemTitle(frame.previousId,
   1.694 +                                           frame.previousText);
   1.695 +      }
   1.696 +    } catch(e) {
   1.697 +    }
   1.698 +
   1.699 +
   1.700 +    // Set last modified date as the last change.
   1.701 +    if (frame.previousId > 0 && frame.previousLastModifiedDate > 0) {
   1.702 +      try {
   1.703 +        PlacesUtils.bookmarks.setItemLastModified(frame.previousId,
   1.704 +                                                  frame.previousLastModifiedDate);
   1.705 +      } catch(e) {
   1.706 +      }
   1.707 +      // Note: don't clear previousLastModifiedDate, because if this item has a
   1.708 +      // description, we'll need to set it again.
   1.709 +    }
   1.710 +
   1.711 +    frame.previousText = "";
   1.712 +
   1.713 +  },
   1.714 +
   1.715 +  _openContainer: function openContainer(aElt) {
   1.716 +    if (aElt.namespaceURI != "http://www.w3.org/1999/xhtml") {
   1.717 +      return;
   1.718 +    }
   1.719 +    switch(aElt.localName) {
   1.720 +      case "h1":
   1.721 +        this._handleHead1Begin(aElt);
   1.722 +        break;
   1.723 +      case "h2":
   1.724 +      case "h3":
   1.725 +      case "h4":
   1.726 +      case "h5":
   1.727 +      case "h6":
   1.728 +        this._handleHeadBegin(aElt);
   1.729 +        break;
   1.730 +      case "a":
   1.731 +        this._handleLinkBegin(aElt);
   1.732 +        break;
   1.733 +      case "dl":
   1.734 +      case "ul":
   1.735 +      case "menu":
   1.736 +        this._handleContainerBegin();
   1.737 +        break;
   1.738 +      case "dd":
   1.739 +        this._curFrame.inDescription = true;
   1.740 +        break;
   1.741 +      case "hr":
   1.742 +        this._handleSeparator(aElt);
   1.743 +        break;
   1.744 +    }
   1.745 +  },
   1.746 +
   1.747 +  _closeContainer: function closeContainer(aElt) {
   1.748 +    let frame = this._curFrame;
   1.749 +
   1.750 +    // see the comment for the definition of inDescription. Basically, we commit
   1.751 +    // any text in previousText to the description of the node/folder if there
   1.752 +    // is any.
   1.753 +    if (frame.inDescription) {
   1.754 +      // NOTE ES5 trim trims more than the previous C++ trim.
   1.755 +      frame.previousText = frame.previousText.trim(); // important
   1.756 +      if (frame.previousText) {
   1.757 +
   1.758 +        let itemId = !frame.previousLink ? frame.containerId
   1.759 +                                         : frame.previousId;
   1.760 +
   1.761 +        try {
   1.762 +          if (!PlacesUtils.annotations.itemHasAnnotation(itemId, DESCRIPTION_ANNO)) {
   1.763 +            PlacesUtils.annotations.setItemAnnotation(itemId,
   1.764 +                                                      DESCRIPTION_ANNO,
   1.765 +                                                      frame.previousText,
   1.766 +                                                      0,
   1.767 +                                                      PlacesUtils.annotations.EXPIRE_NEVER);
   1.768 +          }
   1.769 +        } catch(e) {
   1.770 +        }
   1.771 +        frame.previousText = "";
   1.772 +
   1.773 +        // Set last-modified a 2nd time for all items with descriptions
   1.774 +        // we need to set last-modified as the *last* step in processing 
   1.775 +        // any item type in the bookmarks.html file, so that we do
   1.776 +        // not overwrite the imported value. for items without descriptions, 
   1.777 +        // setting this value after setting the item title is that 
   1.778 +        // last point at which we can save this value before it gets reset.
   1.779 +        // for items with descriptions, it must set after that point.
   1.780 +        // however, at the point at which we set the title, there's no way 
   1.781 +        // to determine if there will be a description following, 
   1.782 +        // so we need to set the last-modified-date at both places.
   1.783 +
   1.784 +        let lastModified;
   1.785 +        if (!frame.previousLink) {
   1.786 +          lastModified = this._previousFrame.previousLastModifiedDate;
   1.787 +        } else {
   1.788 +          lastModified = frame.previousLastModifiedDate;
   1.789 +        }
   1.790 +
   1.791 +        if (itemId > 0 && lastModified > 0) {
   1.792 +          PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified);
   1.793 +        }
   1.794 +      }
   1.795 +      frame.inDescription = false;
   1.796 +    }
   1.797 +
   1.798 +    if (aElt.namespaceURI != "http://www.w3.org/1999/xhtml") {
   1.799 +      return;
   1.800 +    }
   1.801 +    switch(aElt.localName) {
   1.802 +      case "dl":
   1.803 +      case "ul":
   1.804 +      case "menu":
   1.805 +        this._handleContainerEnd();
   1.806 +        break;
   1.807 +      case "dt":
   1.808 +        break;
   1.809 +      case "h1":
   1.810 +        // ignore
   1.811 +        break;
   1.812 +      case "h2":
   1.813 +      case "h3":
   1.814 +      case "h4":
   1.815 +      case "h5":
   1.816 +      case "h6":
   1.817 +        this._handleHeadEnd();
   1.818 +        break;
   1.819 +      case "a":
   1.820 +        this._handleLinkEnd();
   1.821 +        break;
   1.822 +      default:
   1.823 +        break;
   1.824 +    }
   1.825 +  },
   1.826 +
   1.827 +  _appendText: function appendText(str) {
   1.828 +    this._curFrame.previousText += str;
   1.829 +  },
   1.830 +
   1.831 +  /**
   1.832 +   * data is a string that is a data URI for the favicon. Our job is to
   1.833 +   * decode it and store it in the favicon service.
   1.834 +   *
   1.835 +   * When aIconURI is non-null, we will use that as the URI of the favicon
   1.836 +   * when storing in the favicon service.
   1.837 +   *
   1.838 +   * When aIconURI is null, we have to make up a URI for this favicon so that
   1.839 +   * it can be stored in the service. The real one will be set the next time
   1.840 +   * the user visits the page. Our made up one should get expired when the
   1.841 +   * page no longer references it.
   1.842 +   */
   1.843 +  _setFaviconForURI: function setFaviconForURI(aPageURI, aIconURI, aData) {
   1.844 +    // if the input favicon URI is a chrome: URI, then we just save it and don't
   1.845 +    // worry about data
   1.846 +    if (aIconURI) {
   1.847 +      if (aIconURI.schemeIs("chrome")) {
   1.848 +        PlacesUtils.favicons.setAndFetchFaviconForPage(aPageURI, aIconURI,
   1.849 +                                                       false,
   1.850 +                                                       PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
   1.851 +        return;
   1.852 +      }
   1.853 +    }
   1.854 +
   1.855 +    // some bookmarks have placeholder URIs that contain just "data:"
   1.856 +    // ignore these
   1.857 +    if (aData.length <= 5) {
   1.858 +      return;
   1.859 +    }
   1.860 +
   1.861 +    let faviconURI;
   1.862 +    if (aIconURI) {
   1.863 +      faviconURI = aIconURI;
   1.864 +    } else {
   1.865 +      // Make up a favicon URI for this page.  Later, we'll make sure that this
   1.866 +      // favicon URI is always associated with local favicon data, so that we
   1.867 +      // don't load this URI from the network.
   1.868 +      let faviconSpec = "http://www.mozilla.org/2005/made-up-favicon/"
   1.869 +                      + serialNumber
   1.870 +                      + "-"
   1.871 +                      + new Date().getTime();
   1.872 +      faviconURI = NetUtil.newURI(faviconSpec);
   1.873 +      serialNumber++;
   1.874 +    }
   1.875 +
   1.876 +    // This could fail if the favicon is bigger than defined limit, in such a
   1.877 +    // case neither the favicon URI nor the favicon data will be saved.  If the
   1.878 +    // bookmark is visited again later, the URI and data will be fetched.
   1.879 +    PlacesUtils.favicons.replaceFaviconDataFromDataURL(faviconURI, aData);
   1.880 +    PlacesUtils.favicons.setAndFetchFaviconForPage(aPageURI, faviconURI, false, PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
   1.881 +  },
   1.882 +
   1.883 +  /**
   1.884 +   * Converts a string date in seconds to an int date in microseconds
   1.885 +   */
   1.886 +  _convertImportedDateToInternalDate: function convertImportedDateToInternalDate(aDate) {
   1.887 +    if (aDate && !isNaN(aDate)) {
   1.888 +      return parseInt(aDate) * 1000000; // in bookmarks.html this value is in seconds, not microseconds
   1.889 +    } else {
   1.890 +      return Date.now();
   1.891 +    }
   1.892 +  },
   1.893 +
   1.894 +  runBatched: function runBatched(aDoc) {
   1.895 +    if (!aDoc) {
   1.896 +      return;
   1.897 +    }
   1.898 +
   1.899 +    if (this._isImportDefaults) {
   1.900 +      PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarksMenuFolderId);
   1.901 +      PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.toolbarFolderId);
   1.902 +      PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
   1.903 +    }
   1.904 +
   1.905 +    let current = aDoc;
   1.906 +    let next;
   1.907 +    for (;;) {
   1.908 +      switch (current.nodeType) {
   1.909 +        case Ci.nsIDOMNode.ELEMENT_NODE:
   1.910 +          this._openContainer(current);
   1.911 +          break;
   1.912 +        case Ci.nsIDOMNode.TEXT_NODE:
   1.913 +          this._appendText(current.data);
   1.914 +          break;
   1.915 +      }
   1.916 +      if ((next = current.firstChild)) {
   1.917 +        current = next;
   1.918 +        continue;
   1.919 +      }
   1.920 +      for (;;) {
   1.921 +        if (current.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
   1.922 +          this._closeContainer(current);
   1.923 +        }
   1.924 +        if (current == aDoc) {
   1.925 +          return;
   1.926 +        }
   1.927 +        if ((next = current.nextSibling)) {
   1.928 +          current = next;
   1.929 +          break;
   1.930 +        }
   1.931 +        current = current.parentNode;
   1.932 +      }
   1.933 +    }
   1.934 +  },
   1.935 +
   1.936 +  _walkTreeForImport: function walkTreeForImport(aDoc) {
   1.937 +    PlacesUtils.bookmarks.runInBatchMode(this, aDoc);
   1.938 +  },
   1.939 +
   1.940 +  importFromURL: function importFromURL(aSpec) {
   1.941 +    let deferred = Promise.defer();
   1.942 +    let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
   1.943 +                .createInstance(Ci.nsIXMLHttpRequest);
   1.944 +    xhr.onload = () => {
   1.945 +      try {
   1.946 +        this._walkTreeForImport(xhr.responseXML);
   1.947 +        deferred.resolve();
   1.948 +      } catch(e) {
   1.949 +        deferred.reject(e);
   1.950 +        throw e;
   1.951 +      }
   1.952 +    };
   1.953 +    xhr.onabort = xhr.onerror = xhr.ontimeout = () => {
   1.954 +      deferred.reject(new Error("xmlhttprequest failed"));
   1.955 +    };
   1.956 +    try {
   1.957 +      xhr.open("GET", aSpec);
   1.958 +      xhr.responseType = "document";
   1.959 +      xhr.overrideMimeType("text/html");
   1.960 +      xhr.send();
   1.961 +    } catch (e) {
   1.962 +      deferred.reject(e);
   1.963 +    }
   1.964 +    return deferred.promise;
   1.965 +  },
   1.966 +
   1.967 +};
   1.968 +
   1.969 +function BookmarkExporter(aBookmarksTree) {
   1.970 +  // Create a map of the roots.
   1.971 +  let rootsMap = new Map();
   1.972 +  for (let child of aBookmarksTree.children) {
   1.973 +    if (child.root)
   1.974 +      rootsMap.set(child.root, child);
   1.975 +  }
   1.976 +
   1.977 +  // For backwards compatibility reasons the bookmarks menu is the root, while
   1.978 +  // the bookmarks toolbar and unfiled bookmarks will be child items.
   1.979 +  this._root = rootsMap.get("bookmarksMenuFolder");
   1.980 +
   1.981 +  for (let key of [ "toolbarFolder", "unfiledBookmarksFolder" ]) {
   1.982 +    let root = rootsMap.get(key);
   1.983 +    if (root.children && root.children.length > 0) {
   1.984 +      if (!this._root.children)
   1.985 +        this._root.children = [];
   1.986 +      this._root.children.push(root);
   1.987 +    }
   1.988 +  }
   1.989 +}
   1.990 +
   1.991 +BookmarkExporter.prototype = {
   1.992 +  exportToFile: function exportToFile(aFilePath) {
   1.993 +    return Task.spawn(function* () {
   1.994 +      // Create a file that can be accessed by the current user only.
   1.995 +      let out = FileUtils.openAtomicFileOutputStream(new FileUtils.File(aFilePath));
   1.996 +      try {
   1.997 +        // We need a buffered output stream for performance.  See bug 202477.
   1.998 +        let bufferedOut = Cc["@mozilla.org/network/buffered-output-stream;1"]
   1.999 +                          .createInstance(Ci.nsIBufferedOutputStream);
  1.1000 +        bufferedOut.init(out, 4096);
  1.1001 +        try {
  1.1002 +          // Write bookmarks in UTF-8.
  1.1003 +          this._converterOut = Cc["@mozilla.org/intl/converter-output-stream;1"]
  1.1004 +                               .createInstance(Ci.nsIConverterOutputStream);
  1.1005 +          this._converterOut.init(bufferedOut, "utf-8", 0, 0);
  1.1006 +          try {
  1.1007 +            this._writeHeader();
  1.1008 +            yield this._writeContainer(this._root);
  1.1009 +            // Retain the target file on success only.
  1.1010 +            bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
  1.1011 +          } finally {
  1.1012 +            this._converterOut.close();
  1.1013 +            this._converterOut = null;
  1.1014 +          }
  1.1015 +        } finally {
  1.1016 +          bufferedOut.close();
  1.1017 +        }
  1.1018 +      } finally {
  1.1019 +        out.close();
  1.1020 +      }
  1.1021 +    }.bind(this));
  1.1022 +  },
  1.1023 +
  1.1024 +  _converterOut: null,
  1.1025 +
  1.1026 +  _write: function (aText) {
  1.1027 +    this._converterOut.writeString(aText || "");
  1.1028 +  },
  1.1029 +
  1.1030 +  _writeAttribute: function (aName, aValue) {
  1.1031 +    this._write(' ' +  aName + '="' + aValue + '"');
  1.1032 +  },
  1.1033 +
  1.1034 +  _writeLine: function (aText) {
  1.1035 +    this._write(aText + "\n");
  1.1036 +  },
  1.1037 +
  1.1038 +  _writeHeader: function () {
  1.1039 +    this._writeLine("<!DOCTYPE NETSCAPE-Bookmark-file-1>");
  1.1040 +    this._writeLine("<!-- This is an automatically generated file.");
  1.1041 +    this._writeLine("     It will be read and overwritten.");
  1.1042 +    this._writeLine("     DO NOT EDIT! -->");
  1.1043 +    this._writeLine('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; ' +
  1.1044 +                    'charset=UTF-8">');
  1.1045 +    this._writeLine("<TITLE>Bookmarks</TITLE>");
  1.1046 +  },
  1.1047 +
  1.1048 +  _writeContainer: function (aItem, aIndent = "") {
  1.1049 +    if (aItem == this._root) {
  1.1050 +      this._writeLine("<H1>" + escapeHtmlEntities(this._root.title) + "</H1>");
  1.1051 +      this._writeLine("");
  1.1052 +    }
  1.1053 +    else {
  1.1054 +      this._write(aIndent + "<DT><H3");
  1.1055 +      this._writeDateAttributes(aItem);
  1.1056 +
  1.1057 +      if (aItem.root === "toolbarFolder")
  1.1058 +        this._writeAttribute("PERSONAL_TOOLBAR_FOLDER", "true");
  1.1059 +      else if (aItem.root === "unfiledBookmarksFolder")
  1.1060 +        this._writeAttribute("UNFILED_BOOKMARKS_FOLDER", "true");
  1.1061 +      this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</H3>");
  1.1062 +    }
  1.1063 +
  1.1064 +    this._writeDescription(aItem, aIndent);
  1.1065 +
  1.1066 +    this._writeLine(aIndent + "<DL><p>");
  1.1067 +    if (aItem.children)
  1.1068 +      yield this._writeContainerContents(aItem, aIndent);
  1.1069 +    if (aItem == this._root)
  1.1070 +      this._writeLine(aIndent + "</DL>");
  1.1071 +    else
  1.1072 +      this._writeLine(aIndent + "</DL><p>");
  1.1073 +  },
  1.1074 +
  1.1075 +  _writeContainerContents: function (aItem, aIndent) {
  1.1076 +    let localIndent = aIndent + EXPORT_INDENT;
  1.1077 +
  1.1078 +    for (let child of aItem.children) {
  1.1079 +      if (child.annos && child.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI))
  1.1080 +          this._writeLivemark(child, localIndent);
  1.1081 +      else if (child.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
  1.1082 +          yield this._writeContainer(child, localIndent);
  1.1083 +      else if (child.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR)
  1.1084 +        this._writeSeparator(child, localIndent);
  1.1085 +      else
  1.1086 +        yield this._writeItem(child, localIndent);
  1.1087 +    }
  1.1088 +  },
  1.1089 +
  1.1090 +  _writeSeparator: function (aItem, aIndent) {
  1.1091 +    this._write(aIndent + "<HR");
  1.1092 +    // We keep exporting separator titles, but don't support them anymore.
  1.1093 +    if (aItem.title)
  1.1094 +      this._writeAttribute("NAME", escapeHtmlEntities(aItem.title));
  1.1095 +    this._write(">");
  1.1096 +  },
  1.1097 +
  1.1098 +  _writeLivemark: function (aItem, aIndent) {
  1.1099 +    this._write(aIndent + "<DT><A");
  1.1100 +    let feedSpec = aItem.annos.find(anno => anno.name == PlacesUtils.LMANNO_FEEDURI).value;
  1.1101 +    this._writeAttribute("FEEDURL", escapeUrl(feedSpec));
  1.1102 +    let siteSpecAnno = aItem.annos.find(anno => anno.name == PlacesUtils.LMANNO_SITEURI);
  1.1103 +    if (siteSpecAnno)
  1.1104 +      this._writeAttribute("HREF", escapeUrl(siteSpecAnno.value));
  1.1105 +    this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</A>");
  1.1106 +    this._writeDescription(aItem, aIndent);
  1.1107 +  },
  1.1108 +
  1.1109 +  _writeItem: function (aItem, aIndent) {
  1.1110 +    // This is a workaround for "too much recursion" error, due to the fact
  1.1111 +    // Task.jsm still uses old on-same-tick promises.  It may be removed as
  1.1112 +    // soon as bug 887923 is fixed.
  1.1113 +    yield promiseSoon();
  1.1114 +    let uri = null;
  1.1115 +    try {
  1.1116 +      uri = NetUtil.newURI(aItem.uri);
  1.1117 +    } catch (ex) {
  1.1118 +      // If the item URI is invalid, skip the item instead of failing later.
  1.1119 +      return;
  1.1120 +    }
  1.1121 +
  1.1122 +    this._write(aIndent + "<DT><A");
  1.1123 +    this._writeAttribute("HREF", escapeUrl(aItem.uri));
  1.1124 +    this._writeDateAttributes(aItem);
  1.1125 +    yield this._writeFaviconAttribute(aItem);
  1.1126 +
  1.1127 +    let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(aItem.id);
  1.1128 +    if (aItem.keyword)
  1.1129 +      this._writeAttribute("SHORTCUTURL", escapeHtmlEntities(keyword));
  1.1130 +
  1.1131 +    let postDataAnno = aItem.annos &&
  1.1132 +                       aItem.annos.find(anno => anno.name == PlacesUtils.POST_DATA_ANNO);
  1.1133 +    if (postDataAnno)
  1.1134 +      this._writeAttribute("POST_DATA", escapeHtmlEntities(postDataAnno.value));
  1.1135 +
  1.1136 +    if (aItem.annos && aItem.annos.some(anno => anno.name == LOAD_IN_SIDEBAR_ANNO))
  1.1137 +      this._writeAttribute("WEB_PANEL", "true");
  1.1138 +    if (aItem.charset)
  1.1139 +      this._writeAttribute("LAST_CHARSET", escapeHtmlEntities(aItem.charset));
  1.1140 +    if (aItem.tags)
  1.1141 +      this._writeAttribute("TAGS", aItem.tags);
  1.1142 +    this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</A>");
  1.1143 +    this._writeDescription(aItem, aIndent);
  1.1144 +  },
  1.1145 +
  1.1146 +  _writeDateAttributes: function (aItem) {
  1.1147 +    if (aItem.dateAdded)
  1.1148 +      this._writeAttribute("ADD_DATE",
  1.1149 +                           Math.floor(aItem.dateAdded / MICROSEC_PER_SEC));
  1.1150 +    if (aItem.lastModified)
  1.1151 +      this._writeAttribute("LAST_MODIFIED",
  1.1152 +                           Math.floor(aItem.lastModified / MICROSEC_PER_SEC));
  1.1153 +  },
  1.1154 +
  1.1155 +  _writeFaviconAttribute: function (aItem) {
  1.1156 +    if (!aItem.iconuri)
  1.1157 +      return;
  1.1158 +    let favicon;
  1.1159 +    try {
  1.1160 +      favicon  = yield PlacesUtils.promiseFaviconData(aItem.uri);
  1.1161 +    } catch (ex) {
  1.1162 +      Components.utils.reportError("Unexpected Error trying to fetch icon data");
  1.1163 +      return;
  1.1164 +    }
  1.1165 +
  1.1166 +    this._writeAttribute("ICON_URI", escapeUrl(favicon.uri.spec));
  1.1167 +
  1.1168 +    if (!favicon.uri.schemeIs("chrome") && favicon.dataLen > 0) {
  1.1169 +      let faviconContents = "data:image/png;base64," +
  1.1170 +        base64EncodeString(String.fromCharCode.apply(String, favicon.data));
  1.1171 +      this._writeAttribute("ICON", faviconContents);
  1.1172 +    }
  1.1173 +  },
  1.1174 +
  1.1175 +  _writeDescription: function (aItem, aIndent) {
  1.1176 +    let descriptionAnno = aItem.annos &&
  1.1177 +                          aItem.annos.find(anno => anno.name == DESCRIPTION_ANNO);
  1.1178 +    if (descriptionAnno)
  1.1179 +      this._writeLine(aIndent + "<DD>" + escapeHtmlEntities(descriptionAnno.value));
  1.1180 +  }
  1.1181 +};

mercurial