toolkit/components/places/BookmarkHTMLUtils.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial