Sat, 03 Jan 2015 20:18:00 +0100
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("&", "&", "g") |
michael@0 | 110 | .replace("<", "<", "g") |
michael@0 | 111 | .replace(">", ">", "g") |
michael@0 | 112 | .replace("\"", """, "g") |
michael@0 | 113 | .replace("'", "'", "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 | }; |