toolkit/components/places/PlacesBackups.jsm

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

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

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

     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/. */
     7 this.EXPORTED_SYMBOLS = ["PlacesBackups"];
     9 const Ci = Components.interfaces;
    10 const Cu = Components.utils;
    11 const Cc = Components.classes;
    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");
    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");
    29 XPCOMUtils.defineLazyGetter(this, "localFileCtor",
    30   () => Components.Constructor("@mozilla.org/file/local;1",
    31                                "nsILocalFile", "initWithPath"));
    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 );
    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 }
    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 }
    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);
    67   return sourceMatches && targetMatches &&
    68          sourceMatches[1] == targetMatches[1] &&
    69          sourceMatches[4] == targetMatches[4];
    70 }
    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 }
    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,
    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   },
   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   },
   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   },
   139   get profileRelativeFolderPath() "bookmarkbackups",
   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   },
   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   },
   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;
   190       this._backupFiles = [];
   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         }
   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();
   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       });
   220       return this._backupFiles;
   221     }.bind(this));
   222   },
   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   },
   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   },
   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");
   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   },
   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   },
   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);
   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           }
   362           yield OS.File.copy(aFilePath, newFilePath);
   363         }
   364       }
   366       return nodeCount;
   367     }.bind(this));
   368   },
   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);
   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       }
   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;
   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       }
   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));
   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       }
   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);
   458       // Limit the number of backups.
   459       yield limitBackups(aMaxBackups);
   460     }.bind(this));
   461   },
   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   },
   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       }
   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           }
   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       }
   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       }
   592       for (let id of PlacesUtils.annotations.getItemsWithAnnotation(
   593                        PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO)) {
   594         removeFromMap(id);
   595       }
   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       }
   607       return [itemsMap.get(PlacesUtils.placesRootId), itemsMap.size];
   608     });
   609   }
   610 }
   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") });
   626   let type = aRow.getResultByName("type");
   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   }
   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;
   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