1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/places/PlacesBackups.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,679 @@ 1.4 +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 1.5 + * vim: sw=2 ts=2 sts=2 expandtab filetype=javascript 1.6 + * This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["PlacesBackups"]; 1.11 + 1.12 +const Ci = Components.interfaces; 1.13 +const Cu = Components.utils; 1.14 +const Cc = Components.classes; 1.15 + 1.16 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.17 +Cu.import("resource://gre/modules/Services.jsm"); 1.18 +Cu.import("resource://gre/modules/PlacesUtils.jsm"); 1.19 +Cu.import("resource://gre/modules/Task.jsm"); 1.20 +Cu.import("resource://gre/modules/osfile.jsm"); 1.21 +Cu.import("resource://gre/modules/NetUtil.jsm"); 1.22 + 1.23 +XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils", 1.24 + "resource://gre/modules/BookmarkJSONUtils.jsm"); 1.25 +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", 1.26 + "resource://gre/modules/Deprecated.jsm"); 1.27 +XPCOMUtils.defineLazyModuleGetter(this, "OS", 1.28 + "resource://gre/modules/osfile.jsm"); 1.29 +XPCOMUtils.defineLazyModuleGetter(this, "Sqlite", 1.30 + "resource://gre/modules/Sqlite.jsm"); 1.31 + 1.32 +XPCOMUtils.defineLazyGetter(this, "localFileCtor", 1.33 + () => Components.Constructor("@mozilla.org/file/local;1", 1.34 + "nsILocalFile", "initWithPath")); 1.35 + 1.36 +XPCOMUtils.defineLazyGetter(this, "filenamesRegex", 1.37 + () => new RegExp("^bookmarks-([0-9\-]+)(?:_([0-9]+)){0,1}(?:_([a-z0-9=\+\-]{24})){0,1}\.(json|html)", "i") 1.38 +); 1.39 + 1.40 +/** 1.41 + * Appends meta-data information to a given filename. 1.42 + */ 1.43 +function appendMetaDataToFilename(aFilename, aMetaData) { 1.44 + let matches = aFilename.match(filenamesRegex); 1.45 + return "bookmarks-" + matches[1] + 1.46 + "_" + aMetaData.count + 1.47 + "_" + aMetaData.hash + 1.48 + "." + matches[4]; 1.49 +} 1.50 + 1.51 +/** 1.52 + * Gets the hash from a backup filename. 1.53 + * 1.54 + * @return the extracted hash or null. 1.55 + */ 1.56 +function getHashFromFilename(aFilename) { 1.57 + let matches = aFilename.match(filenamesRegex); 1.58 + if (matches && matches[3]) 1.59 + return matches[3]; 1.60 + return null; 1.61 +} 1.62 + 1.63 +/** 1.64 + * Given two filenames, checks if they contain the same date. 1.65 + */ 1.66 +function isFilenameWithSameDate(aSourceName, aTargetName) { 1.67 + let sourceMatches = aSourceName.match(filenamesRegex); 1.68 + let targetMatches = aTargetName.match(filenamesRegex); 1.69 + 1.70 + return sourceMatches && targetMatches && 1.71 + sourceMatches[1] == targetMatches[1] && 1.72 + sourceMatches[4] == targetMatches[4]; 1.73 +} 1.74 + 1.75 +/** 1.76 + * Given a filename, searches for another backup with the same date. 1.77 + * 1.78 + * @return OS.File path string or null. 1.79 + */ 1.80 +function getBackupFileForSameDate(aFilename) { 1.81 + return Task.spawn(function* () { 1.82 + let backupFiles = yield PlacesBackups.getBackupFiles(); 1.83 + for (let backupFile of backupFiles) { 1.84 + if (isFilenameWithSameDate(OS.Path.basename(backupFile), aFilename)) 1.85 + return backupFile; 1.86 + } 1.87 + return null; 1.88 + }); 1.89 +} 1.90 + 1.91 +this.PlacesBackups = { 1.92 + /** 1.93 + * Matches the backup filename: 1.94 + * 0: file name 1.95 + * 1: date in form Y-m-d 1.96 + * 2: bookmarks count 1.97 + * 3: contents hash 1.98 + * 4: file extension 1.99 + */ 1.100 + get filenamesRegex() filenamesRegex, 1.101 + 1.102 + get folder() { 1.103 + Deprecated.warning( 1.104 + "PlacesBackups.folder is deprecated and will be removed in a future version", 1.105 + "https://bugzilla.mozilla.org/show_bug.cgi?id=859695"); 1.106 + return this._folder; 1.107 + }, 1.108 + 1.109 + /** 1.110 + * This exists just to avoid spamming deprecate warnings from internal calls 1.111 + * needed to support deprecated methods themselves. 1.112 + */ 1.113 + get _folder() { 1.114 + let bookmarksBackupDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile); 1.115 + bookmarksBackupDir.append(this.profileRelativeFolderPath); 1.116 + if (!bookmarksBackupDir.exists()) { 1.117 + bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0700", 8)); 1.118 + if (!bookmarksBackupDir.exists()) 1.119 + throw("Unable to create bookmarks backup folder"); 1.120 + } 1.121 + delete this._folder; 1.122 + return this._folder = bookmarksBackupDir; 1.123 + }, 1.124 + 1.125 + /** 1.126 + * Gets backup folder asynchronously. 1.127 + * @return {Promise} 1.128 + * @resolve the folder (the folder string path). 1.129 + */ 1.130 + getBackupFolder: function PB_getBackupFolder() { 1.131 + return Task.spawn(function* () { 1.132 + if (this._backupFolder) { 1.133 + return this._backupFolder; 1.134 + } 1.135 + let profileDir = OS.Constants.Path.profileDir; 1.136 + let backupsDirPath = OS.Path.join(profileDir, this.profileRelativeFolderPath); 1.137 + yield OS.File.makeDir(backupsDirPath, { ignoreExisting: true }); 1.138 + return this._backupFolder = backupsDirPath; 1.139 + }.bind(this)); 1.140 + }, 1.141 + 1.142 + get profileRelativeFolderPath() "bookmarkbackups", 1.143 + 1.144 + /** 1.145 + * Cache current backups in a sorted (by date DESC) array. 1.146 + */ 1.147 + get entries() { 1.148 + Deprecated.warning( 1.149 + "PlacesBackups.entries is deprecated and will be removed in a future version", 1.150 + "https://bugzilla.mozilla.org/show_bug.cgi?id=859695"); 1.151 + return this._entries; 1.152 + }, 1.153 + 1.154 + /** 1.155 + * This exists just to avoid spamming deprecate warnings from internal calls 1.156 + * needed to support deprecated methods themselves. 1.157 + */ 1.158 + get _entries() { 1.159 + delete this._entries; 1.160 + this._entries = []; 1.161 + let files = this._folder.directoryEntries; 1.162 + while (files.hasMoreElements()) { 1.163 + let entry = files.getNext().QueryInterface(Ci.nsIFile); 1.164 + // A valid backup is any file that matches either the localized or 1.165 + // not-localized filename (bug 445704). 1.166 + if (!entry.isHidden() && filenamesRegex.test(entry.leafName)) { 1.167 + // Remove bogus backups in future dates. 1.168 + if (this.getDateForFile(entry) > new Date()) { 1.169 + entry.remove(false); 1.170 + continue; 1.171 + } 1.172 + this._entries.push(entry); 1.173 + } 1.174 + } 1.175 + this._entries.sort((a, b) => { 1.176 + let aDate = this.getDateForFile(a); 1.177 + let bDate = this.getDateForFile(b); 1.178 + return aDate < bDate ? 1 : aDate > bDate ? -1 : 0; 1.179 + }); 1.180 + return this._entries; 1.181 + }, 1.182 + 1.183 + /** 1.184 + * Cache current backups in a sorted (by date DESC) array. 1.185 + * @return {Promise} 1.186 + * @resolve a sorted array of string paths. 1.187 + */ 1.188 + getBackupFiles: function PB_getBackupFiles() { 1.189 + return Task.spawn(function* () { 1.190 + if (this._backupFiles) 1.191 + return this._backupFiles; 1.192 + 1.193 + this._backupFiles = []; 1.194 + 1.195 + let backupFolderPath = yield this.getBackupFolder(); 1.196 + let iterator = new OS.File.DirectoryIterator(backupFolderPath); 1.197 + yield iterator.forEach(function(aEntry) { 1.198 + // Since this is a lazy getter and OS.File I/O is serialized, we can 1.199 + // safely remove .tmp files without risking to remove ongoing backups. 1.200 + if (aEntry.name.endsWith(".tmp")) { 1.201 + OS.File.remove(aEntry.path); 1.202 + return; 1.203 + } 1.204 + 1.205 + if (filenamesRegex.test(aEntry.name)) { 1.206 + // Remove bogus backups in future dates. 1.207 + let filePath = aEntry.path; 1.208 + if (this.getDateForFile(filePath) > new Date()) { 1.209 + return OS.File.remove(filePath); 1.210 + } else { 1.211 + this._backupFiles.push(filePath); 1.212 + } 1.213 + } 1.214 + }.bind(this)); 1.215 + iterator.close(); 1.216 + 1.217 + this._backupFiles.sort((a, b) => { 1.218 + let aDate = this.getDateForFile(a); 1.219 + let bDate = this.getDateForFile(b); 1.220 + return aDate < bDate ? 1 : aDate > bDate ? -1 : 0; 1.221 + }); 1.222 + 1.223 + return this._backupFiles; 1.224 + }.bind(this)); 1.225 + }, 1.226 + 1.227 + /** 1.228 + * Creates a filename for bookmarks backup files. 1.229 + * 1.230 + * @param [optional] aDateObj 1.231 + * Date object used to build the filename. 1.232 + * Will use current date if empty. 1.233 + * @return A bookmarks backup filename. 1.234 + */ 1.235 + getFilenameForDate: function PB_getFilenameForDate(aDateObj) { 1.236 + let dateObj = aDateObj || new Date(); 1.237 + // Use YYYY-MM-DD (ISO 8601) as it doesn't contain illegal characters 1.238 + // and makes the alphabetical order of multiple backup files more useful. 1.239 + return "bookmarks-" + dateObj.toLocaleFormat("%Y-%m-%d") + ".json"; 1.240 + }, 1.241 + 1.242 + /** 1.243 + * Creates a Date object from a backup file. The date is the backup 1.244 + * creation date. 1.245 + * 1.246 + * @param aBackupFile 1.247 + * nsIFile or string path of the backup. 1.248 + * @return A Date object for the backup's creation time. 1.249 + */ 1.250 + getDateForFile: function PB_getDateForFile(aBackupFile) { 1.251 + let filename = (aBackupFile instanceof Ci.nsIFile) ? aBackupFile.leafName 1.252 + : OS.Path.basename(aBackupFile); 1.253 + let matches = filename.match(filenamesRegex); 1.254 + if (!matches) 1.255 + throw("Invalid backup file name: " + filename); 1.256 + return new Date(matches[1].replace(/-/g, "/")); 1.257 + }, 1.258 + 1.259 + /** 1.260 + * Get the most recent backup file. 1.261 + * 1.262 + * @param [optional] aFileExt 1.263 + * Force file extension. Either "html" or "json". 1.264 + * Will check for both if not defined. 1.265 + * @returns nsIFile backup file 1.266 + */ 1.267 + getMostRecent: function PB_getMostRecent(aFileExt) { 1.268 + Deprecated.warning( 1.269 + "PlacesBackups.getMostRecent is deprecated and will be removed in a future version", 1.270 + "https://bugzilla.mozilla.org/show_bug.cgi?id=859695"); 1.271 + 1.272 + let fileExt = aFileExt || "(json|html)"; 1.273 + for (let i = 0; i < this._entries.length; i++) { 1.274 + let rx = new RegExp("\." + fileExt + "$"); 1.275 + if (this._entries[i].leafName.match(rx)) 1.276 + return this._entries[i]; 1.277 + } 1.278 + return null; 1.279 + }, 1.280 + 1.281 + /** 1.282 + * Get the most recent backup file. 1.283 + * 1.284 + * @param [optional] aFileExt 1.285 + * Force file extension. Either "html" or "json". 1.286 + * Will check for both if not defined. 1.287 + * @return {Promise} 1.288 + * @result the path to the file. 1.289 + */ 1.290 + getMostRecentBackup: function PB_getMostRecentBackup(aFileExt) { 1.291 + return Task.spawn(function* () { 1.292 + let fileExt = aFileExt || "(json|html)"; 1.293 + let entries = yield this.getBackupFiles(); 1.294 + for (let entry of entries) { 1.295 + let rx = new RegExp("\." + fileExt + "$"); 1.296 + if (OS.Path.basename(entry).match(rx)) { 1.297 + return entry; 1.298 + } 1.299 + } 1.300 + return null; 1.301 + }.bind(this)); 1.302 + }, 1.303 + 1.304 + /** 1.305 + * Serializes bookmarks using JSON, and writes to the supplied file. 1.306 + * Note: any item that should not be backed up must be annotated with 1.307 + * "places/excludeFromBackup". 1.308 + * 1.309 + * @param aFilePath 1.310 + * OS.File path for the "bookmarks.json" file to be created. 1.311 + * @return {Promise} 1.312 + * @resolves the number of serialized uri nodes. 1.313 + * @deprecated passing an nsIFile is deprecated 1.314 + */ 1.315 + saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFilePath) { 1.316 + if (aFilePath instanceof Ci.nsIFile) { 1.317 + Deprecated.warning("Passing an nsIFile to PlacesBackups.saveBookmarksToJSONFile " + 1.318 + "is deprecated. Please use an OS.File path instead.", 1.319 + "https://developer.mozilla.org/docs/JavaScript_OS.File"); 1.320 + aFilePath = aFilePath.path; 1.321 + } 1.322 + return Task.spawn(function* () { 1.323 + let { count: nodeCount, hash: hash } = 1.324 + yield BookmarkJSONUtils.exportToFile(aFilePath); 1.325 + 1.326 + let backupFolderPath = yield this.getBackupFolder(); 1.327 + if (OS.Path.dirname(aFilePath) == backupFolderPath) { 1.328 + // We are creating a backup in the default backups folder, 1.329 + // so just update the internal cache. 1.330 + this._entries.unshift(new localFileCtor(aFilePath)); 1.331 + if (!this._backupFiles) { 1.332 + yield this.getBackupFiles(); 1.333 + } 1.334 + this._backupFiles.unshift(aFilePath); 1.335 + } else { 1.336 + // If we are saving to a folder different than our backups folder, then 1.337 + // we also want to copy this new backup to it. 1.338 + // This way we ensure the latest valid backup is the same saved by the 1.339 + // user. See bug 424389. 1.340 + let mostRecentBackupFile = yield this.getMostRecentBackup("json"); 1.341 + if (!mostRecentBackupFile || 1.342 + hash != getHashFromFilename(OS.Path.basename(mostRecentBackupFile))) { 1.343 + let name = this.getFilenameForDate(); 1.344 + let newFilename = appendMetaDataToFilename(name, 1.345 + { count: nodeCount, 1.346 + hash: hash }); 1.347 + let newFilePath = OS.Path.join(backupFolderPath, newFilename); 1.348 + let backupFile = yield getBackupFileForSameDate(name); 1.349 + if (backupFile) { 1.350 + // There is already a backup for today, replace it. 1.351 + yield OS.File.remove(backupFile, { ignoreAbsent: true }); 1.352 + if (!this._backupFiles) 1.353 + yield this.getBackupFiles(); 1.354 + else 1.355 + this._backupFiles.shift(); 1.356 + this._backupFiles.unshift(newFilePath); 1.357 + } else { 1.358 + // There is no backup for today, add the new one. 1.359 + this._entries.unshift(new localFileCtor(newFilePath)); 1.360 + if (!this._backupFiles) 1.361 + yield this.getBackupFiles(); 1.362 + this._backupFiles.unshift(newFilePath); 1.363 + } 1.364 + 1.365 + yield OS.File.copy(aFilePath, newFilePath); 1.366 + } 1.367 + } 1.368 + 1.369 + return nodeCount; 1.370 + }.bind(this)); 1.371 + }, 1.372 + 1.373 + /** 1.374 + * Creates a dated backup in <profile>/bookmarkbackups. 1.375 + * Stores the bookmarks using JSON. 1.376 + * Note: any item that should not be backed up must be annotated with 1.377 + * "places/excludeFromBackup". 1.378 + * 1.379 + * @param [optional] int aMaxBackups 1.380 + * The maximum number of backups to keep. If set to 0 1.381 + * all existing backups are removed and aForceBackup is 1.382 + * ignored, so a new one won't be created. 1.383 + * @param [optional] bool aForceBackup 1.384 + * Forces creating a backup even if one was already 1.385 + * created that day (overwrites). 1.386 + * @return {Promise} 1.387 + */ 1.388 + create: function PB_create(aMaxBackups, aForceBackup) { 1.389 + let limitBackups = function* () { 1.390 + let backupFiles = yield this.getBackupFiles(); 1.391 + if (typeof aMaxBackups == "number" && aMaxBackups > -1 && 1.392 + backupFiles.length >= aMaxBackups) { 1.393 + let numberOfBackupsToDelete = backupFiles.length - aMaxBackups; 1.394 + while (numberOfBackupsToDelete--) { 1.395 + this._entries.pop(); 1.396 + let oldestBackup = this._backupFiles.pop(); 1.397 + yield OS.File.remove(oldestBackup); 1.398 + } 1.399 + } 1.400 + }.bind(this); 1.401 + 1.402 + return Task.spawn(function* () { 1.403 + if (aMaxBackups === 0) { 1.404 + // Backups are disabled, delete any existing one and bail out. 1.405 + yield limitBackups(0); 1.406 + return; 1.407 + } 1.408 + 1.409 + // Ensure to initialize _backupFiles 1.410 + if (!this._backupFiles) 1.411 + yield this.getBackupFiles(); 1.412 + let newBackupFilename = this.getFilenameForDate(); 1.413 + // If we already have a backup for today we should do nothing, unless we 1.414 + // were required to enforce a new backup. 1.415 + let backupFile = yield getBackupFileForSameDate(newBackupFilename); 1.416 + if (backupFile && !aForceBackup) 1.417 + return; 1.418 + 1.419 + if (backupFile) { 1.420 + // In case there is a backup for today we should recreate it. 1.421 + this._backupFiles.shift(); 1.422 + this._entries.shift(); 1.423 + yield OS.File.remove(backupFile, { ignoreAbsent: true }); 1.424 + } 1.425 + 1.426 + // Now check the hash of the most recent backup, and try to create a new 1.427 + // backup, if that fails due to hash conflict, just rename the old backup. 1.428 + let mostRecentBackupFile = yield this.getMostRecentBackup(); 1.429 + let mostRecentHash = mostRecentBackupFile && 1.430 + getHashFromFilename(OS.Path.basename(mostRecentBackupFile)); 1.431 + 1.432 + // Save bookmarks to a backup file. 1.433 + let backupFolder = yield this.getBackupFolder(); 1.434 + let newBackupFile = OS.Path.join(backupFolder, newBackupFilename); 1.435 + let newFilenameWithMetaData; 1.436 + try { 1.437 + let { count: nodeCount, hash: hash } = 1.438 + yield BookmarkJSONUtils.exportToFile(newBackupFile, 1.439 + { failIfHashIs: mostRecentHash }); 1.440 + newFilenameWithMetaData = appendMetaDataToFilename(newBackupFilename, 1.441 + { count: nodeCount, 1.442 + hash: hash }); 1.443 + } catch (ex if ex.becauseSameHash) { 1.444 + // The last backup already contained up-to-date information, just 1.445 + // rename it as if it was today's backup. 1.446 + this._backupFiles.shift(); 1.447 + this._entries.shift(); 1.448 + newBackupFile = mostRecentBackupFile; 1.449 + newFilenameWithMetaData = appendMetaDataToFilename( 1.450 + newBackupFilename, 1.451 + { count: this.getBookmarkCountForFile(mostRecentBackupFile), 1.452 + hash: mostRecentHash }); 1.453 + } 1.454 + 1.455 + // Append metadata to the backup filename. 1.456 + let newBackupFileWithMetadata = OS.Path.join(backupFolder, newFilenameWithMetaData); 1.457 + yield OS.File.move(newBackupFile, newBackupFileWithMetadata); 1.458 + this._entries.unshift(new localFileCtor(newBackupFileWithMetadata)); 1.459 + this._backupFiles.unshift(newBackupFileWithMetadata); 1.460 + 1.461 + // Limit the number of backups. 1.462 + yield limitBackups(aMaxBackups); 1.463 + }.bind(this)); 1.464 + }, 1.465 + 1.466 + /** 1.467 + * Gets the bookmark count for backup file. 1.468 + * 1.469 + * @param aFilePath 1.470 + * File path The backup file. 1.471 + * 1.472 + * @return the bookmark count or null. 1.473 + */ 1.474 + getBookmarkCountForFile: function PB_getBookmarkCountForFile(aFilePath) { 1.475 + let count = null; 1.476 + let filename = OS.Path.basename(aFilePath); 1.477 + let matches = filename.match(filenamesRegex); 1.478 + if (matches && matches[2]) 1.479 + count = matches[2]; 1.480 + return count; 1.481 + }, 1.482 + 1.483 + /** 1.484 + * Gets a bookmarks tree representation usable to create backups in different 1.485 + * file formats. The root or the tree is PlacesUtils.placesRootId. 1.486 + * Items annotated with PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO and all of their 1.487 + * descendants are excluded. 1.488 + * 1.489 + * @return an object representing a tree with the places root as its root. 1.490 + * Each bookmark is represented by an object having these properties: 1.491 + * * id: the item id (make this not enumerable after bug 824502) 1.492 + * * title: the title 1.493 + * * guid: unique id 1.494 + * * parent: item id of the parent folder, not enumerable 1.495 + * * index: the position in the parent 1.496 + * * dateAdded: microseconds from the epoch 1.497 + * * lastModified: microseconds from the epoch 1.498 + * * type: type of the originating node as defined in PlacesUtils 1.499 + * The following properties exist only for a subset of bookmarks: 1.500 + * * annos: array of annotations 1.501 + * * uri: url 1.502 + * * iconuri: favicon's url 1.503 + * * keyword: associated keyword 1.504 + * * charset: last known charset 1.505 + * * tags: csv string of tags 1.506 + * * root: string describing whether this represents a root 1.507 + * * children: array of child items in a folder 1.508 + */ 1.509 + getBookmarksTree: function () { 1.510 + return Task.spawn(function* () { 1.511 + let dbFilePath = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite"); 1.512 + let conn = yield Sqlite.openConnection({ path: dbFilePath, 1.513 + sharedMemoryCache: false }); 1.514 + let rows = []; 1.515 + try { 1.516 + rows = yield conn.execute( 1.517 + "SELECT b.id, h.url, IFNULL(b.title, '') AS title, b.parent, " + 1.518 + "b.position AS [index], b.type, b.dateAdded, b.lastModified, " + 1.519 + "b.guid, f.url AS iconuri, " + 1.520 + "( SELECT GROUP_CONCAT(t.title, ',') " + 1.521 + "FROM moz_bookmarks b2 " + 1.522 + "JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder " + 1.523 + "WHERE b2.fk = h.id " + 1.524 + ") AS tags, " + 1.525 + "EXISTS (SELECT 1 FROM moz_items_annos WHERE item_id = b.id LIMIT 1) AS has_annos, " + 1.526 + "( SELECT a.content FROM moz_annos a " + 1.527 + "JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id " + 1.528 + "WHERE place_id = h.id AND n.name = :charset_anno " + 1.529 + ") AS charset " + 1.530 + "FROM moz_bookmarks b " + 1.531 + "LEFT JOIN moz_bookmarks p ON p.id = b.parent " + 1.532 + "LEFT JOIN moz_places h ON h.id = b.fk " + 1.533 + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id " + 1.534 + "WHERE b.id <> :tags_folder AND b.parent <> :tags_folder AND p.parent <> :tags_folder " + 1.535 + "ORDER BY b.parent, b.position", 1.536 + { tags_folder: PlacesUtils.tagsFolderId, 1.537 + charset_anno: PlacesUtils.CHARSET_ANNO }); 1.538 + } catch(e) { 1.539 + Cu.reportError("Unable to query the database " + e); 1.540 + } finally { 1.541 + yield conn.close(); 1.542 + } 1.543 + 1.544 + let startTime = Date.now(); 1.545 + // Create a Map for lookup and recursive building of the tree. 1.546 + let itemsMap = new Map(); 1.547 + for (let row of rows) { 1.548 + let id = row.getResultByName("id"); 1.549 + try { 1.550 + let bookmark = sqliteRowToBookmarkObject(row); 1.551 + if (itemsMap.has(id)) { 1.552 + // Since children may be added before parents, we should merge with 1.553 + // the existing object. 1.554 + let original = itemsMap.get(id); 1.555 + for (let prop of Object.getOwnPropertyNames(bookmark)) { 1.556 + original[prop] = bookmark[prop]; 1.557 + } 1.558 + bookmark = original; 1.559 + } 1.560 + else { 1.561 + itemsMap.set(id, bookmark); 1.562 + } 1.563 + 1.564 + // Append bookmark to its parent. 1.565 + if (!itemsMap.has(bookmark.parent)) 1.566 + itemsMap.set(bookmark.parent, {}); 1.567 + let parent = itemsMap.get(bookmark.parent); 1.568 + if (!("children" in parent)) 1.569 + parent.children = []; 1.570 + parent.children.push(bookmark); 1.571 + } catch (e) { 1.572 + Cu.reportError("Error while reading node " + id + " " + e); 1.573 + } 1.574 + } 1.575 + 1.576 + // Handle excluded items, by removing entire subtrees pointed by them. 1.577 + function removeFromMap(id) { 1.578 + // Could have been removed by a previous call, since we can't 1.579 + // predict order of items in EXCLUDE_FROM_BACKUP_ANNO. 1.580 + if (itemsMap.has(id)) { 1.581 + let excludedItem = itemsMap.get(id); 1.582 + if (excludedItem.children) { 1.583 + for (let child of excludedItem.children) { 1.584 + removeFromMap(child.id); 1.585 + } 1.586 + } 1.587 + // Remove the excluded item from its parent's children... 1.588 + let parentItem = itemsMap.get(excludedItem.parent); 1.589 + parentItem.children = parentItem.children.filter(aChild => aChild.id != id); 1.590 + // ...then remove it from the map. 1.591 + itemsMap.delete(id); 1.592 + } 1.593 + } 1.594 + 1.595 + for (let id of PlacesUtils.annotations.getItemsWithAnnotation( 1.596 + PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO)) { 1.597 + removeFromMap(id); 1.598 + } 1.599 + 1.600 + // Report the time taken to build the tree. This doesn't take into 1.601 + // account the time spent in the query since that's off the main-thread. 1.602 + try { 1.603 + Services.telemetry 1.604 + .getHistogramById("PLACES_BACKUPS_BOOKMARKSTREE_MS") 1.605 + .add(Date.now() - startTime); 1.606 + } catch (ex) { 1.607 + Components.utils.reportError("Unable to report telemetry."); 1.608 + } 1.609 + 1.610 + return [itemsMap.get(PlacesUtils.placesRootId), itemsMap.size]; 1.611 + }); 1.612 + } 1.613 +} 1.614 + 1.615 +/** 1.616 + * Helper function to convert a Sqlite.jsm row to a bookmark object 1.617 + * representation. 1.618 + * 1.619 + * @param aRow The Sqlite.jsm result row. 1.620 + */ 1.621 +function sqliteRowToBookmarkObject(aRow) { 1.622 + let bookmark = {}; 1.623 + for (let p of [ "id" ,"guid", "title", "index", "dateAdded", "lastModified" ]) { 1.624 + bookmark[p] = aRow.getResultByName(p); 1.625 + } 1.626 + Object.defineProperty(bookmark, "parent", 1.627 + { value: aRow.getResultByName("parent") }); 1.628 + 1.629 + let type = aRow.getResultByName("type"); 1.630 + 1.631 + // Add annotations. 1.632 + if (aRow.getResultByName("has_annos")) { 1.633 + try { 1.634 + bookmark.annos = PlacesUtils.getAnnotationsForItem(bookmark.id); 1.635 + } catch (e) { 1.636 + Cu.reportError("Unexpected error while reading annotations " + e); 1.637 + } 1.638 + } 1.639 + 1.640 + switch (type) { 1.641 + case Ci.nsINavBookmarksService.TYPE_BOOKMARK: 1.642 + // TODO: What about shortcuts? 1.643 + bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE; 1.644 + // This will throw if we try to serialize an invalid url and the node will 1.645 + // just be skipped. 1.646 + bookmark.uri = NetUtil.newURI(aRow.getResultByName("url")).spec; 1.647 + // Keywords are cached, so this should be decently fast. 1.648 + let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(bookmark.id); 1.649 + if (keyword) 1.650 + bookmark.keyword = keyword; 1.651 + let charset = aRow.getResultByName("charset"); 1.652 + if (charset) 1.653 + bookmark.charset = charset; 1.654 + let tags = aRow.getResultByName("tags"); 1.655 + if (tags) 1.656 + bookmark.tags = tags; 1.657 + let iconuri = aRow.getResultByName("iconuri"); 1.658 + if (iconuri) 1.659 + bookmark.iconuri = iconuri; 1.660 + break; 1.661 + case Ci.nsINavBookmarksService.TYPE_FOLDER: 1.662 + bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER; 1.663 + 1.664 + // Mark root folders. 1.665 + if (bookmark.id == PlacesUtils.placesRootId) 1.666 + bookmark.root = "placesRoot"; 1.667 + else if (bookmark.id == PlacesUtils.bookmarksMenuFolderId) 1.668 + bookmark.root = "bookmarksMenuFolder"; 1.669 + else if (bookmark.id == PlacesUtils.unfiledBookmarksFolderId) 1.670 + bookmark.root = "unfiledBookmarksFolder"; 1.671 + else if (bookmark.id == PlacesUtils.toolbarFolderId) 1.672 + bookmark.root = "toolbarFolder"; 1.673 + break; 1.674 + case Ci.nsINavBookmarksService.TYPE_SEPARATOR: 1.675 + bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR; 1.676 + break; 1.677 + default: 1.678 + Cu.reportError("Unexpected bookmark type"); 1.679 + break; 1.680 + } 1.681 + return bookmark; 1.682 +}