toolkit/components/places/BookmarkHTMLUtils.jsm

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 /**
michael@0 6 * This file works on the old-style "bookmarks.html" file. It includes
michael@0 7 * functions to import and export existing bookmarks to this file format.
michael@0 8 *
michael@0 9 * Format
michael@0 10 * ------
michael@0 11 *
michael@0 12 * Primary heading := h1
michael@0 13 * Old version used this to set attributes on the bookmarks RDF root, such
michael@0 14 * as the last modified date. We only use H1 to check for the attribute
michael@0 15 * PLACES_ROOT, which tells us that this hierarchy root is the places root.
michael@0 16 * For backwards compatibility, if we don't find this, we assume that the
michael@0 17 * hierarchy is rooted at the bookmarks menu.
michael@0 18 * Heading := any heading other than h1
michael@0 19 * Old version used this to set attributes on the current container. We only
michael@0 20 * care about the content of the heading container, which contains the title
michael@0 21 * of the bookmark container.
michael@0 22 * Bookmark := a
michael@0 23 * HREF is the destination of the bookmark
michael@0 24 * FEEDURL is the URI of the RSS feed if this is a livemark.
michael@0 25 * LAST_CHARSET is stored as an annotation so that the next time we go to
michael@0 26 * that page we remember the user's preference.
michael@0 27 * WEB_PANEL is set to "true" if the bookmark should be loaded in the sidebar.
michael@0 28 * ICON will be stored in the favicon service
michael@0 29 * ICON_URI is new for places bookmarks.html, it refers to the original
michael@0 30 * URI of the favicon so we don't have to make up favicon URLs.
michael@0 31 * Text of the <a> container is the name of the bookmark
michael@0 32 * Ignored: LAST_VISIT, ID (writing out non-RDF IDs can confuse Firefox 2)
michael@0 33 * Bookmark comment := dd
michael@0 34 * This affects the previosly added bookmark
michael@0 35 * Separator := hr
michael@0 36 * Insert a separator into the current container
michael@0 37 * The folder hierarchy is defined by <dl>/<ul>/<menu> (the old importing code
michael@0 38 * handles all these cases, when we write, use <dl>).
michael@0 39 *
michael@0 40 * Overall design
michael@0 41 * --------------
michael@0 42 *
michael@0 43 * We need to emulate a recursive parser. A "Bookmark import frame" is created
michael@0 44 * corresponding to each folder we encounter. These are arranged in a stack,
michael@0 45 * and contain all the state we need to keep track of.
michael@0 46 *
michael@0 47 * A frame is created when we find a heading, which defines a new container.
michael@0 48 * The frame also keeps track of the nesting of <DL>s, (in well-formed
michael@0 49 * bookmarks files, these will have a 1-1 correspondence with frames, but we
michael@0 50 * try to be a little more flexible here). When the nesting count decreases
michael@0 51 * to 0, then we know a frame is complete and to pop back to the previous
michael@0 52 * frame.
michael@0 53 *
michael@0 54 * Note that a lot of things happen when tags are CLOSED because we need to
michael@0 55 * get the text from the content of the tag. For example, link and heading tags
michael@0 56 * both require the content (= title) before actually creating it.
michael@0 57 */
michael@0 58
michael@0 59 this.EXPORTED_SYMBOLS = [ "BookmarkHTMLUtils" ];
michael@0 60
michael@0 61 const Ci = Components.interfaces;
michael@0 62 const Cc = Components.classes;
michael@0 63 const Cu = Components.utils;
michael@0 64
michael@0 65 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 66 Cu.import("resource://gre/modules/Services.jsm");
michael@0 67 Cu.import("resource://gre/modules/NetUtil.jsm");
michael@0 68 Cu.import("resource://gre/modules/osfile.jsm");
michael@0 69 Cu.import("resource://gre/modules/FileUtils.jsm");
michael@0 70 Cu.import("resource://gre/modules/PlacesUtils.jsm");
michael@0 71 Cu.import("resource://gre/modules/Promise.jsm");
michael@0 72 Cu.import("resource://gre/modules/Task.jsm");
michael@0 73
michael@0 74 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
michael@0 75 "resource://gre/modules/PlacesBackups.jsm");
michael@0 76 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
michael@0 77 "resource://gre/modules/Deprecated.jsm");
michael@0 78
michael@0 79 const Container_Normal = 0;
michael@0 80 const Container_Toolbar = 1;
michael@0 81 const Container_Menu = 2;
michael@0 82 const Container_Unfiled = 3;
michael@0 83 const Container_Places = 4;
michael@0 84
michael@0 85 const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
michael@0 86 const DESCRIPTION_ANNO = "bookmarkProperties/description";
michael@0 87
michael@0 88 const MICROSEC_PER_SEC = 1000000;
michael@0 89
michael@0 90 const EXPORT_INDENT = " "; // four spaces
michael@0 91
michael@0 92 // Counter used to build fake favicon urls.
michael@0 93 let serialNumber = 0;
michael@0 94
michael@0 95 function base64EncodeString(aString) {
michael@0 96 let stream = Cc["@mozilla.org/io/string-input-stream;1"]
michael@0 97 .createInstance(Ci.nsIStringInputStream);
michael@0 98 stream.setData(aString, aString.length);
michael@0 99 let encoder = Cc["@mozilla.org/scriptablebase64encoder;1"]
michael@0 100 .createInstance(Ci.nsIScriptableBase64Encoder);
michael@0 101 return encoder.encodeToString(stream, aString.length);
michael@0 102 }
michael@0 103
michael@0 104 /**
michael@0 105 * Provides HTML escaping for use in HTML attributes and body of the bookmarks
michael@0 106 * file, compatible with the old bookmarks system.
michael@0 107 */
michael@0 108 function escapeHtmlEntities(aText) {
michael@0 109 return (aText || "").replace("&", "&amp;", "g")
michael@0 110 .replace("<", "&lt;", "g")
michael@0 111 .replace(">", "&gt;", "g")
michael@0 112 .replace("\"", "&quot;", "g")
michael@0 113 .replace("'", "&#39;", "g");
michael@0 114 }
michael@0 115
michael@0 116 /**
michael@0 117 * Provides URL escaping for use in HTML attributes of the bookmarks file,
michael@0 118 * compatible with the old bookmarks system.
michael@0 119 */
michael@0 120 function escapeUrl(aText) {
michael@0 121 return (aText || "").replace("\"", "%22", "g");
michael@0 122 }
michael@0 123
michael@0 124 function notifyObservers(aTopic, aInitialImport) {
michael@0 125 Services.obs.notifyObservers(null, aTopic, aInitialImport ? "html-initial"
michael@0 126 : "html");
michael@0 127 }
michael@0 128
michael@0 129 function promiseSoon() {
michael@0 130 let deferred = Promise.defer();
michael@0 131 Services.tm.mainThread.dispatch(deferred.resolve,
michael@0 132 Ci.nsIThread.DISPATCH_NORMAL);
michael@0 133 return deferred.promise;
michael@0 134 }
michael@0 135
michael@0 136 this.BookmarkHTMLUtils = Object.freeze({
michael@0 137 /**
michael@0 138 * Loads the current bookmarks hierarchy from a "bookmarks.html" file.
michael@0 139 *
michael@0 140 * @param aSpec
michael@0 141 * String containing the "file:" URI for the existing "bookmarks.html"
michael@0 142 * file to be loaded.
michael@0 143 * @param aInitialImport
michael@0 144 * Whether this is the initial import executed on a new profile.
michael@0 145 *
michael@0 146 * @return {Promise}
michael@0 147 * @resolves When the new bookmarks have been created.
michael@0 148 * @rejects JavaScript exception.
michael@0 149 */
michael@0 150 importFromURL: function BHU_importFromURL(aSpec, aInitialImport) {
michael@0 151 return Task.spawn(function* () {
michael@0 152 notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
michael@0 153 try {
michael@0 154 let importer = new BookmarkImporter(aInitialImport);
michael@0 155 yield importer.importFromURL(aSpec);
michael@0 156
michael@0 157 notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aInitialImport);
michael@0 158 } catch(ex) {
michael@0 159 Cu.reportError("Failed to import bookmarks from " + aSpec + ": " + ex);
michael@0 160 notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED, aInitialImport);
michael@0 161 throw ex;
michael@0 162 }
michael@0 163 });
michael@0 164 },
michael@0 165
michael@0 166 /**
michael@0 167 * Loads the current bookmarks hierarchy from a "bookmarks.html" file.
michael@0 168 *
michael@0 169 * @param aFilePath
michael@0 170 * OS.File path string of the "bookmarks.html" file to be loaded.
michael@0 171 * @param aInitialImport
michael@0 172 * Whether this is the initial import executed on a new profile.
michael@0 173 *
michael@0 174 * @return {Promise}
michael@0 175 * @resolves When the new bookmarks have been created.
michael@0 176 * @rejects JavaScript exception.
michael@0 177 * @deprecated passing an nsIFile is deprecated
michael@0 178 */
michael@0 179 importFromFile: function BHU_importFromFile(aFilePath, aInitialImport) {
michael@0 180 if (aFilePath instanceof Ci.nsIFile) {
michael@0 181 Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.importFromFile " +
michael@0 182 "is deprecated. Please use an OS.File path string instead.",
michael@0 183 "https://developer.mozilla.org/docs/JavaScript_OS.File");
michael@0 184 aFilePath = aFilePath.path;
michael@0 185 }
michael@0 186
michael@0 187 return Task.spawn(function* () {
michael@0 188 notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
michael@0 189 try {
michael@0 190 if (!(yield OS.File.exists(aFilePath)))
michael@0 191 throw new Error("Cannot import from nonexisting html file");
michael@0 192
michael@0 193 let importer = new BookmarkImporter(aInitialImport);
michael@0 194 yield importer.importFromURL(OS.Path.toFileURI(aFilePath));
michael@0 195
michael@0 196 notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aInitialImport);
michael@0 197 } catch(ex) {
michael@0 198 Cu.reportError("Failed to import bookmarks from " + aFilePath + ": " + ex);
michael@0 199 notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED, aInitialImport);
michael@0 200 throw ex;
michael@0 201 }
michael@0 202 });
michael@0 203 },
michael@0 204
michael@0 205 /**
michael@0 206 * Saves the current bookmarks hierarchy to a "bookmarks.html" file.
michael@0 207 *
michael@0 208 * @param aFilePath
michael@0 209 * OS.File path string for the "bookmarks.html" file to be created.
michael@0 210 *
michael@0 211 * @return {Promise}
michael@0 212 * @resolves To the exported bookmarks count when the file has been created.
michael@0 213 * @rejects JavaScript exception.
michael@0 214 * @deprecated passing an nsIFile is deprecated
michael@0 215 */
michael@0 216 exportToFile: function BHU_exportToFile(aFilePath) {
michael@0 217 if (aFilePath instanceof Ci.nsIFile) {
michael@0 218 Deprecated.warning("Passing an nsIFile to BookmarksHTMLUtils.exportToFile " +
michael@0 219 "is deprecated. Please use an OS.File path string instead.",
michael@0 220 "https://developer.mozilla.org/docs/JavaScript_OS.File");
michael@0 221 aFilePath = aFilePath.path;
michael@0 222 }
michael@0 223 return Task.spawn(function* () {
michael@0 224 let [bookmarks, count] = yield PlacesBackups.getBookmarksTree();
michael@0 225 let startTime = Date.now();
michael@0 226
michael@0 227 // Report the time taken to convert the tree to HTML.
michael@0 228 let exporter = new BookmarkExporter(bookmarks);
michael@0 229 yield exporter.exportToFile(aFilePath);
michael@0 230
michael@0 231 try {
michael@0 232 Services.telemetry
michael@0 233 .getHistogramById("PLACES_EXPORT_TOHTML_MS")
michael@0 234 .add(Date.now() - startTime);
michael@0 235 } catch (ex) {
michael@0 236 Components.utils.reportError("Unable to report telemetry.");
michael@0 237 }
michael@0 238
michael@0 239 return count;
michael@0 240 });
michael@0 241 },
michael@0 242
michael@0 243 get defaultPath() {
michael@0 244 try {
michael@0 245 return Services.prefs.getCharPref("browser.bookmarks.file");
michael@0 246 } catch (ex) {}
michael@0 247 return OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.html")
michael@0 248 }
michael@0 249 });
michael@0 250
michael@0 251 function Frame(aFrameId) {
michael@0 252 this.containerId = aFrameId;
michael@0 253
michael@0 254 /**
michael@0 255 * How many <dl>s have been nested. Each frame/container should start
michael@0 256 * with a heading, and is then followed by a <dl>, <ul>, or <menu>. When
michael@0 257 * that list is complete, then it is the end of this container and we need
michael@0 258 * to pop back up one level for new items. If we never get an open tag for
michael@0 259 * one of these things, we should assume that the container is empty and
michael@0 260 * that things we find should be siblings of it. Normally, these <dl>s won't
michael@0 261 * be nested so this will be 0 or 1.
michael@0 262 */
michael@0 263 this.containerNesting = 0;
michael@0 264
michael@0 265 /**
michael@0 266 * when we find a heading tag, it actually affects the title of the NEXT
michael@0 267 * container in the list. This stores that heading tag and whether it was
michael@0 268 * special. 'consumeHeading' resets this._
michael@0 269 */
michael@0 270 this.lastContainerType = Container_Normal;
michael@0 271
michael@0 272 /**
michael@0 273 * this contains the text from the last begin tag until now. It is reset
michael@0 274 * at every begin tag. We can check it when we see a </a>, or </h3>
michael@0 275 * to see what the text content of that node should be.
michael@0 276 */
michael@0 277 this.previousText = "";
michael@0 278
michael@0 279 /**
michael@0 280 * true when we hit a <dd>, which contains the description for the preceding
michael@0 281 * <a> tag. We can't just check for </dd> like we can for </a> or </h3>
michael@0 282 * because if there is a sub-folder, it is actually a child of the <dd>
michael@0 283 * because the tag is never explicitly closed. If this is true and we see a
michael@0 284 * new open tag, that means to commit the description to the previous
michael@0 285 * bookmark.
michael@0 286 *
michael@0 287 * Additional weirdness happens when the previous <dt> tag contains a <h3>:
michael@0 288 * this means there is a new folder with the given description, and whose
michael@0 289 * children are contained in the following <dl> list.
michael@0 290 *
michael@0 291 * This is handled in openContainer(), which commits previous text if
michael@0 292 * necessary.
michael@0 293 */
michael@0 294 this.inDescription = false;
michael@0 295
michael@0 296 /**
michael@0 297 * contains the URL of the previous bookmark created. This is used so that
michael@0 298 * when we encounter a <dd>, we know what bookmark to associate the text with.
michael@0 299 * This is cleared whenever we hit a <h3>, so that we know NOT to save this
michael@0 300 * with a bookmark, but to keep it until
michael@0 301 */
michael@0 302 this.previousLink = null; // nsIURI
michael@0 303
michael@0 304 /**
michael@0 305 * contains the URL of the previous livemark, so that when the link ends,
michael@0 306 * and the livemark title is known, we can create it.
michael@0 307 */
michael@0 308 this.previousFeed = null; // nsIURI
michael@0 309
michael@0 310 /**
michael@0 311 * Contains the id of an imported, or newly created bookmark.
michael@0 312 */
michael@0 313 this.previousId = 0;
michael@0 314
michael@0 315 /**
michael@0 316 * Contains the date-added and last-modified-date of an imported item.
michael@0 317 * Used to override the values set by insertBookmark, createFolder, etc.
michael@0 318 */
michael@0 319 this.previousDateAdded = 0;
michael@0 320 this.previousLastModifiedDate = 0;
michael@0 321 }
michael@0 322
michael@0 323 function BookmarkImporter(aInitialImport) {
michael@0 324 this._isImportDefaults = aInitialImport;
michael@0 325 this._frames = new Array();
michael@0 326 this._frames.push(new Frame(PlacesUtils.bookmarksMenuFolderId));
michael@0 327 }
michael@0 328
michael@0 329 BookmarkImporter.prototype = {
michael@0 330
michael@0 331 _safeTrim: function safeTrim(aStr) {
michael@0 332 return aStr ? aStr.trim() : aStr;
michael@0 333 },
michael@0 334
michael@0 335 get _curFrame() {
michael@0 336 return this._frames[this._frames.length - 1];
michael@0 337 },
michael@0 338
michael@0 339 get _previousFrame() {
michael@0 340 return this._frames[this._frames.length - 2];
michael@0 341 },
michael@0 342
michael@0 343 /**
michael@0 344 * This is called when there is a new folder found. The folder takes the
michael@0 345 * name from the previous frame's heading.
michael@0 346 */
michael@0 347 _newFrame: function newFrame() {
michael@0 348 let containerId = -1;
michael@0 349 let frame = this._curFrame;
michael@0 350 let containerTitle = frame.previousText;
michael@0 351 frame.previousText = "";
michael@0 352 let containerType = frame.lastContainerType;
michael@0 353
michael@0 354 switch (containerType) {
michael@0 355 case Container_Normal:
michael@0 356 // append a new folder
michael@0 357 containerId =
michael@0 358 PlacesUtils.bookmarks.createFolder(frame.containerId,
michael@0 359 containerTitle,
michael@0 360 PlacesUtils.bookmarks.DEFAULT_INDEX);
michael@0 361 break;
michael@0 362 case Container_Places:
michael@0 363 containerId = PlacesUtils.placesRootId;
michael@0 364 break;
michael@0 365 case Container_Menu:
michael@0 366 containerId = PlacesUtils.bookmarksMenuFolderId;
michael@0 367 break;
michael@0 368 case Container_Unfiled:
michael@0 369 containerId = PlacesUtils.unfiledBookmarksFolderId;
michael@0 370 break;
michael@0 371 case Container_Toolbar:
michael@0 372 containerId = PlacesUtils.toolbarFolderId;
michael@0 373 break;
michael@0 374 default:
michael@0 375 // NOT REACHED
michael@0 376 throw new Error("Unreached");
michael@0 377 }
michael@0 378
michael@0 379 if (frame.previousDateAdded > 0) {
michael@0 380 try {
michael@0 381 PlacesUtils.bookmarks.setItemDateAdded(containerId, frame.previousDateAdded);
michael@0 382 } catch(e) {
michael@0 383 }
michael@0 384 frame.previousDateAdded = 0;
michael@0 385 }
michael@0 386 if (frame.previousLastModifiedDate > 0) {
michael@0 387 try {
michael@0 388 PlacesUtils.bookmarks.setItemLastModified(containerId, frame.previousLastModifiedDate);
michael@0 389 } catch(e) {
michael@0 390 }
michael@0 391 // don't clear last-modified, in case there's a description
michael@0 392 }
michael@0 393
michael@0 394 frame.previousId = containerId;
michael@0 395
michael@0 396 this._frames.push(new Frame(containerId));
michael@0 397 },
michael@0 398
michael@0 399 /**
michael@0 400 * Handles <hr> as a separator.
michael@0 401 *
michael@0 402 * @note Separators may have a title in old html files, though Places dropped
michael@0 403 * support for them.
michael@0 404 * We also don't import ADD_DATE or LAST_MODIFIED for separators because
michael@0 405 * pre-Places bookmarks did not support them.
michael@0 406 */
michael@0 407 _handleSeparator: function handleSeparator(aElt) {
michael@0 408 let frame = this._curFrame;
michael@0 409 try {
michael@0 410 frame.previousId =
michael@0 411 PlacesUtils.bookmarks.insertSeparator(frame.containerId,
michael@0 412 PlacesUtils.bookmarks.DEFAULT_INDEX);
michael@0 413 } catch(e) {}
michael@0 414 },
michael@0 415
michael@0 416 /**
michael@0 417 * Handles <H1>. We check for the attribute PLACES_ROOT and reset the
michael@0 418 * container id if it's found. Otherwise, the default bookmark menu
michael@0 419 * root is assumed and imported things will go into the bookmarks menu.
michael@0 420 */
michael@0 421 _handleHead1Begin: function handleHead1Begin(aElt) {
michael@0 422 if (this._frames.length > 1) {
michael@0 423 return;
michael@0 424 }
michael@0 425 if (aElt.hasAttribute("places_root")) {
michael@0 426 this._curFrame.containerId = PlacesUtils.placesRootId;
michael@0 427 }
michael@0 428 },
michael@0 429
michael@0 430 /**
michael@0 431 * Called for h2,h3,h4,h5,h6. This just stores the correct information in
michael@0 432 * the current frame; the actual new frame corresponding to the container
michael@0 433 * associated with the heading will be created when the tag has been closed
michael@0 434 * and we know the title (we don't know to create a new folder or to merge
michael@0 435 * with an existing one until we have the title).
michael@0 436 */
michael@0 437 _handleHeadBegin: function handleHeadBegin(aElt) {
michael@0 438 let frame = this._curFrame;
michael@0 439
michael@0 440 // after a heading, a previous bookmark is not applicable (for example, for
michael@0 441 // the descriptions contained in a <dd>). Neither is any previous head type
michael@0 442 frame.previousLink = null;
michael@0 443 frame.lastContainerType = Container_Normal;
michael@0 444
michael@0 445 // It is syntactically possible for a heading to appear after another heading
michael@0 446 // but before the <dl> that encloses that folder's contents. This should not
michael@0 447 // happen in practice, as the file will contain "<dl></dl>" sequence for
michael@0 448 // empty containers.
michael@0 449 //
michael@0 450 // Just to be on the safe side, if we encounter
michael@0 451 // <h3>FOO</h3>
michael@0 452 // <h3>BAR</h3>
michael@0 453 // <dl>...content 1...</dl>
michael@0 454 // <dl>...content 2...</dl>
michael@0 455 // we'll pop the stack when we find the h3 for BAR, treating that as an
michael@0 456 // implicit ending of the FOO container. The output will be FOO and BAR as
michael@0 457 // siblings. If there's another <dl> following (as in "content 2"), those
michael@0 458 // items will be treated as further siblings of FOO and BAR
michael@0 459 // This special frame popping business, of course, only happens when our
michael@0 460 // frame array has more than one element so we can avoid situations where
michael@0 461 // we don't have a frame to parse into anymore.
michael@0 462 if (frame.containerNesting == 0 && this._frames.length > 1) {
michael@0 463 this._frames.pop();
michael@0 464 }
michael@0 465
michael@0 466 // We have to check for some attributes to see if this is a "special"
michael@0 467 // folder, which will have different creation rules when the end tag is
michael@0 468 // processed.
michael@0 469 if (aElt.hasAttribute("personal_toolbar_folder")) {
michael@0 470 if (this._isImportDefaults) {
michael@0 471 frame.lastContainerType = Container_Toolbar;
michael@0 472 }
michael@0 473 } else if (aElt.hasAttribute("bookmarks_menu")) {
michael@0 474 if (this._isImportDefaults) {
michael@0 475 frame.lastContainerType = Container_Menu;
michael@0 476 }
michael@0 477 } else if (aElt.hasAttribute("unfiled_bookmarks_folder")) {
michael@0 478 if (this._isImportDefaults) {
michael@0 479 frame.lastContainerType = Container_Unfiled;
michael@0 480 }
michael@0 481 } else if (aElt.hasAttribute("places_root")) {
michael@0 482 if (this._isImportDefaults) {
michael@0 483 frame.lastContainerType = Container_Places;
michael@0 484 }
michael@0 485 } else {
michael@0 486 let addDate = aElt.getAttribute("add_date");
michael@0 487 if (addDate) {
michael@0 488 frame.previousDateAdded =
michael@0 489 this._convertImportedDateToInternalDate(addDate);
michael@0 490 }
michael@0 491 let modDate = aElt.getAttribute("last_modified");
michael@0 492 if (modDate) {
michael@0 493 frame.previousLastModifiedDate =
michael@0 494 this._convertImportedDateToInternalDate(modDate);
michael@0 495 }
michael@0 496 }
michael@0 497 this._curFrame.previousText = "";
michael@0 498 },
michael@0 499
michael@0 500 /*
michael@0 501 * Handles "<a" tags by creating a new bookmark. The title of the bookmark
michael@0 502 * will be the text content, which will be stuffed in previousText for us
michael@0 503 * and which will be saved by handleLinkEnd
michael@0 504 */
michael@0 505 _handleLinkBegin: function handleLinkBegin(aElt) {
michael@0 506 let frame = this._curFrame;
michael@0 507
michael@0 508 // Make sure that the feed URIs from previous frames are emptied.
michael@0 509 frame.previousFeed = null;
michael@0 510 // Make sure that the bookmark id from previous frames are emptied.
michael@0 511 frame.previousId = 0;
michael@0 512 // mPreviousText will hold link text, clear it.
michael@0 513 frame.previousText = "";
michael@0 514
michael@0 515 // Get the attributes we care about.
michael@0 516 let href = this._safeTrim(aElt.getAttribute("href"));
michael@0 517 let feedUrl = this._safeTrim(aElt.getAttribute("feedurl"));
michael@0 518 let icon = this._safeTrim(aElt.getAttribute("icon"));
michael@0 519 let iconUri = this._safeTrim(aElt.getAttribute("icon_uri"));
michael@0 520 let lastCharset = this._safeTrim(aElt.getAttribute("last_charset"));
michael@0 521 let keyword = this._safeTrim(aElt.getAttribute("shortcuturl"));
michael@0 522 let postData = this._safeTrim(aElt.getAttribute("post_data"));
michael@0 523 let webPanel = this._safeTrim(aElt.getAttribute("web_panel"));
michael@0 524 let micsumGenURI = this._safeTrim(aElt.getAttribute("micsum_gen_uri"));
michael@0 525 let generatedTitle = this._safeTrim(aElt.getAttribute("generated_title"));
michael@0 526 let dateAdded = this._safeTrim(aElt.getAttribute("add_date"));
michael@0 527 let lastModified = this._safeTrim(aElt.getAttribute("last_modified"));
michael@0 528
michael@0 529 // For feeds, get the feed URL. If it is invalid, mPreviousFeed will be
michael@0 530 // NULL and we'll create it as a normal bookmark.
michael@0 531 if (feedUrl) {
michael@0 532 frame.previousFeed = NetUtil.newURI(feedUrl);
michael@0 533 }
michael@0 534
michael@0 535 // Ignore <a> tags that have no href.
michael@0 536 if (href) {
michael@0 537 // Save the address if it's valid. Note that we ignore errors if this is a
michael@0 538 // feed since href is optional for them.
michael@0 539 try {
michael@0 540 frame.previousLink = NetUtil.newURI(href);
michael@0 541 } catch(e) {
michael@0 542 if (!frame.previousFeed) {
michael@0 543 frame.previousLink = null;
michael@0 544 return;
michael@0 545 }
michael@0 546 }
michael@0 547 } else {
michael@0 548 frame.previousLink = null;
michael@0 549 // The exception is for feeds, where the href is an optional component
michael@0 550 // indicating the source web site.
michael@0 551 if (!frame.previousFeed) {
michael@0 552 return;
michael@0 553 }
michael@0 554 }
michael@0 555
michael@0 556 // Save bookmark's last modified date.
michael@0 557 if (lastModified) {
michael@0 558 frame.previousLastModifiedDate =
michael@0 559 this._convertImportedDateToInternalDate(lastModified);
michael@0 560 }
michael@0 561
michael@0 562 // If this is a live bookmark, we will handle it in HandleLinkEnd(), so we
michael@0 563 // can skip bookmark creation.
michael@0 564 if (frame.previousFeed) {
michael@0 565 return;
michael@0 566 }
michael@0 567
michael@0 568 // Create the bookmark. The title is unknown for now, we will set it later.
michael@0 569 try {
michael@0 570 frame.previousId =
michael@0 571 PlacesUtils.bookmarks.insertBookmark(frame.containerId,
michael@0 572 frame.previousLink,
michael@0 573 PlacesUtils.bookmarks.DEFAULT_INDEX,
michael@0 574 "");
michael@0 575 } catch(e) {
michael@0 576 return;
michael@0 577 }
michael@0 578
michael@0 579 // Set the date added value, if we have it.
michael@0 580 if (dateAdded) {
michael@0 581 try {
michael@0 582 PlacesUtils.bookmarks.setItemDateAdded(frame.previousId,
michael@0 583 this._convertImportedDateToInternalDate(dateAdded));
michael@0 584 } catch(e) {
michael@0 585 }
michael@0 586 }
michael@0 587
michael@0 588 // Save the favicon.
michael@0 589 if (icon || iconUri) {
michael@0 590 let iconUriObject;
michael@0 591 try {
michael@0 592 iconUriObject = NetUtil.newURI(iconUri);
michael@0 593 } catch(e) {
michael@0 594 }
michael@0 595 if (icon || iconUriObject) {
michael@0 596 try {
michael@0 597 this._setFaviconForURI(frame.previousLink, iconUriObject, icon);
michael@0 598 } catch(e) {
michael@0 599 }
michael@0 600 }
michael@0 601 }
michael@0 602
michael@0 603 // Save the keyword.
michael@0 604 if (keyword) {
michael@0 605 try {
michael@0 606 PlacesUtils.bookmarks.setKeywordForBookmark(frame.previousId, keyword);
michael@0 607 if (postData) {
michael@0 608 PlacesUtils.annotations.setItemAnnotation(frame.previousId,
michael@0 609 PlacesUtils.POST_DATA_ANNO,
michael@0 610 postData,
michael@0 611 0,
michael@0 612 PlacesUtils.annotations.EXPIRE_NEVER);
michael@0 613 }
michael@0 614 } catch(e) {
michael@0 615 }
michael@0 616 }
michael@0 617
michael@0 618 // Set load-in-sidebar annotation for the bookmark.
michael@0 619 if (webPanel && webPanel.toLowerCase() == "true") {
michael@0 620 try {
michael@0 621 PlacesUtils.annotations.setItemAnnotation(frame.previousId,
michael@0 622 LOAD_IN_SIDEBAR_ANNO,
michael@0 623 1,
michael@0 624 0,
michael@0 625 PlacesUtils.annotations.EXPIRE_NEVER);
michael@0 626 } catch(e) {
michael@0 627 }
michael@0 628 }
michael@0 629
michael@0 630 // Import last charset.
michael@0 631 if (lastCharset) {
michael@0 632 PlacesUtils.setCharsetForURI(frame.previousLink, lastCharset);
michael@0 633 }
michael@0 634 },
michael@0 635
michael@0 636 _handleContainerBegin: function handleContainerBegin() {
michael@0 637 this._curFrame.containerNesting++;
michael@0 638 },
michael@0 639
michael@0 640 /**
michael@0 641 * Our "indent" count has decreased, and when we hit 0 that means that this
michael@0 642 * container is complete and we need to pop back to the outer frame. Never
michael@0 643 * pop the toplevel frame
michael@0 644 */
michael@0 645 _handleContainerEnd: function handleContainerEnd() {
michael@0 646 let frame = this._curFrame;
michael@0 647 if (frame.containerNesting > 0)
michael@0 648 frame.containerNesting --;
michael@0 649 if (this._frames.length > 1 && frame.containerNesting == 0) {
michael@0 650 // we also need to re-set the imported last-modified date here. Otherwise
michael@0 651 // the addition of items will override the imported field.
michael@0 652 let prevFrame = this._previousFrame;
michael@0 653 if (prevFrame.previousLastModifiedDate > 0) {
michael@0 654 PlacesUtils.bookmarks.setItemLastModified(frame.containerId,
michael@0 655 prevFrame.previousLastModifiedDate);
michael@0 656 }
michael@0 657 this._frames.pop();
michael@0 658 }
michael@0 659 },
michael@0 660
michael@0 661 /**
michael@0 662 * Creates the new frame for this heading now that we know the name of the
michael@0 663 * container (tokens since the heading open tag will have been placed in
michael@0 664 * previousText).
michael@0 665 */
michael@0 666 _handleHeadEnd: function handleHeadEnd() {
michael@0 667 this._newFrame();
michael@0 668 },
michael@0 669
michael@0 670 /**
michael@0 671 * Saves the title for the given bookmark.
michael@0 672 */
michael@0 673 _handleLinkEnd: function handleLinkEnd() {
michael@0 674 let frame = this._curFrame;
michael@0 675 frame.previousText = frame.previousText.trim();
michael@0 676
michael@0 677 try {
michael@0 678 if (frame.previousFeed) {
michael@0 679 // The is a live bookmark. We create it here since in HandleLinkBegin we
michael@0 680 // don't know the title.
michael@0 681 PlacesUtils.livemarks.addLivemark({
michael@0 682 "title": frame.previousText,
michael@0 683 "parentId": frame.containerId,
michael@0 684 "index": PlacesUtils.bookmarks.DEFAULT_INDEX,
michael@0 685 "feedURI": frame.previousFeed,
michael@0 686 "siteURI": frame.previousLink,
michael@0 687 }).then(null, Cu.reportError);
michael@0 688 } else if (frame.previousLink) {
michael@0 689 // This is a common bookmark.
michael@0 690 PlacesUtils.bookmarks.setItemTitle(frame.previousId,
michael@0 691 frame.previousText);
michael@0 692 }
michael@0 693 } catch(e) {
michael@0 694 }
michael@0 695
michael@0 696
michael@0 697 // Set last modified date as the last change.
michael@0 698 if (frame.previousId > 0 && frame.previousLastModifiedDate > 0) {
michael@0 699 try {
michael@0 700 PlacesUtils.bookmarks.setItemLastModified(frame.previousId,
michael@0 701 frame.previousLastModifiedDate);
michael@0 702 } catch(e) {
michael@0 703 }
michael@0 704 // Note: don't clear previousLastModifiedDate, because if this item has a
michael@0 705 // description, we'll need to set it again.
michael@0 706 }
michael@0 707
michael@0 708 frame.previousText = "";
michael@0 709
michael@0 710 },
michael@0 711
michael@0 712 _openContainer: function openContainer(aElt) {
michael@0 713 if (aElt.namespaceURI != "http://www.w3.org/1999/xhtml") {
michael@0 714 return;
michael@0 715 }
michael@0 716 switch(aElt.localName) {
michael@0 717 case "h1":
michael@0 718 this._handleHead1Begin(aElt);
michael@0 719 break;
michael@0 720 case "h2":
michael@0 721 case "h3":
michael@0 722 case "h4":
michael@0 723 case "h5":
michael@0 724 case "h6":
michael@0 725 this._handleHeadBegin(aElt);
michael@0 726 break;
michael@0 727 case "a":
michael@0 728 this._handleLinkBegin(aElt);
michael@0 729 break;
michael@0 730 case "dl":
michael@0 731 case "ul":
michael@0 732 case "menu":
michael@0 733 this._handleContainerBegin();
michael@0 734 break;
michael@0 735 case "dd":
michael@0 736 this._curFrame.inDescription = true;
michael@0 737 break;
michael@0 738 case "hr":
michael@0 739 this._handleSeparator(aElt);
michael@0 740 break;
michael@0 741 }
michael@0 742 },
michael@0 743
michael@0 744 _closeContainer: function closeContainer(aElt) {
michael@0 745 let frame = this._curFrame;
michael@0 746
michael@0 747 // see the comment for the definition of inDescription. Basically, we commit
michael@0 748 // any text in previousText to the description of the node/folder if there
michael@0 749 // is any.
michael@0 750 if (frame.inDescription) {
michael@0 751 // NOTE ES5 trim trims more than the previous C++ trim.
michael@0 752 frame.previousText = frame.previousText.trim(); // important
michael@0 753 if (frame.previousText) {
michael@0 754
michael@0 755 let itemId = !frame.previousLink ? frame.containerId
michael@0 756 : frame.previousId;
michael@0 757
michael@0 758 try {
michael@0 759 if (!PlacesUtils.annotations.itemHasAnnotation(itemId, DESCRIPTION_ANNO)) {
michael@0 760 PlacesUtils.annotations.setItemAnnotation(itemId,
michael@0 761 DESCRIPTION_ANNO,
michael@0 762 frame.previousText,
michael@0 763 0,
michael@0 764 PlacesUtils.annotations.EXPIRE_NEVER);
michael@0 765 }
michael@0 766 } catch(e) {
michael@0 767 }
michael@0 768 frame.previousText = "";
michael@0 769
michael@0 770 // Set last-modified a 2nd time for all items with descriptions
michael@0 771 // we need to set last-modified as the *last* step in processing
michael@0 772 // any item type in the bookmarks.html file, so that we do
michael@0 773 // not overwrite the imported value. for items without descriptions,
michael@0 774 // setting this value after setting the item title is that
michael@0 775 // last point at which we can save this value before it gets reset.
michael@0 776 // for items with descriptions, it must set after that point.
michael@0 777 // however, at the point at which we set the title, there's no way
michael@0 778 // to determine if there will be a description following,
michael@0 779 // so we need to set the last-modified-date at both places.
michael@0 780
michael@0 781 let lastModified;
michael@0 782 if (!frame.previousLink) {
michael@0 783 lastModified = this._previousFrame.previousLastModifiedDate;
michael@0 784 } else {
michael@0 785 lastModified = frame.previousLastModifiedDate;
michael@0 786 }
michael@0 787
michael@0 788 if (itemId > 0 && lastModified > 0) {
michael@0 789 PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified);
michael@0 790 }
michael@0 791 }
michael@0 792 frame.inDescription = false;
michael@0 793 }
michael@0 794
michael@0 795 if (aElt.namespaceURI != "http://www.w3.org/1999/xhtml") {
michael@0 796 return;
michael@0 797 }
michael@0 798 switch(aElt.localName) {
michael@0 799 case "dl":
michael@0 800 case "ul":
michael@0 801 case "menu":
michael@0 802 this._handleContainerEnd();
michael@0 803 break;
michael@0 804 case "dt":
michael@0 805 break;
michael@0 806 case "h1":
michael@0 807 // ignore
michael@0 808 break;
michael@0 809 case "h2":
michael@0 810 case "h3":
michael@0 811 case "h4":
michael@0 812 case "h5":
michael@0 813 case "h6":
michael@0 814 this._handleHeadEnd();
michael@0 815 break;
michael@0 816 case "a":
michael@0 817 this._handleLinkEnd();
michael@0 818 break;
michael@0 819 default:
michael@0 820 break;
michael@0 821 }
michael@0 822 },
michael@0 823
michael@0 824 _appendText: function appendText(str) {
michael@0 825 this._curFrame.previousText += str;
michael@0 826 },
michael@0 827
michael@0 828 /**
michael@0 829 * data is a string that is a data URI for the favicon. Our job is to
michael@0 830 * decode it and store it in the favicon service.
michael@0 831 *
michael@0 832 * When aIconURI is non-null, we will use that as the URI of the favicon
michael@0 833 * when storing in the favicon service.
michael@0 834 *
michael@0 835 * When aIconURI is null, we have to make up a URI for this favicon so that
michael@0 836 * it can be stored in the service. The real one will be set the next time
michael@0 837 * the user visits the page. Our made up one should get expired when the
michael@0 838 * page no longer references it.
michael@0 839 */
michael@0 840 _setFaviconForURI: function setFaviconForURI(aPageURI, aIconURI, aData) {
michael@0 841 // if the input favicon URI is a chrome: URI, then we just save it and don't
michael@0 842 // worry about data
michael@0 843 if (aIconURI) {
michael@0 844 if (aIconURI.schemeIs("chrome")) {
michael@0 845 PlacesUtils.favicons.setAndFetchFaviconForPage(aPageURI, aIconURI,
michael@0 846 false,
michael@0 847 PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
michael@0 848 return;
michael@0 849 }
michael@0 850 }
michael@0 851
michael@0 852 // some bookmarks have placeholder URIs that contain just "data:"
michael@0 853 // ignore these
michael@0 854 if (aData.length <= 5) {
michael@0 855 return;
michael@0 856 }
michael@0 857
michael@0 858 let faviconURI;
michael@0 859 if (aIconURI) {
michael@0 860 faviconURI = aIconURI;
michael@0 861 } else {
michael@0 862 // Make up a favicon URI for this page. Later, we'll make sure that this
michael@0 863 // favicon URI is always associated with local favicon data, so that we
michael@0 864 // don't load this URI from the network.
michael@0 865 let faviconSpec = "http://www.mozilla.org/2005/made-up-favicon/"
michael@0 866 + serialNumber
michael@0 867 + "-"
michael@0 868 + new Date().getTime();
michael@0 869 faviconURI = NetUtil.newURI(faviconSpec);
michael@0 870 serialNumber++;
michael@0 871 }
michael@0 872
michael@0 873 // This could fail if the favicon is bigger than defined limit, in such a
michael@0 874 // case neither the favicon URI nor the favicon data will be saved. If the
michael@0 875 // bookmark is visited again later, the URI and data will be fetched.
michael@0 876 PlacesUtils.favicons.replaceFaviconDataFromDataURL(faviconURI, aData);
michael@0 877 PlacesUtils.favicons.setAndFetchFaviconForPage(aPageURI, faviconURI, false, PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
michael@0 878 },
michael@0 879
michael@0 880 /**
michael@0 881 * Converts a string date in seconds to an int date in microseconds
michael@0 882 */
michael@0 883 _convertImportedDateToInternalDate: function convertImportedDateToInternalDate(aDate) {
michael@0 884 if (aDate && !isNaN(aDate)) {
michael@0 885 return parseInt(aDate) * 1000000; // in bookmarks.html this value is in seconds, not microseconds
michael@0 886 } else {
michael@0 887 return Date.now();
michael@0 888 }
michael@0 889 },
michael@0 890
michael@0 891 runBatched: function runBatched(aDoc) {
michael@0 892 if (!aDoc) {
michael@0 893 return;
michael@0 894 }
michael@0 895
michael@0 896 if (this._isImportDefaults) {
michael@0 897 PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarksMenuFolderId);
michael@0 898 PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.toolbarFolderId);
michael@0 899 PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
michael@0 900 }
michael@0 901
michael@0 902 let current = aDoc;
michael@0 903 let next;
michael@0 904 for (;;) {
michael@0 905 switch (current.nodeType) {
michael@0 906 case Ci.nsIDOMNode.ELEMENT_NODE:
michael@0 907 this._openContainer(current);
michael@0 908 break;
michael@0 909 case Ci.nsIDOMNode.TEXT_NODE:
michael@0 910 this._appendText(current.data);
michael@0 911 break;
michael@0 912 }
michael@0 913 if ((next = current.firstChild)) {
michael@0 914 current = next;
michael@0 915 continue;
michael@0 916 }
michael@0 917 for (;;) {
michael@0 918 if (current.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
michael@0 919 this._closeContainer(current);
michael@0 920 }
michael@0 921 if (current == aDoc) {
michael@0 922 return;
michael@0 923 }
michael@0 924 if ((next = current.nextSibling)) {
michael@0 925 current = next;
michael@0 926 break;
michael@0 927 }
michael@0 928 current = current.parentNode;
michael@0 929 }
michael@0 930 }
michael@0 931 },
michael@0 932
michael@0 933 _walkTreeForImport: function walkTreeForImport(aDoc) {
michael@0 934 PlacesUtils.bookmarks.runInBatchMode(this, aDoc);
michael@0 935 },
michael@0 936
michael@0 937 importFromURL: function importFromURL(aSpec) {
michael@0 938 let deferred = Promise.defer();
michael@0 939 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
michael@0 940 .createInstance(Ci.nsIXMLHttpRequest);
michael@0 941 xhr.onload = () => {
michael@0 942 try {
michael@0 943 this._walkTreeForImport(xhr.responseXML);
michael@0 944 deferred.resolve();
michael@0 945 } catch(e) {
michael@0 946 deferred.reject(e);
michael@0 947 throw e;
michael@0 948 }
michael@0 949 };
michael@0 950 xhr.onabort = xhr.onerror = xhr.ontimeout = () => {
michael@0 951 deferred.reject(new Error("xmlhttprequest failed"));
michael@0 952 };
michael@0 953 try {
michael@0 954 xhr.open("GET", aSpec);
michael@0 955 xhr.responseType = "document";
michael@0 956 xhr.overrideMimeType("text/html");
michael@0 957 xhr.send();
michael@0 958 } catch (e) {
michael@0 959 deferred.reject(e);
michael@0 960 }
michael@0 961 return deferred.promise;
michael@0 962 },
michael@0 963
michael@0 964 };
michael@0 965
michael@0 966 function BookmarkExporter(aBookmarksTree) {
michael@0 967 // Create a map of the roots.
michael@0 968 let rootsMap = new Map();
michael@0 969 for (let child of aBookmarksTree.children) {
michael@0 970 if (child.root)
michael@0 971 rootsMap.set(child.root, child);
michael@0 972 }
michael@0 973
michael@0 974 // For backwards compatibility reasons the bookmarks menu is the root, while
michael@0 975 // the bookmarks toolbar and unfiled bookmarks will be child items.
michael@0 976 this._root = rootsMap.get("bookmarksMenuFolder");
michael@0 977
michael@0 978 for (let key of [ "toolbarFolder", "unfiledBookmarksFolder" ]) {
michael@0 979 let root = rootsMap.get(key);
michael@0 980 if (root.children && root.children.length > 0) {
michael@0 981 if (!this._root.children)
michael@0 982 this._root.children = [];
michael@0 983 this._root.children.push(root);
michael@0 984 }
michael@0 985 }
michael@0 986 }
michael@0 987
michael@0 988 BookmarkExporter.prototype = {
michael@0 989 exportToFile: function exportToFile(aFilePath) {
michael@0 990 return Task.spawn(function* () {
michael@0 991 // Create a file that can be accessed by the current user only.
michael@0 992 let out = FileUtils.openAtomicFileOutputStream(new FileUtils.File(aFilePath));
michael@0 993 try {
michael@0 994 // We need a buffered output stream for performance. See bug 202477.
michael@0 995 let bufferedOut = Cc["@mozilla.org/network/buffered-output-stream;1"]
michael@0 996 .createInstance(Ci.nsIBufferedOutputStream);
michael@0 997 bufferedOut.init(out, 4096);
michael@0 998 try {
michael@0 999 // Write bookmarks in UTF-8.
michael@0 1000 this._converterOut = Cc["@mozilla.org/intl/converter-output-stream;1"]
michael@0 1001 .createInstance(Ci.nsIConverterOutputStream);
michael@0 1002 this._converterOut.init(bufferedOut, "utf-8", 0, 0);
michael@0 1003 try {
michael@0 1004 this._writeHeader();
michael@0 1005 yield this._writeContainer(this._root);
michael@0 1006 // Retain the target file on success only.
michael@0 1007 bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
michael@0 1008 } finally {
michael@0 1009 this._converterOut.close();
michael@0 1010 this._converterOut = null;
michael@0 1011 }
michael@0 1012 } finally {
michael@0 1013 bufferedOut.close();
michael@0 1014 }
michael@0 1015 } finally {
michael@0 1016 out.close();
michael@0 1017 }
michael@0 1018 }.bind(this));
michael@0 1019 },
michael@0 1020
michael@0 1021 _converterOut: null,
michael@0 1022
michael@0 1023 _write: function (aText) {
michael@0 1024 this._converterOut.writeString(aText || "");
michael@0 1025 },
michael@0 1026
michael@0 1027 _writeAttribute: function (aName, aValue) {
michael@0 1028 this._write(' ' + aName + '="' + aValue + '"');
michael@0 1029 },
michael@0 1030
michael@0 1031 _writeLine: function (aText) {
michael@0 1032 this._write(aText + "\n");
michael@0 1033 },
michael@0 1034
michael@0 1035 _writeHeader: function () {
michael@0 1036 this._writeLine("<!DOCTYPE NETSCAPE-Bookmark-file-1>");
michael@0 1037 this._writeLine("<!-- This is an automatically generated file.");
michael@0 1038 this._writeLine(" It will be read and overwritten.");
michael@0 1039 this._writeLine(" DO NOT EDIT! -->");
michael@0 1040 this._writeLine('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; ' +
michael@0 1041 'charset=UTF-8">');
michael@0 1042 this._writeLine("<TITLE>Bookmarks</TITLE>");
michael@0 1043 },
michael@0 1044
michael@0 1045 _writeContainer: function (aItem, aIndent = "") {
michael@0 1046 if (aItem == this._root) {
michael@0 1047 this._writeLine("<H1>" + escapeHtmlEntities(this._root.title) + "</H1>");
michael@0 1048 this._writeLine("");
michael@0 1049 }
michael@0 1050 else {
michael@0 1051 this._write(aIndent + "<DT><H3");
michael@0 1052 this._writeDateAttributes(aItem);
michael@0 1053
michael@0 1054 if (aItem.root === "toolbarFolder")
michael@0 1055 this._writeAttribute("PERSONAL_TOOLBAR_FOLDER", "true");
michael@0 1056 else if (aItem.root === "unfiledBookmarksFolder")
michael@0 1057 this._writeAttribute("UNFILED_BOOKMARKS_FOLDER", "true");
michael@0 1058 this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</H3>");
michael@0 1059 }
michael@0 1060
michael@0 1061 this._writeDescription(aItem, aIndent);
michael@0 1062
michael@0 1063 this._writeLine(aIndent + "<DL><p>");
michael@0 1064 if (aItem.children)
michael@0 1065 yield this._writeContainerContents(aItem, aIndent);
michael@0 1066 if (aItem == this._root)
michael@0 1067 this._writeLine(aIndent + "</DL>");
michael@0 1068 else
michael@0 1069 this._writeLine(aIndent + "</DL><p>");
michael@0 1070 },
michael@0 1071
michael@0 1072 _writeContainerContents: function (aItem, aIndent) {
michael@0 1073 let localIndent = aIndent + EXPORT_INDENT;
michael@0 1074
michael@0 1075 for (let child of aItem.children) {
michael@0 1076 if (child.annos && child.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI))
michael@0 1077 this._writeLivemark(child, localIndent);
michael@0 1078 else if (child.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
michael@0 1079 yield this._writeContainer(child, localIndent);
michael@0 1080 else if (child.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR)
michael@0 1081 this._writeSeparator(child, localIndent);
michael@0 1082 else
michael@0 1083 yield this._writeItem(child, localIndent);
michael@0 1084 }
michael@0 1085 },
michael@0 1086
michael@0 1087 _writeSeparator: function (aItem, aIndent) {
michael@0 1088 this._write(aIndent + "<HR");
michael@0 1089 // We keep exporting separator titles, but don't support them anymore.
michael@0 1090 if (aItem.title)
michael@0 1091 this._writeAttribute("NAME", escapeHtmlEntities(aItem.title));
michael@0 1092 this._write(">");
michael@0 1093 },
michael@0 1094
michael@0 1095 _writeLivemark: function (aItem, aIndent) {
michael@0 1096 this._write(aIndent + "<DT><A");
michael@0 1097 let feedSpec = aItem.annos.find(anno => anno.name == PlacesUtils.LMANNO_FEEDURI).value;
michael@0 1098 this._writeAttribute("FEEDURL", escapeUrl(feedSpec));
michael@0 1099 let siteSpecAnno = aItem.annos.find(anno => anno.name == PlacesUtils.LMANNO_SITEURI);
michael@0 1100 if (siteSpecAnno)
michael@0 1101 this._writeAttribute("HREF", escapeUrl(siteSpecAnno.value));
michael@0 1102 this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</A>");
michael@0 1103 this._writeDescription(aItem, aIndent);
michael@0 1104 },
michael@0 1105
michael@0 1106 _writeItem: function (aItem, aIndent) {
michael@0 1107 // This is a workaround for "too much recursion" error, due to the fact
michael@0 1108 // Task.jsm still uses old on-same-tick promises. It may be removed as
michael@0 1109 // soon as bug 887923 is fixed.
michael@0 1110 yield promiseSoon();
michael@0 1111 let uri = null;
michael@0 1112 try {
michael@0 1113 uri = NetUtil.newURI(aItem.uri);
michael@0 1114 } catch (ex) {
michael@0 1115 // If the item URI is invalid, skip the item instead of failing later.
michael@0 1116 return;
michael@0 1117 }
michael@0 1118
michael@0 1119 this._write(aIndent + "<DT><A");
michael@0 1120 this._writeAttribute("HREF", escapeUrl(aItem.uri));
michael@0 1121 this._writeDateAttributes(aItem);
michael@0 1122 yield this._writeFaviconAttribute(aItem);
michael@0 1123
michael@0 1124 let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(aItem.id);
michael@0 1125 if (aItem.keyword)
michael@0 1126 this._writeAttribute("SHORTCUTURL", escapeHtmlEntities(keyword));
michael@0 1127
michael@0 1128 let postDataAnno = aItem.annos &&
michael@0 1129 aItem.annos.find(anno => anno.name == PlacesUtils.POST_DATA_ANNO);
michael@0 1130 if (postDataAnno)
michael@0 1131 this._writeAttribute("POST_DATA", escapeHtmlEntities(postDataAnno.value));
michael@0 1132
michael@0 1133 if (aItem.annos && aItem.annos.some(anno => anno.name == LOAD_IN_SIDEBAR_ANNO))
michael@0 1134 this._writeAttribute("WEB_PANEL", "true");
michael@0 1135 if (aItem.charset)
michael@0 1136 this._writeAttribute("LAST_CHARSET", escapeHtmlEntities(aItem.charset));
michael@0 1137 if (aItem.tags)
michael@0 1138 this._writeAttribute("TAGS", aItem.tags);
michael@0 1139 this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</A>");
michael@0 1140 this._writeDescription(aItem, aIndent);
michael@0 1141 },
michael@0 1142
michael@0 1143 _writeDateAttributes: function (aItem) {
michael@0 1144 if (aItem.dateAdded)
michael@0 1145 this._writeAttribute("ADD_DATE",
michael@0 1146 Math.floor(aItem.dateAdded / MICROSEC_PER_SEC));
michael@0 1147 if (aItem.lastModified)
michael@0 1148 this._writeAttribute("LAST_MODIFIED",
michael@0 1149 Math.floor(aItem.lastModified / MICROSEC_PER_SEC));
michael@0 1150 },
michael@0 1151
michael@0 1152 _writeFaviconAttribute: function (aItem) {
michael@0 1153 if (!aItem.iconuri)
michael@0 1154 return;
michael@0 1155 let favicon;
michael@0 1156 try {
michael@0 1157 favicon = yield PlacesUtils.promiseFaviconData(aItem.uri);
michael@0 1158 } catch (ex) {
michael@0 1159 Components.utils.reportError("Unexpected Error trying to fetch icon data");
michael@0 1160 return;
michael@0 1161 }
michael@0 1162
michael@0 1163 this._writeAttribute("ICON_URI", escapeUrl(favicon.uri.spec));
michael@0 1164
michael@0 1165 if (!favicon.uri.schemeIs("chrome") && favicon.dataLen > 0) {
michael@0 1166 let faviconContents = "data:image/png;base64," +
michael@0 1167 base64EncodeString(String.fromCharCode.apply(String, favicon.data));
michael@0 1168 this._writeAttribute("ICON", faviconContents);
michael@0 1169 }
michael@0 1170 },
michael@0 1171
michael@0 1172 _writeDescription: function (aItem, aIndent) {
michael@0 1173 let descriptionAnno = aItem.annos &&
michael@0 1174 aItem.annos.find(anno => anno.name == DESCRIPTION_ANNO);
michael@0 1175 if (descriptionAnno)
michael@0 1176 this._writeLine(aIndent + "<DD>" + escapeHtmlEntities(descriptionAnno.value));
michael@0 1177 }
michael@0 1178 };

mercurial