toolkit/components/places/PlacesBackups.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:876c99a83669
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: sw=2 ts=2 sts=2 expandtab filetype=javascript
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 this.EXPORTED_SYMBOLS = ["PlacesBackups"];
8
9 const Ci = Components.interfaces;
10 const Cu = Components.utils;
11 const Cc = Components.classes;
12
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
14 Cu.import("resource://gre/modules/Services.jsm");
15 Cu.import("resource://gre/modules/PlacesUtils.jsm");
16 Cu.import("resource://gre/modules/Task.jsm");
17 Cu.import("resource://gre/modules/osfile.jsm");
18 Cu.import("resource://gre/modules/NetUtil.jsm");
19
20 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
21 "resource://gre/modules/BookmarkJSONUtils.jsm");
22 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
23 "resource://gre/modules/Deprecated.jsm");
24 XPCOMUtils.defineLazyModuleGetter(this, "OS",
25 "resource://gre/modules/osfile.jsm");
26 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
27 "resource://gre/modules/Sqlite.jsm");
28
29 XPCOMUtils.defineLazyGetter(this, "localFileCtor",
30 () => Components.Constructor("@mozilla.org/file/local;1",
31 "nsILocalFile", "initWithPath"));
32
33 XPCOMUtils.defineLazyGetter(this, "filenamesRegex",
34 () => new RegExp("^bookmarks-([0-9\-]+)(?:_([0-9]+)){0,1}(?:_([a-z0-9=\+\-]{24})){0,1}\.(json|html)", "i")
35 );
36
37 /**
38 * Appends meta-data information to a given filename.
39 */
40 function appendMetaDataToFilename(aFilename, aMetaData) {
41 let matches = aFilename.match(filenamesRegex);
42 return "bookmarks-" + matches[1] +
43 "_" + aMetaData.count +
44 "_" + aMetaData.hash +
45 "." + matches[4];
46 }
47
48 /**
49 * Gets the hash from a backup filename.
50 *
51 * @return the extracted hash or null.
52 */
53 function getHashFromFilename(aFilename) {
54 let matches = aFilename.match(filenamesRegex);
55 if (matches && matches[3])
56 return matches[3];
57 return null;
58 }
59
60 /**
61 * Given two filenames, checks if they contain the same date.
62 */
63 function isFilenameWithSameDate(aSourceName, aTargetName) {
64 let sourceMatches = aSourceName.match(filenamesRegex);
65 let targetMatches = aTargetName.match(filenamesRegex);
66
67 return sourceMatches && targetMatches &&
68 sourceMatches[1] == targetMatches[1] &&
69 sourceMatches[4] == targetMatches[4];
70 }
71
72 /**
73 * Given a filename, searches for another backup with the same date.
74 *
75 * @return OS.File path string or null.
76 */
77 function getBackupFileForSameDate(aFilename) {
78 return Task.spawn(function* () {
79 let backupFiles = yield PlacesBackups.getBackupFiles();
80 for (let backupFile of backupFiles) {
81 if (isFilenameWithSameDate(OS.Path.basename(backupFile), aFilename))
82 return backupFile;
83 }
84 return null;
85 });
86 }
87
88 this.PlacesBackups = {
89 /**
90 * Matches the backup filename:
91 * 0: file name
92 * 1: date in form Y-m-d
93 * 2: bookmarks count
94 * 3: contents hash
95 * 4: file extension
96 */
97 get filenamesRegex() filenamesRegex,
98
99 get folder() {
100 Deprecated.warning(
101 "PlacesBackups.folder is deprecated and will be removed in a future version",
102 "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
103 return this._folder;
104 },
105
106 /**
107 * This exists just to avoid spamming deprecate warnings from internal calls
108 * needed to support deprecated methods themselves.
109 */
110 get _folder() {
111 let bookmarksBackupDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
112 bookmarksBackupDir.append(this.profileRelativeFolderPath);
113 if (!bookmarksBackupDir.exists()) {
114 bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0700", 8));
115 if (!bookmarksBackupDir.exists())
116 throw("Unable to create bookmarks backup folder");
117 }
118 delete this._folder;
119 return this._folder = bookmarksBackupDir;
120 },
121
122 /**
123 * Gets backup folder asynchronously.
124 * @return {Promise}
125 * @resolve the folder (the folder string path).
126 */
127 getBackupFolder: function PB_getBackupFolder() {
128 return Task.spawn(function* () {
129 if (this._backupFolder) {
130 return this._backupFolder;
131 }
132 let profileDir = OS.Constants.Path.profileDir;
133 let backupsDirPath = OS.Path.join(profileDir, this.profileRelativeFolderPath);
134 yield OS.File.makeDir(backupsDirPath, { ignoreExisting: true });
135 return this._backupFolder = backupsDirPath;
136 }.bind(this));
137 },
138
139 get profileRelativeFolderPath() "bookmarkbackups",
140
141 /**
142 * Cache current backups in a sorted (by date DESC) array.
143 */
144 get entries() {
145 Deprecated.warning(
146 "PlacesBackups.entries is deprecated and will be removed in a future version",
147 "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
148 return this._entries;
149 },
150
151 /**
152 * This exists just to avoid spamming deprecate warnings from internal calls
153 * needed to support deprecated methods themselves.
154 */
155 get _entries() {
156 delete this._entries;
157 this._entries = [];
158 let files = this._folder.directoryEntries;
159 while (files.hasMoreElements()) {
160 let entry = files.getNext().QueryInterface(Ci.nsIFile);
161 // A valid backup is any file that matches either the localized or
162 // not-localized filename (bug 445704).
163 if (!entry.isHidden() && filenamesRegex.test(entry.leafName)) {
164 // Remove bogus backups in future dates.
165 if (this.getDateForFile(entry) > new Date()) {
166 entry.remove(false);
167 continue;
168 }
169 this._entries.push(entry);
170 }
171 }
172 this._entries.sort((a, b) => {
173 let aDate = this.getDateForFile(a);
174 let bDate = this.getDateForFile(b);
175 return aDate < bDate ? 1 : aDate > bDate ? -1 : 0;
176 });
177 return this._entries;
178 },
179
180 /**
181 * Cache current backups in a sorted (by date DESC) array.
182 * @return {Promise}
183 * @resolve a sorted array of string paths.
184 */
185 getBackupFiles: function PB_getBackupFiles() {
186 return Task.spawn(function* () {
187 if (this._backupFiles)
188 return this._backupFiles;
189
190 this._backupFiles = [];
191
192 let backupFolderPath = yield this.getBackupFolder();
193 let iterator = new OS.File.DirectoryIterator(backupFolderPath);
194 yield iterator.forEach(function(aEntry) {
195 // Since this is a lazy getter and OS.File I/O is serialized, we can
196 // safely remove .tmp files without risking to remove ongoing backups.
197 if (aEntry.name.endsWith(".tmp")) {
198 OS.File.remove(aEntry.path);
199 return;
200 }
201
202 if (filenamesRegex.test(aEntry.name)) {
203 // Remove bogus backups in future dates.
204 let filePath = aEntry.path;
205 if (this.getDateForFile(filePath) > new Date()) {
206 return OS.File.remove(filePath);
207 } else {
208 this._backupFiles.push(filePath);
209 }
210 }
211 }.bind(this));
212 iterator.close();
213
214 this._backupFiles.sort((a, b) => {
215 let aDate = this.getDateForFile(a);
216 let bDate = this.getDateForFile(b);
217 return aDate < bDate ? 1 : aDate > bDate ? -1 : 0;
218 });
219
220 return this._backupFiles;
221 }.bind(this));
222 },
223
224 /**
225 * Creates a filename for bookmarks backup files.
226 *
227 * @param [optional] aDateObj
228 * Date object used to build the filename.
229 * Will use current date if empty.
230 * @return A bookmarks backup filename.
231 */
232 getFilenameForDate: function PB_getFilenameForDate(aDateObj) {
233 let dateObj = aDateObj || new Date();
234 // Use YYYY-MM-DD (ISO 8601) as it doesn't contain illegal characters
235 // and makes the alphabetical order of multiple backup files more useful.
236 return "bookmarks-" + dateObj.toLocaleFormat("%Y-%m-%d") + ".json";
237 },
238
239 /**
240 * Creates a Date object from a backup file. The date is the backup
241 * creation date.
242 *
243 * @param aBackupFile
244 * nsIFile or string path of the backup.
245 * @return A Date object for the backup's creation time.
246 */
247 getDateForFile: function PB_getDateForFile(aBackupFile) {
248 let filename = (aBackupFile instanceof Ci.nsIFile) ? aBackupFile.leafName
249 : OS.Path.basename(aBackupFile);
250 let matches = filename.match(filenamesRegex);
251 if (!matches)
252 throw("Invalid backup file name: " + filename);
253 return new Date(matches[1].replace(/-/g, "/"));
254 },
255
256 /**
257 * Get the most recent backup file.
258 *
259 * @param [optional] aFileExt
260 * Force file extension. Either "html" or "json".
261 * Will check for both if not defined.
262 * @returns nsIFile backup file
263 */
264 getMostRecent: function PB_getMostRecent(aFileExt) {
265 Deprecated.warning(
266 "PlacesBackups.getMostRecent is deprecated and will be removed in a future version",
267 "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
268
269 let fileExt = aFileExt || "(json|html)";
270 for (let i = 0; i < this._entries.length; i++) {
271 let rx = new RegExp("\." + fileExt + "$");
272 if (this._entries[i].leafName.match(rx))
273 return this._entries[i];
274 }
275 return null;
276 },
277
278 /**
279 * Get the most recent backup file.
280 *
281 * @param [optional] aFileExt
282 * Force file extension. Either "html" or "json".
283 * Will check for both if not defined.
284 * @return {Promise}
285 * @result the path to the file.
286 */
287 getMostRecentBackup: function PB_getMostRecentBackup(aFileExt) {
288 return Task.spawn(function* () {
289 let fileExt = aFileExt || "(json|html)";
290 let entries = yield this.getBackupFiles();
291 for (let entry of entries) {
292 let rx = new RegExp("\." + fileExt + "$");
293 if (OS.Path.basename(entry).match(rx)) {
294 return entry;
295 }
296 }
297 return null;
298 }.bind(this));
299 },
300
301 /**
302 * Serializes bookmarks using JSON, and writes to the supplied file.
303 * Note: any item that should not be backed up must be annotated with
304 * "places/excludeFromBackup".
305 *
306 * @param aFilePath
307 * OS.File path for the "bookmarks.json" file to be created.
308 * @return {Promise}
309 * @resolves the number of serialized uri nodes.
310 * @deprecated passing an nsIFile is deprecated
311 */
312 saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFilePath) {
313 if (aFilePath instanceof Ci.nsIFile) {
314 Deprecated.warning("Passing an nsIFile to PlacesBackups.saveBookmarksToJSONFile " +
315 "is deprecated. Please use an OS.File path instead.",
316 "https://developer.mozilla.org/docs/JavaScript_OS.File");
317 aFilePath = aFilePath.path;
318 }
319 return Task.spawn(function* () {
320 let { count: nodeCount, hash: hash } =
321 yield BookmarkJSONUtils.exportToFile(aFilePath);
322
323 let backupFolderPath = yield this.getBackupFolder();
324 if (OS.Path.dirname(aFilePath) == backupFolderPath) {
325 // We are creating a backup in the default backups folder,
326 // so just update the internal cache.
327 this._entries.unshift(new localFileCtor(aFilePath));
328 if (!this._backupFiles) {
329 yield this.getBackupFiles();
330 }
331 this._backupFiles.unshift(aFilePath);
332 } else {
333 // If we are saving to a folder different than our backups folder, then
334 // we also want to copy this new backup to it.
335 // This way we ensure the latest valid backup is the same saved by the
336 // user. See bug 424389.
337 let mostRecentBackupFile = yield this.getMostRecentBackup("json");
338 if (!mostRecentBackupFile ||
339 hash != getHashFromFilename(OS.Path.basename(mostRecentBackupFile))) {
340 let name = this.getFilenameForDate();
341 let newFilename = appendMetaDataToFilename(name,
342 { count: nodeCount,
343 hash: hash });
344 let newFilePath = OS.Path.join(backupFolderPath, newFilename);
345 let backupFile = yield getBackupFileForSameDate(name);
346 if (backupFile) {
347 // There is already a backup for today, replace it.
348 yield OS.File.remove(backupFile, { ignoreAbsent: true });
349 if (!this._backupFiles)
350 yield this.getBackupFiles();
351 else
352 this._backupFiles.shift();
353 this._backupFiles.unshift(newFilePath);
354 } else {
355 // There is no backup for today, add the new one.
356 this._entries.unshift(new localFileCtor(newFilePath));
357 if (!this._backupFiles)
358 yield this.getBackupFiles();
359 this._backupFiles.unshift(newFilePath);
360 }
361
362 yield OS.File.copy(aFilePath, newFilePath);
363 }
364 }
365
366 return nodeCount;
367 }.bind(this));
368 },
369
370 /**
371 * Creates a dated backup in <profile>/bookmarkbackups.
372 * Stores the bookmarks using JSON.
373 * Note: any item that should not be backed up must be annotated with
374 * "places/excludeFromBackup".
375 *
376 * @param [optional] int aMaxBackups
377 * The maximum number of backups to keep. If set to 0
378 * all existing backups are removed and aForceBackup is
379 * ignored, so a new one won't be created.
380 * @param [optional] bool aForceBackup
381 * Forces creating a backup even if one was already
382 * created that day (overwrites).
383 * @return {Promise}
384 */
385 create: function PB_create(aMaxBackups, aForceBackup) {
386 let limitBackups = function* () {
387 let backupFiles = yield this.getBackupFiles();
388 if (typeof aMaxBackups == "number" && aMaxBackups > -1 &&
389 backupFiles.length >= aMaxBackups) {
390 let numberOfBackupsToDelete = backupFiles.length - aMaxBackups;
391 while (numberOfBackupsToDelete--) {
392 this._entries.pop();
393 let oldestBackup = this._backupFiles.pop();
394 yield OS.File.remove(oldestBackup);
395 }
396 }
397 }.bind(this);
398
399 return Task.spawn(function* () {
400 if (aMaxBackups === 0) {
401 // Backups are disabled, delete any existing one and bail out.
402 yield limitBackups(0);
403 return;
404 }
405
406 // Ensure to initialize _backupFiles
407 if (!this._backupFiles)
408 yield this.getBackupFiles();
409 let newBackupFilename = this.getFilenameForDate();
410 // If we already have a backup for today we should do nothing, unless we
411 // were required to enforce a new backup.
412 let backupFile = yield getBackupFileForSameDate(newBackupFilename);
413 if (backupFile && !aForceBackup)
414 return;
415
416 if (backupFile) {
417 // In case there is a backup for today we should recreate it.
418 this._backupFiles.shift();
419 this._entries.shift();
420 yield OS.File.remove(backupFile, { ignoreAbsent: true });
421 }
422
423 // Now check the hash of the most recent backup, and try to create a new
424 // backup, if that fails due to hash conflict, just rename the old backup.
425 let mostRecentBackupFile = yield this.getMostRecentBackup();
426 let mostRecentHash = mostRecentBackupFile &&
427 getHashFromFilename(OS.Path.basename(mostRecentBackupFile));
428
429 // Save bookmarks to a backup file.
430 let backupFolder = yield this.getBackupFolder();
431 let newBackupFile = OS.Path.join(backupFolder, newBackupFilename);
432 let newFilenameWithMetaData;
433 try {
434 let { count: nodeCount, hash: hash } =
435 yield BookmarkJSONUtils.exportToFile(newBackupFile,
436 { failIfHashIs: mostRecentHash });
437 newFilenameWithMetaData = appendMetaDataToFilename(newBackupFilename,
438 { count: nodeCount,
439 hash: hash });
440 } catch (ex if ex.becauseSameHash) {
441 // The last backup already contained up-to-date information, just
442 // rename it as if it was today's backup.
443 this._backupFiles.shift();
444 this._entries.shift();
445 newBackupFile = mostRecentBackupFile;
446 newFilenameWithMetaData = appendMetaDataToFilename(
447 newBackupFilename,
448 { count: this.getBookmarkCountForFile(mostRecentBackupFile),
449 hash: mostRecentHash });
450 }
451
452 // Append metadata to the backup filename.
453 let newBackupFileWithMetadata = OS.Path.join(backupFolder, newFilenameWithMetaData);
454 yield OS.File.move(newBackupFile, newBackupFileWithMetadata);
455 this._entries.unshift(new localFileCtor(newBackupFileWithMetadata));
456 this._backupFiles.unshift(newBackupFileWithMetadata);
457
458 // Limit the number of backups.
459 yield limitBackups(aMaxBackups);
460 }.bind(this));
461 },
462
463 /**
464 * Gets the bookmark count for backup file.
465 *
466 * @param aFilePath
467 * File path The backup file.
468 *
469 * @return the bookmark count or null.
470 */
471 getBookmarkCountForFile: function PB_getBookmarkCountForFile(aFilePath) {
472 let count = null;
473 let filename = OS.Path.basename(aFilePath);
474 let matches = filename.match(filenamesRegex);
475 if (matches && matches[2])
476 count = matches[2];
477 return count;
478 },
479
480 /**
481 * Gets a bookmarks tree representation usable to create backups in different
482 * file formats. The root or the tree is PlacesUtils.placesRootId.
483 * Items annotated with PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO and all of their
484 * descendants are excluded.
485 *
486 * @return an object representing a tree with the places root as its root.
487 * Each bookmark is represented by an object having these properties:
488 * * id: the item id (make this not enumerable after bug 824502)
489 * * title: the title
490 * * guid: unique id
491 * * parent: item id of the parent folder, not enumerable
492 * * index: the position in the parent
493 * * dateAdded: microseconds from the epoch
494 * * lastModified: microseconds from the epoch
495 * * type: type of the originating node as defined in PlacesUtils
496 * The following properties exist only for a subset of bookmarks:
497 * * annos: array of annotations
498 * * uri: url
499 * * iconuri: favicon's url
500 * * keyword: associated keyword
501 * * charset: last known charset
502 * * tags: csv string of tags
503 * * root: string describing whether this represents a root
504 * * children: array of child items in a folder
505 */
506 getBookmarksTree: function () {
507 return Task.spawn(function* () {
508 let dbFilePath = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
509 let conn = yield Sqlite.openConnection({ path: dbFilePath,
510 sharedMemoryCache: false });
511 let rows = [];
512 try {
513 rows = yield conn.execute(
514 "SELECT b.id, h.url, IFNULL(b.title, '') AS title, b.parent, " +
515 "b.position AS [index], b.type, b.dateAdded, b.lastModified, " +
516 "b.guid, f.url AS iconuri, " +
517 "( SELECT GROUP_CONCAT(t.title, ',') " +
518 "FROM moz_bookmarks b2 " +
519 "JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder " +
520 "WHERE b2.fk = h.id " +
521 ") AS tags, " +
522 "EXISTS (SELECT 1 FROM moz_items_annos WHERE item_id = b.id LIMIT 1) AS has_annos, " +
523 "( SELECT a.content FROM moz_annos a " +
524 "JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id " +
525 "WHERE place_id = h.id AND n.name = :charset_anno " +
526 ") AS charset " +
527 "FROM moz_bookmarks b " +
528 "LEFT JOIN moz_bookmarks p ON p.id = b.parent " +
529 "LEFT JOIN moz_places h ON h.id = b.fk " +
530 "LEFT JOIN moz_favicons f ON f.id = h.favicon_id " +
531 "WHERE b.id <> :tags_folder AND b.parent <> :tags_folder AND p.parent <> :tags_folder " +
532 "ORDER BY b.parent, b.position",
533 { tags_folder: PlacesUtils.tagsFolderId,
534 charset_anno: PlacesUtils.CHARSET_ANNO });
535 } catch(e) {
536 Cu.reportError("Unable to query the database " + e);
537 } finally {
538 yield conn.close();
539 }
540
541 let startTime = Date.now();
542 // Create a Map for lookup and recursive building of the tree.
543 let itemsMap = new Map();
544 for (let row of rows) {
545 let id = row.getResultByName("id");
546 try {
547 let bookmark = sqliteRowToBookmarkObject(row);
548 if (itemsMap.has(id)) {
549 // Since children may be added before parents, we should merge with
550 // the existing object.
551 let original = itemsMap.get(id);
552 for (let prop of Object.getOwnPropertyNames(bookmark)) {
553 original[prop] = bookmark[prop];
554 }
555 bookmark = original;
556 }
557 else {
558 itemsMap.set(id, bookmark);
559 }
560
561 // Append bookmark to its parent.
562 if (!itemsMap.has(bookmark.parent))
563 itemsMap.set(bookmark.parent, {});
564 let parent = itemsMap.get(bookmark.parent);
565 if (!("children" in parent))
566 parent.children = [];
567 parent.children.push(bookmark);
568 } catch (e) {
569 Cu.reportError("Error while reading node " + id + " " + e);
570 }
571 }
572
573 // Handle excluded items, by removing entire subtrees pointed by them.
574 function removeFromMap(id) {
575 // Could have been removed by a previous call, since we can't
576 // predict order of items in EXCLUDE_FROM_BACKUP_ANNO.
577 if (itemsMap.has(id)) {
578 let excludedItem = itemsMap.get(id);
579 if (excludedItem.children) {
580 for (let child of excludedItem.children) {
581 removeFromMap(child.id);
582 }
583 }
584 // Remove the excluded item from its parent's children...
585 let parentItem = itemsMap.get(excludedItem.parent);
586 parentItem.children = parentItem.children.filter(aChild => aChild.id != id);
587 // ...then remove it from the map.
588 itemsMap.delete(id);
589 }
590 }
591
592 for (let id of PlacesUtils.annotations.getItemsWithAnnotation(
593 PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO)) {
594 removeFromMap(id);
595 }
596
597 // Report the time taken to build the tree. This doesn't take into
598 // account the time spent in the query since that's off the main-thread.
599 try {
600 Services.telemetry
601 .getHistogramById("PLACES_BACKUPS_BOOKMARKSTREE_MS")
602 .add(Date.now() - startTime);
603 } catch (ex) {
604 Components.utils.reportError("Unable to report telemetry.");
605 }
606
607 return [itemsMap.get(PlacesUtils.placesRootId), itemsMap.size];
608 });
609 }
610 }
611
612 /**
613 * Helper function to convert a Sqlite.jsm row to a bookmark object
614 * representation.
615 *
616 * @param aRow The Sqlite.jsm result row.
617 */
618 function sqliteRowToBookmarkObject(aRow) {
619 let bookmark = {};
620 for (let p of [ "id" ,"guid", "title", "index", "dateAdded", "lastModified" ]) {
621 bookmark[p] = aRow.getResultByName(p);
622 }
623 Object.defineProperty(bookmark, "parent",
624 { value: aRow.getResultByName("parent") });
625
626 let type = aRow.getResultByName("type");
627
628 // Add annotations.
629 if (aRow.getResultByName("has_annos")) {
630 try {
631 bookmark.annos = PlacesUtils.getAnnotationsForItem(bookmark.id);
632 } catch (e) {
633 Cu.reportError("Unexpected error while reading annotations " + e);
634 }
635 }
636
637 switch (type) {
638 case Ci.nsINavBookmarksService.TYPE_BOOKMARK:
639 // TODO: What about shortcuts?
640 bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE;
641 // This will throw if we try to serialize an invalid url and the node will
642 // just be skipped.
643 bookmark.uri = NetUtil.newURI(aRow.getResultByName("url")).spec;
644 // Keywords are cached, so this should be decently fast.
645 let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(bookmark.id);
646 if (keyword)
647 bookmark.keyword = keyword;
648 let charset = aRow.getResultByName("charset");
649 if (charset)
650 bookmark.charset = charset;
651 let tags = aRow.getResultByName("tags");
652 if (tags)
653 bookmark.tags = tags;
654 let iconuri = aRow.getResultByName("iconuri");
655 if (iconuri)
656 bookmark.iconuri = iconuri;
657 break;
658 case Ci.nsINavBookmarksService.TYPE_FOLDER:
659 bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
660
661 // Mark root folders.
662 if (bookmark.id == PlacesUtils.placesRootId)
663 bookmark.root = "placesRoot";
664 else if (bookmark.id == PlacesUtils.bookmarksMenuFolderId)
665 bookmark.root = "bookmarksMenuFolder";
666 else if (bookmark.id == PlacesUtils.unfiledBookmarksFolderId)
667 bookmark.root = "unfiledBookmarksFolder";
668 else if (bookmark.id == PlacesUtils.toolbarFolderId)
669 bookmark.root = "toolbarFolder";
670 break;
671 case Ci.nsINavBookmarksService.TYPE_SEPARATOR:
672 bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
673 break;
674 default:
675 Cu.reportError("Unexpected bookmark type");
676 break;
677 }
678 return bookmark;
679 }

mercurial