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("&", "&", "g") 1.113 + .replace("<", "<", "g") 1.114 + .replace(">", ">", "g") 1.115 + .replace("\"", """, "g") 1.116 + .replace("'", "'", "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 +};