diff -r 000000000000 -r 6474c204b198 toolkit/components/places/BookmarkHTMLUtils.jsm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/toolkit/components/places/BookmarkHTMLUtils.jsm Wed Dec 31 06:09:35 2014 +0100
@@ -0,0 +1,1178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file works on the old-style "bookmarks.html" file. It includes
+ * functions to import and export existing bookmarks to this file format.
+ *
+ * Format
+ * ------
+ *
+ * Primary heading := h1
+ * Old version used this to set attributes on the bookmarks RDF root, such
+ * as the last modified date. We only use H1 to check for the attribute
+ * PLACES_ROOT, which tells us that this hierarchy root is the places root.
+ * For backwards compatibility, if we don't find this, we assume that the
+ * hierarchy is rooted at the bookmarks menu.
+ * Heading := any heading other than h1
+ * Old version used this to set attributes on the current container. We only
+ * care about the content of the heading container, which contains the title
+ * of the bookmark container.
+ * Bookmark := a
+ * HREF is the destination of the bookmark
+ * FEEDURL is the URI of the RSS feed if this is a livemark.
+ * LAST_CHARSET is stored as an annotation so that the next time we go to
+ * that page we remember the user's preference.
+ * WEB_PANEL is set to "true" if the bookmark should be loaded in the sidebar.
+ * ICON will be stored in the favicon service
+ * ICON_URI is new for places bookmarks.html, it refers to the original
+ * URI of the favicon so we don't have to make up favicon URLs.
+ * Text of the container is the name of the bookmark
+ * Ignored: LAST_VISIT, ID (writing out non-RDF IDs can confuse Firefox 2)
+ * Bookmark comment := dd
+ * This affects the previosly added bookmark
+ * Separator := hr
+ * Insert a separator into the current container
+ * The folder hierarchy is defined by /
, or
+ * to see what the text content of that node should be.
+ */
+ this.previousText = "";
+
+ /**
+ * true when we hit a /
).
+ *
+ * Overall design
+ * --------------
+ *
+ * We need to emulate a recursive parser. A "Bookmark import frame" is created
+ * corresponding to each folder we encounter. These are arranged in a stack,
+ * and contain all the state we need to keep track of.
+ *
+ * A frame is created when we find a heading, which defines a new container.
+ * The frame also keeps track of the nesting of
s, (in well-formed
+ * bookmarks files, these will have a 1-1 correspondence with frames, but we
+ * try to be a little more flexible here). When the nesting count decreases
+ * to 0, then we know a frame is complete and to pop back to the previous
+ * frame.
+ *
+ * Note that a lot of things happen when tags are CLOSED because we need to
+ * get the text from the content of the tag. For example, link and heading tags
+ * both require the content (= title) before actually creating it.
+ */
+
+this.EXPORTED_SYMBOLS = [ "BookmarkHTMLUtils" ];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
+ "resource://gre/modules/PlacesBackups.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+
+const Container_Normal = 0;
+const Container_Toolbar = 1;
+const Container_Menu = 2;
+const Container_Unfiled = 3;
+const Container_Places = 4;
+
+const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+
+const MICROSEC_PER_SEC = 1000000;
+
+const EXPORT_INDENT = " "; // four spaces
+
+// Counter used to build fake favicon urls.
+let serialNumber = 0;
+
+function base64EncodeString(aString) {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stream.setData(aString, aString.length);
+ let encoder = Cc["@mozilla.org/scriptablebase64encoder;1"]
+ .createInstance(Ci.nsIScriptableBase64Encoder);
+ return encoder.encodeToString(stream, aString.length);
+}
+
+/**
+ * Provides HTML escaping for use in HTML attributes and body of the bookmarks
+ * file, compatible with the old bookmarks system.
+ */
+function escapeHtmlEntities(aText) {
+ return (aText || "").replace("&", "&", "g")
+ .replace("<", "<", "g")
+ .replace(">", ">", "g")
+ .replace("\"", """, "g")
+ .replace("'", "'", "g");
+}
+
+/**
+ * Provides URL escaping for use in HTML attributes of the bookmarks file,
+ * compatible with the old bookmarks system.
+ */
+function escapeUrl(aText) {
+ return (aText || "").replace("\"", "%22", "g");
+}
+
+function notifyObservers(aTopic, aInitialImport) {
+ Services.obs.notifyObservers(null, aTopic, aInitialImport ? "html-initial"
+ : "html");
+}
+
+function promiseSoon() {
+ let deferred = Promise.defer();
+ Services.tm.mainThread.dispatch(deferred.resolve,
+ Ci.nsIThread.DISPATCH_NORMAL);
+ return deferred.promise;
+}
+
+this.BookmarkHTMLUtils = Object.freeze({
+ /**
+ * Loads the current bookmarks hierarchy from a "bookmarks.html" file.
+ *
+ * @param aSpec
+ * String containing the "file:" URI for the existing "bookmarks.html"
+ * file to be loaded.
+ * @param aInitialImport
+ * Whether this is the initial import executed on a new profile.
+ *
+ * @return {Promise}
+ * @resolves When the new bookmarks have been created.
+ * @rejects JavaScript exception.
+ */
+ importFromURL: function BHU_importFromURL(aSpec, aInitialImport) {
+ return Task.spawn(function* () {
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
+ try {
+ let importer = new BookmarkImporter(aInitialImport);
+ yield importer.importFromURL(aSpec);
+
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aInitialImport);
+ } catch(ex) {
+ Cu.reportError("Failed to import bookmarks from " + aSpec + ": " + ex);
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED, aInitialImport);
+ throw ex;
+ }
+ });
+ },
+
+ /**
+ * Loads the current bookmarks hierarchy from a "bookmarks.html" file.
+ *
+ * @param aFilePath
+ * OS.File path string of the "bookmarks.html" file to be loaded.
+ * @param aInitialImport
+ * Whether this is the initial import executed on a new profile.
+ *
+ * @return {Promise}
+ * @resolves When the new bookmarks have been created.
+ * @rejects JavaScript exception.
+ * @deprecated passing an nsIFile is deprecated
+ */
+ importFromFile: function BHU_importFromFile(aFilePath, aInitialImport) {
+ if (aFilePath instanceof Ci.nsIFile) {
+ Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.importFromFile " +
+ "is deprecated. Please use an OS.File path string instead.",
+ "https://developer.mozilla.org/docs/JavaScript_OS.File");
+ aFilePath = aFilePath.path;
+ }
+
+ return Task.spawn(function* () {
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
+ try {
+ if (!(yield OS.File.exists(aFilePath)))
+ throw new Error("Cannot import from nonexisting html file");
+
+ let importer = new BookmarkImporter(aInitialImport);
+ yield importer.importFromURL(OS.Path.toFileURI(aFilePath));
+
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aInitialImport);
+ } catch(ex) {
+ Cu.reportError("Failed to import bookmarks from " + aFilePath + ": " + ex);
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED, aInitialImport);
+ throw ex;
+ }
+ });
+ },
+
+ /**
+ * Saves the current bookmarks hierarchy to a "bookmarks.html" file.
+ *
+ * @param aFilePath
+ * OS.File path string for the "bookmarks.html" file to be created.
+ *
+ * @return {Promise}
+ * @resolves To the exported bookmarks count when the file has been created.
+ * @rejects JavaScript exception.
+ * @deprecated passing an nsIFile is deprecated
+ */
+ exportToFile: function BHU_exportToFile(aFilePath) {
+ if (aFilePath instanceof Ci.nsIFile) {
+ Deprecated.warning("Passing an nsIFile to BookmarksHTMLUtils.exportToFile " +
+ "is deprecated. Please use an OS.File path string instead.",
+ "https://developer.mozilla.org/docs/JavaScript_OS.File");
+ aFilePath = aFilePath.path;
+ }
+ return Task.spawn(function* () {
+ let [bookmarks, count] = yield PlacesBackups.getBookmarksTree();
+ let startTime = Date.now();
+
+ // Report the time taken to convert the tree to HTML.
+ let exporter = new BookmarkExporter(bookmarks);
+ yield exporter.exportToFile(aFilePath);
+
+ try {
+ Services.telemetry
+ .getHistogramById("PLACES_EXPORT_TOHTML_MS")
+ .add(Date.now() - startTime);
+ } catch (ex) {
+ Components.utils.reportError("Unable to report telemetry.");
+ }
+
+ return count;
+ });
+ },
+
+ get defaultPath() {
+ try {
+ return Services.prefs.getCharPref("browser.bookmarks.file");
+ } catch (ex) {}
+ return OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.html")
+ }
+});
+
+function Frame(aFrameId) {
+ this.containerId = aFrameId;
+
+ /**
+ * How many
s have been nested. Each frame/container should start
+ * with a heading, and is then followed by a
,
, or
s won't
+ * be nested so this will be 0 or 1.
+ */
+ this.containerNesting = 0;
+
+ /**
+ * when we find a heading tag, it actually affects the title of the NEXT
+ * container in the list. This stores that heading tag and whether it was
+ * special. 'consumeHeading' resets this._
+ */
+ this.lastContainerType = Container_Normal;
+
+ /**
+ * this contains the text from the last begin tag until now. It is reset
+ * at every begin tag. We can check it when we see a
"); + if (aItem.children) + yield this._writeContainerContents(aItem, aIndent); + if (aItem == this._root) + this._writeLine(aIndent + "
"); + }, + + _writeContainerContents: function (aItem, aIndent) { + let localIndent = aIndent + EXPORT_INDENT; + + for (let child of aItem.children) { + if (child.annos && child.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) + this._writeLivemark(child, localIndent); + else if (child.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) + yield this._writeContainer(child, localIndent); + else if (child.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) + this._writeSeparator(child, localIndent); + else + yield this._writeItem(child, localIndent); + } + }, + + _writeSeparator: function (aItem, aIndent) { + this._write(aIndent + "