/* 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: " + aFilePath);
}
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 + "