|
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 } |