tags that have no href.
if (href) {
// Save the address if it's valid. Note that we ignore errors if this is a
// feed since href is optional for them.
try {
frame.previousLink = NetUtil.newURI(href);
} catch(e) {
if (!frame.previousFeed) {
frame.previousLink = null;
return;
}
}
} else {
frame.previousLink = null;
// The exception is for feeds, where the href is an optional component
// indicating the source web site.
if (!frame.previousFeed) {
return;
}
}
// Save bookmark's last modified date.
if (lastModified) {
frame.previousLastModifiedDate =
this._convertImportedDateToInternalDate(lastModified);
}
// If this is a live bookmark, we will handle it in HandleLinkEnd(), so we
// can skip bookmark creation.
if (frame.previousFeed) {
return;
}
// Create the bookmark. The title is unknown for now, we will set it later.
try {
frame.previousId =
PlacesUtils.bookmarks.insertBookmark(frame.containerId,
frame.previousLink,
PlacesUtils.bookmarks.DEFAULT_INDEX,
"");
} catch(e) {
return;
}
// Set the date added value, if we have it.
if (dateAdded) {
try {
PlacesUtils.bookmarks.setItemDateAdded(frame.previousId,
this._convertImportedDateToInternalDate(dateAdded));
} catch(e) {
}
}
// Save the favicon.
if (icon || iconUri) {
let iconUriObject;
try {
iconUriObject = NetUtil.newURI(iconUri);
} catch(e) {
}
if (icon || iconUriObject) {
try {
this._setFaviconForURI(frame.previousLink, iconUriObject, icon);
} catch(e) {
}
}
}
// Save the keyword.
if (keyword) {
try {
PlacesUtils.bookmarks.setKeywordForBookmark(frame.previousId, keyword);
if (postData) {
PlacesUtils.annotations.setItemAnnotation(frame.previousId,
PlacesUtils.POST_DATA_ANNO,
postData,
0,
PlacesUtils.annotations.EXPIRE_NEVER);
}
} catch(e) {
}
}
// Set load-in-sidebar annotation for the bookmark.
if (webPanel && webPanel.toLowerCase() == "true") {
try {
PlacesUtils.annotations.setItemAnnotation(frame.previousId,
LOAD_IN_SIDEBAR_ANNO,
1,
0,
PlacesUtils.annotations.EXPIRE_NEVER);
} catch(e) {
}
}
// Import last charset.
if (lastCharset) {
PlacesUtils.setCharsetForURI(frame.previousLink, lastCharset);
}
},
_handleContainerBegin: function handleContainerBegin() {
this._curFrame.containerNesting++;
},
/**
* Our "indent" count has decreased, and when we hit 0 that means that this
* container is complete and we need to pop back to the outer frame. Never
* pop the toplevel frame
*/
_handleContainerEnd: function handleContainerEnd() {
let frame = this._curFrame;
if (frame.containerNesting > 0)
frame.containerNesting --;
if (this._frames.length > 1 && frame.containerNesting == 0) {
// we also need to re-set the imported last-modified date here. Otherwise
// the addition of items will override the imported field.
let prevFrame = this._previousFrame;
if (prevFrame.previousLastModifiedDate > 0) {
PlacesUtils.bookmarks.setItemLastModified(frame.containerId,
prevFrame.previousLastModifiedDate);
}
this._frames.pop();
}
},
/**
* Creates the new frame for this heading now that we know the name of the
* container (tokens since the heading open tag will have been placed in
* previousText).
*/
_handleHeadEnd: function handleHeadEnd() {
this._newFrame();
},
/**
* Saves the title for the given bookmark.
*/
_handleLinkEnd: function handleLinkEnd() {
let frame = this._curFrame;
frame.previousText = frame.previousText.trim();
try {
if (frame.previousFeed) {
// The is a live bookmark. We create it here since in HandleLinkBegin we
// don't know the title.
PlacesUtils.livemarks.addLivemark({
"title": frame.previousText,
"parentId": frame.containerId,
"index": PlacesUtils.bookmarks.DEFAULT_INDEX,
"feedURI": frame.previousFeed,
"siteURI": frame.previousLink,
});
} else if (frame.previousLink) {
// This is a common bookmark.
PlacesUtils.bookmarks.setItemTitle(frame.previousId,
frame.previousText);
}
} catch(e) {
}
// Set last modified date as the last change.
if (frame.previousId > 0 && frame.previousLastModifiedDate > 0) {
try {
PlacesUtils.bookmarks.setItemLastModified(frame.previousId,
frame.previousLastModifiedDate);
} catch(e) {
}
// Note: don't clear previousLastModifiedDate, because if this item has a
// description, we'll need to set it again.
}
frame.previousText = "";
},
_openContainer: function openContainer(aElt) {
if (aElt.namespaceURI != "http://www.w3.org/1999/xhtml") {
return;
}
switch(aElt.localName) {
case "h1":
this._handleHead1Begin(aElt);
break;
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
this._handleHeadBegin(aElt);
break;
case "a":
this._handleLinkBegin(aElt);
break;
case "dl":
case "ul":
case "menu":
this._handleContainerBegin();
break;
case "dd":
this._curFrame.inDescription = true;
break;
case "hr":
this._handleSeparator(aElt);
break;
}
},
_closeContainer: function closeContainer(aElt) {
let frame = this._curFrame;
// see the comment for the definition of inDescription. Basically, we commit
// any text in previousText to the description of the node/folder if there
// is any.
if (frame.inDescription) {
// NOTE ES5 trim trims more than the previous C++ trim.
frame.previousText = frame.previousText.trim(); // important
if (frame.previousText) {
let itemId = !frame.previousLink ? frame.containerId
: frame.previousId;
try {
if (!PlacesUtils.annotations.itemHasAnnotation(itemId, DESCRIPTION_ANNO)) {
PlacesUtils.annotations.setItemAnnotation(itemId,
DESCRIPTION_ANNO,
frame.previousText,
0,
PlacesUtils.annotations.EXPIRE_NEVER);
}
} catch(e) {
}
frame.previousText = "";
// Set last-modified a 2nd time for all items with descriptions
// we need to set last-modified as the *last* step in processing
// any item type in the bookmarks.html file, so that we do
// not overwrite the imported value. for items without descriptions,
// setting this value after setting the item title is that
// last point at which we can save this value before it gets reset.
// for items with descriptions, it must set after that point.
// however, at the point at which we set the title, there's no way
// to determine if there will be a description following,
// so we need to set the last-modified-date at both places.
let lastModified;
if (!frame.previousLink) {
lastModified = this._previousFrame.previousLastModifiedDate;
} else {
lastModified = frame.previousLastModifiedDate;
}
if (itemId > 0 && lastModified > 0) {
PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified);
}
}
frame.inDescription = false;
}
if (aElt.namespaceURI != "http://www.w3.org/1999/xhtml") {
return;
}
switch(aElt.localName) {
case "dl":
case "ul":
case "menu":
this._handleContainerEnd();
break;
case "dt":
break;
case "h1":
// ignore
break;
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
this._handleHeadEnd();
break;
case "a":
this._handleLinkEnd();
break;
default:
break;
}
},
_appendText: function appendText(str) {
this._curFrame.previousText += str;
},
/**
* data is a string that is a data URI for the favicon. Our job is to
* decode it and store it in the favicon service.
*
* When aIconURI is non-null, we will use that as the URI of the favicon
* when storing in the favicon service.
*
* When aIconURI is null, we have to make up a URI for this favicon so that
* it can be stored in the service. The real one will be set the next time
* the user visits the page. Our made up one should get expired when the
* page no longer references it.
*/
_setFaviconForURI: function setFaviconForURI(aPageURI, aIconURI, aData) {
// if the input favicon URI is a chrome: URI, then we just save it and don't
// worry about data
if (aIconURI) {
if (aIconURI.schemeIs("chrome")) {
PlacesUtils.favicons.setAndFetchFaviconForPage(aPageURI, aIconURI,
false,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
return;
}
}
// some bookmarks have placeholder URIs that contain just "data:"
// ignore these
if (aData.length <= 5) {
return;
}
let faviconURI;
if (aIconURI) {
faviconURI = aIconURI;
} else {
// Make up a favicon URI for this page. Later, we'll make sure that this
// favicon URI is always associated with local favicon data, so that we
// don't load this URI from the network.
let faviconSpec = "http://www.mozilla.org/2005/made-up-favicon/"
+ serialNumber
+ "-"
+ new Date().getTime();
faviconURI = NetUtil.newURI(faviconSpec);
serialNumber++;
}
// This could fail if the favicon is bigger than defined limit, in such a
// case neither the favicon URI nor the favicon data will be saved. If the
// bookmark is visited again later, the URI and data will be fetched.
PlacesUtils.favicons.replaceFaviconDataFromDataURL(faviconURI, aData);
PlacesUtils.favicons.setAndFetchFaviconForPage(aPageURI, faviconURI, false, PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
},
/**
* Converts a string date in seconds to an int date in microseconds
*/
_convertImportedDateToInternalDate: function convertImportedDateToInternalDate(aDate) {
if (aDate && !isNaN(aDate)) {
return parseInt(aDate) * 1000000; // in bookmarks.html this value is in seconds, not microseconds
} else {
return Date.now();
}
},
runBatched: function runBatched(aDoc) {
if (!aDoc) {
return;
}
if (this._isImportDefaults) {
PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarksMenuFolderId);
PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.toolbarFolderId);
PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
}
let current = aDoc;
let next;
for (;;) {
switch (current.nodeType) {
case Ci.nsIDOMNode.ELEMENT_NODE:
this._openContainer(current);
break;
case Ci.nsIDOMNode.TEXT_NODE:
this._appendText(current.data);
break;
}
if ((next = current.firstChild)) {
current = next;
continue;
}
for (;;) {
if (current.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
this._closeContainer(current);
}
if (current == aDoc) {
return;
}
if ((next = current.nextSibling)) {
current = next;
break;
}
current = current.parentNode;
}
}
},
_walkTreeForImport: function walkTreeForImport(aDoc) {
PlacesUtils.bookmarks.runInBatchMode(this, aDoc);
},
_notifyObservers: function notifyObservers(topic) {
Services.obs.notifyObservers(null,
topic,
this._isImportDefaults ? "html-initial"
: "html");
},
importFromURL: function importFromURL(aUrlString, aCallback) {
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
xhr.onload = (function onload() {
try {
this._walkTreeForImport(xhr.responseXML);
this._notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS);
deferred.resolve();
} catch(e) {
this._notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
deferred.reject(e);
throw e;
}
}).bind(this);
xhr.onabort = xhr.onerror = xhr.ontimeout = (function handleFail() {
this._notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
deferred.reject(new Error("xmlhttprequest failed"));
}).bind(this);
this._notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN);
try {
xhr.open("GET", aUrlString);
xhr.responseType = "document";
xhr.overrideMimeType("text/html");
xhr.send();
} catch (e) {
this._notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
deferred.reject(e);
}
return deferred.promise;
},
};
function BookmarkExporter() { }
BookmarkExporter.prototype = {
/**
* Provides HTML escaping for use in HTML attributes and body of the bookmarks
* file, compatible with the old bookmarks system.
*/
escapeHtml: function escapeHtml(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.
*/
escapeUrl: function escapeUrl(aText) {
return (aText || "").replace("\"", "%22", "g");
},
exportToFile: function exportToFile(aLocalFile) {
return Task.spawn(this._doExportToFile(aLocalFile));
},
_doExportToFile: function doExportToFile(aLocalFile) {
// Create a file that can be accessed by the current user only.
let safeFileOut = Cc["@mozilla.org/network/safe-file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
safeFileOut.init(aLocalFile,
FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE
| FileUtils.MODE_TRUNCATE,
parseInt("0600", 8), 0);
try {
// We need a buffered output stream for performance. See bug 202477.
let bufferedOut = Cc["@mozilla.org/network/buffered-output-stream;1"]
.createInstance(Ci.nsIBufferedOutputStream);
bufferedOut.init(safeFileOut, 4096);
try {
// Write bookmarks in UTF-8.
this._converterOut = Cc["@mozilla.org/intl/converter-output-stream;1"]
.createInstance(Ci.nsIConverterOutputStream);
this._converterOut.init(bufferedOut, "utf-8", 0, 0);
try {
yield this._doExport();
// Flush the buffer and retain the target file on success only.
bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
} finally {
this._converterOut.close();
this._converterOut = null;
}
} finally {
bufferedOut.close();
}
} finally {
safeFileOut.close();
}
},
_converterOut: null,
_write: function write(aText) {
this._converterOut.writeString(aText || "");
},
_writeLine: function writeLine(aText) {
this._write(aText + EXPORT_NEWLINE);
},
_doExport: function doExport() {
this._writeLine("");
this._writeLine("");
this._writeLine("");
this._writeLine("Bookmarks");
// Write the Bookmarks Menu as the outer container.
let root = PlacesUtils.getFolderContents(
PlacesUtils.bookmarksMenuFolderId).root;
try {
this._writeLine("" + this.escapeHtml(root.title) + "
");
this._writeLine("");
this._writeLine("");
yield this._writeContainerContents(root, "");
} finally {
root.containerOpen = false;
}
// Write the Bookmarks Toolbar as a child item for backwards compatibility.
root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root;
try {
if (root.childCount > 0) {
yield this._writeContainer(root, EXPORT_INDENT);
}
} finally {
root.containerOpen = false;
}
// Write the Unfiled Bookmarks as a child item for backwards compatibility.
root = PlacesUtils.getFolderContents(
PlacesUtils.unfiledBookmarksFolderId).root;
try {
if (root.childCount > 0) {
yield this._writeContainer(root, EXPORT_INDENT);
}
} finally {
root.containerOpen = false;
}
this._writeLine("
");
},
_writeContainer: function writeContainer(aItem, aIndent) {
this._write(aIndent + "
" + this.escapeHtml(aItem.title) + "
");
yield this._writeDescription(aItem);
this._writeLine(aIndent + "");
yield this._writeContainerContents(aItem, aIndent);
this._writeLine(aIndent + "
");
},
_writeContainerContents: function writeContainerContents(aItem, aIndent) {
let localIndent = aIndent + EXPORT_INDENT;
for (let i = 0; i < aItem.childCount; ++i) {
let child = aItem.getChild(i);
if (child.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER) {
// Since the livemarks service is asynchronous, use the annotations
// service to get the information for now.
if (PlacesUtils.annotations
.itemHasAnnotation(child.itemId,
PlacesUtils.LMANNO_FEEDURI)) {
yield this._writeLivemark(child, localIndent);
} else {
// This is a normal folder, open it.
PlacesUtils.asContainer(child).containerOpen = true;
try {
yield this._writeContainer(child, localIndent);
} finally {
child.containerOpen = false;
}
}
} else if (child.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
yield this._writeSeparator(child, localIndent);
} else {
yield this._writeItem(child, localIndent);
}
}
},
_writeSeparator: function writeSeparator(aItem, aIndent) {
this._write(aIndent + "
");
},
_writeLivemark: function writeLivemark(aItem, aIndent) {
this._write(aIndent + "" + this.escapeHtml(aItem.title) + "");
yield this._writeDescription(aItem);
},
_writeItem: function writeItem(aItem, aIndent) {
let itemUri = null;
try {
itemUri = NetUtil.newURI(aItem.uri);
} catch (ex) {
// If the item URI is invalid, skip the item instead of failing later.
return;
}
this._write(aIndent + "" + this.escapeHtml(aItem.title) + "");
yield this._writeDescription(aItem);
},
_writeDateAttributes: function writeDateAttributes(aItem) {
if (aItem.dateAdded) {
this._write(" ADD_DATE=\"" +
Math.floor(aItem.dateAdded / MICROSEC_PER_SEC) + "\"");
}
if (aItem.lastModified) {
this._write(" LAST_MODIFIED=\"" +
Math.floor(aItem.lastModified / MICROSEC_PER_SEC) + "\"");
}
},
_writeFaviconAttribute: function writeFaviconAttribute(aItemUri) {
let [faviconURI, dataLen, data] = yield this._promiseFaviconData(aItemUri);
if (!faviconURI) {
// Skip in case of errors.
return;
}
this._write(" ICON_URI=\"" + this.escapeUrl(faviconURI.spec) + "\"");
if (!faviconURI.schemeIs("chrome") && dataLen > 0) {
let faviconContents = "data:image/png;base64," +
base64EncodeString(String.fromCharCode.apply(String, data));
this._write(" ICON=\"" + faviconContents + "\"");
}
},
_promiseFaviconData: function(aPageURI) {
var deferred = Promise.defer();
PlacesUtils.favicons.getFaviconDataForPage(aPageURI,
function (aURI, aDataLen, aData, aMimeType) {
deferred.resolve([aURI, aDataLen, aData, aMimeType]);
});
return deferred.promise;
},
_writeDescription: function writeDescription(aItem) {
if (PlacesUtils.annotations.itemHasAnnotation(aItem.itemId,
DESCRIPTION_ANNO)) {
let description = PlacesUtils.annotations
.getItemAnnotation(aItem.itemId,
DESCRIPTION_ANNO);
// The description is not indented.
this._writeLine("" + this.escapeHtml(description));
}
},
};