toolkit/components/places/BookmarkHTMLUtils.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:8a1d46c7e21c
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/. */
4
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 */
58
59 this.EXPORTED_SYMBOLS = [ "BookmarkHTMLUtils" ];
60
61 const Ci = Components.interfaces;
62 const Cc = Components.classes;
63 const Cu = Components.utils;
64
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");
73
74 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
75 "resource://gre/modules/PlacesBackups.jsm");
76 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
77 "resource://gre/modules/Deprecated.jsm");
78
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;
84
85 const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
86 const DESCRIPTION_ANNO = "bookmarkProperties/description";
87
88 const MICROSEC_PER_SEC = 1000000;
89
90 const EXPORT_INDENT = " "; // four spaces
91
92 // Counter used to build fake favicon urls.
93 let serialNumber = 0;
94
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 }
103
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 }
115
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 }
123
124 function notifyObservers(aTopic, aInitialImport) {
125 Services.obs.notifyObservers(null, aTopic, aInitialImport ? "html-initial"
126 : "html");
127 }
128
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 }
135
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);
156
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 },
165
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 }
186
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");
192
193 let importer = new BookmarkImporter(aInitialImport);
194 yield importer.importFromURL(OS.Path.toFileURI(aFilePath));
195
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 },
204
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();
226
227 // Report the time taken to convert the tree to HTML.
228 let exporter = new BookmarkExporter(bookmarks);
229 yield exporter.exportToFile(aFilePath);
230
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 }
238
239 return count;
240 });
241 },
242
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 });
250
251 function Frame(aFrameId) {
252 this.containerId = aFrameId;
253
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;
264
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;
271
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 = "";
278
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;
295
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
303
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
309
310 /**
311 * Contains the id of an imported, or newly created bookmark.
312 */
313 this.previousId = 0;
314
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 }
322
323 function BookmarkImporter(aInitialImport) {
324 this._isImportDefaults = aInitialImport;
325 this._frames = new Array();
326 this._frames.push(new Frame(PlacesUtils.bookmarksMenuFolderId));
327 }
328
329 BookmarkImporter.prototype = {
330
331 _safeTrim: function safeTrim(aStr) {
332 return aStr ? aStr.trim() : aStr;
333 },
334
335 get _curFrame() {
336 return this._frames[this._frames.length - 1];
337 },
338
339 get _previousFrame() {
340 return this._frames[this._frames.length - 2];
341 },
342
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;
353
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 }
378
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 }
393
394 frame.previousId = containerId;
395
396 this._frames.push(new Frame(containerId));
397 },
398
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 },
415
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 },
429
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;
439
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;
444
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 }
465
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 },
499
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;
507
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 = "";
514
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"));
528
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 }
534
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 }
555
556 // Save bookmark's last modified date.
557 if (lastModified) {
558 frame.previousLastModifiedDate =
559 this._convertImportedDateToInternalDate(lastModified);
560 }
561
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 }
567
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 }
578
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 }
587
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 }
602
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 }
617
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 }
629
630 // Import last charset.
631 if (lastCharset) {
632 PlacesUtils.setCharsetForURI(frame.previousLink, lastCharset);
633 }
634 },
635
636 _handleContainerBegin: function handleContainerBegin() {
637 this._curFrame.containerNesting++;
638 },
639
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 },
660
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 },
669
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();
676
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 }
695
696
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 }
707
708 frame.previousText = "";
709
710 },
711
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 },
743
744 _closeContainer: function closeContainer(aElt) {
745 let frame = this._curFrame;
746
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) {
754
755 let itemId = !frame.previousLink ? frame.containerId
756 : frame.previousId;
757
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 = "";
769
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.
780
781 let lastModified;
782 if (!frame.previousLink) {
783 lastModified = this._previousFrame.previousLastModifiedDate;
784 } else {
785 lastModified = frame.previousLastModifiedDate;
786 }
787
788 if (itemId > 0 && lastModified > 0) {
789 PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified);
790 }
791 }
792 frame.inDescription = false;
793 }
794
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 },
823
824 _appendText: function appendText(str) {
825 this._curFrame.previousText += str;
826 },
827
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 }
851
852 // some bookmarks have placeholder URIs that contain just "data:"
853 // ignore these
854 if (aData.length <= 5) {
855 return;
856 }
857
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 }
872
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 },
879
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 },
890
891 runBatched: function runBatched(aDoc) {
892 if (!aDoc) {
893 return;
894 }
895
896 if (this._isImportDefaults) {
897 PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarksMenuFolderId);
898 PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.toolbarFolderId);
899 PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
900 }
901
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 },
932
933 _walkTreeForImport: function walkTreeForImport(aDoc) {
934 PlacesUtils.bookmarks.runInBatchMode(this, aDoc);
935 },
936
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 },
963
964 };
965
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 }
973
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");
977
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 }
987
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;
1011 }
1012 } finally {
1013 bufferedOut.close();
1014 }
1015 } finally {
1016 out.close();
1017 }
1018 }.bind(this));
1019 },
1020
1021 _converterOut: null,
1022
1023 _write: function (aText) {
1024 this._converterOut.writeString(aText || "");
1025 },
1026
1027 _writeAttribute: function (aName, aValue) {
1028 this._write(' ' + aName + '="' + aValue + '"');
1029 },
1030
1031 _writeLine: function (aText) {
1032 this._write(aText + "\n");
1033 },
1034
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 },
1044
1045 _writeContainer: function (aItem, aIndent = "") {
1046 if (aItem == this._root) {
1047 this._writeLine("<H1>" + escapeHtmlEntities(this._root.title) + "</H1>");
1048 this._writeLine("");
1049 }
1050 else {
1051 this._write(aIndent + "<DT><H3");
1052 this._writeDateAttributes(aItem);
1053
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>");
1059 }
1060
1061 this._writeDescription(aItem, aIndent);
1062
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 },
1071
1072 _writeContainerContents: function (aItem, aIndent) {
1073 let localIndent = aIndent + EXPORT_INDENT;
1074
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);
1084 }
1085 },
1086
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 },
1094
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 },
1105
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;
1117 }
1118
1119 this._write(aIndent + "<DT><A");
1120 this._writeAttribute("HREF", escapeUrl(aItem.uri));
1121 this._writeDateAttributes(aItem);
1122 yield this._writeFaviconAttribute(aItem);
1123
1124 let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(aItem.id);
1125 if (aItem.keyword)
1126 this._writeAttribute("SHORTCUTURL", escapeHtmlEntities(keyword));
1127
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));
1132
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 },
1142
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 },
1151
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;
1161 }
1162
1163 this._writeAttribute("ICON_URI", escapeUrl(favicon.uri.spec));
1164
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);
1169 }
1170 },
1171
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));
1177 }
1178 };

mercurial