/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- * vim: sw=2 ts=2 sts=2 expandtab filetype=javascript * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/PlacesUtils.jsm"); this.EXPORTED_SYMBOLS = [ "PlacesDBUtils" ]; //////////////////////////////////////////////////////////////////////////////// //// Constants const FINISHED_MAINTENANCE_TOPIC = "places-maintenance-finished"; const BYTES_PER_MEBIBYTE = 1048576; //////////////////////////////////////////////////////////////////////////////// //// Smart getters XPCOMUtils.defineLazyGetter(this, "DBConn", function() { return PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; }); //////////////////////////////////////////////////////////////////////////////// //// PlacesDBUtils this.PlacesDBUtils = { /** * Executes a list of maintenance tasks. * Once finished it will pass a array log to the callback attached to tasks. * FINISHED_MAINTENANCE_TOPIC is notified through observer service on finish. * * @param aTasks * Tasks object to execute. */ _executeTasks: function PDBU__executeTasks(aTasks) { if (PlacesDBUtils._isShuttingDown) { aTasks.log("- We are shutting down. Will not schedule the tasks."); aTasks.clear(); } let task = aTasks.pop(); if (task) { task.call(PlacesDBUtils, aTasks); } else { // All tasks have been completed. // Telemetry the time it took for maintenance, if a start time exists. if (aTasks._telemetryStart) { Services.telemetry.getHistogramById("PLACES_IDLE_MAINTENANCE_TIME_MS") .add(Date.now() - aTasks._telemetryStart); aTasks._telemetryStart = 0; } if (aTasks.callback) { let scope = aTasks.scope || Cu.getGlobalForObject(aTasks.callback); aTasks.callback.call(scope, aTasks.messages); } // Notify observers that maintenance finished. Services.prefs.setIntPref("places.database.lastMaintenance", parseInt(Date.now() / 1000)); Services.obs.notifyObservers(null, FINISHED_MAINTENANCE_TOPIC, null); } }, _isShuttingDown : false, shutdown: function PDBU_shutdown() { PlacesDBUtils._isShuttingDown = true; }, /** * Executes integrity check and common maintenance tasks. * * @param [optional] aCallback * Callback to be invoked when done. The callback will get a array * of log messages. * @param [optional] aScope * Scope for the callback. */ maintenanceOnIdle: function PDBU_maintenanceOnIdle(aCallback, aScope) { let tasks = new Tasks([ this.checkIntegrity , this.checkCoherence , this._refreshUI ]); tasks._telemetryStart = Date.now(); tasks.callback = aCallback; tasks.scope = aScope; this._executeTasks(tasks); }, /** * Executes integrity check, common and advanced maintenance tasks (like * expiration and vacuum). Will also collect statistics on the database. * * @param [optional] aCallback * Callback to be invoked when done. The callback will get a array * of log messages. * @param [optional] aScope * Scope for the callback. */ checkAndFixDatabase: function PDBU_checkAndFixDatabase(aCallback, aScope) { let tasks = new Tasks([ this.checkIntegrity , this.checkCoherence , this.expire , this.vacuum , this.stats , this._refreshUI ]); tasks.callback = aCallback; tasks.scope = aScope; this._executeTasks(tasks); }, /** * Forces a full refresh of Places views. * * @param [optional] aTasks * Tasks object to execute. */ _refreshUI: function PDBU__refreshUI(aTasks) { let tasks = new Tasks(aTasks); // Send batch update notifications to update the UI. PlacesUtils.history.runInBatchMode({ runBatched: function (aUserData) {} }, null); PlacesDBUtils._executeTasks(tasks); }, _handleError: function PDBU__handleError(aError) { Cu.reportError("Async statement execution returned with '" + aError.result + "', '" + aError.message + "'"); }, /** * Tries to execute a REINDEX on the database. * * @param [optional] aTasks * Tasks object to execute. */ reindex: function PDBU_reindex(aTasks) { let tasks = new Tasks(aTasks); tasks.log("> Reindex"); let stmt = DBConn.createAsyncStatement("REINDEX"); stmt.executeAsync({ handleError: PlacesDBUtils._handleError, handleResult: function () {}, handleCompletion: function (aReason) { if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { tasks.log("+ The database has been reindexed"); } else { tasks.log("- Unable to reindex database"); } PlacesDBUtils._executeTasks(tasks); } }); stmt.finalize(); }, /** * Checks integrity but does not try to fix the database through a reindex. * * @param [optional] aTasks * Tasks object to execute. */ _checkIntegritySkipReindex: function PDBU__checkIntegritySkipReindex(aTasks) this.checkIntegrity(aTasks, true), /** * Checks integrity and tries to fix the database through a reindex. * * @param [optional] aTasks * Tasks object to execute. * @param [optional] aSkipdReindex * Whether to try to reindex database or not. */ checkIntegrity: function PDBU_checkIntegrity(aTasks, aSkipReindex) { let tasks = new Tasks(aTasks); tasks.log("> Integrity check"); // Run a integrity check, but stop at the first error. let stmt = DBConn.createAsyncStatement("PRAGMA integrity_check(1)"); stmt.executeAsync({ handleError: PlacesDBUtils._handleError, _corrupt: false, handleResult: function (aResultSet) { let row = aResultSet.getNextRow(); this._corrupt = row.getResultByIndex(0) != "ok"; }, handleCompletion: function (aReason) { if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { if (this._corrupt) { tasks.log("- The database is corrupt"); if (aSkipReindex) { tasks.log("- Unable to fix corruption, database will be replaced on next startup"); Services.prefs.setBoolPref("places.database.replaceOnStartup", true); tasks.clear(); } else { // Try to reindex, this often fixed simple indices corruption. // We insert from the top of the queue, they will run inverse. tasks.push(PlacesDBUtils._checkIntegritySkipReindex); tasks.push(PlacesDBUtils.reindex); } } else { tasks.log("+ The database is sane"); } } else { tasks.log("- Unable to check database status"); tasks.clear(); } PlacesDBUtils._executeTasks(tasks); } }); stmt.finalize(); }, /** * Checks data coherence and tries to fix most common errors. * * @param [optional] aTasks * Tasks object to execute. */ checkCoherence: function PDBU_checkCoherence(aTasks) { let tasks = new Tasks(aTasks); tasks.log("> Coherence check"); let stmts = PlacesDBUtils._getBoundCoherenceStatements(); DBConn.executeAsync(stmts, stmts.length, { handleError: PlacesDBUtils._handleError, handleResult: function () {}, handleCompletion: function (aReason) { if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { tasks.log("+ The database is coherent"); } else { tasks.log("- Unable to check database coherence"); tasks.clear(); } PlacesDBUtils._executeTasks(tasks); } }); stmts.forEach(function (aStmt) aStmt.finalize()); }, _getBoundCoherenceStatements: function PDBU__getBoundCoherenceStatements() { let cleanupStatements = []; // MOZ_ANNO_ATTRIBUTES // A.1 remove obsolete annotations from moz_annos. // The 'weave0' idiom exploits character ordering (0 follows /) to // efficiently select all annos with a 'weave/' prefix. let deleteObsoleteAnnos = DBConn.createAsyncStatement( `DELETE FROM moz_annos WHERE anno_attribute_id IN ( SELECT id FROM moz_anno_attributes WHERE name BETWEEN 'weave/' AND 'weave0' )`); cleanupStatements.push(deleteObsoleteAnnos); // A.2 remove obsolete annotations from moz_items_annos. let deleteObsoleteItemsAnnos = DBConn.createAsyncStatement( `DELETE FROM moz_items_annos WHERE anno_attribute_id IN ( SELECT id FROM moz_anno_attributes WHERE name = 'sync/children' OR name = 'placesInternal/GUID' OR name BETWEEN 'weave/' AND 'weave0' )`); cleanupStatements.push(deleteObsoleteItemsAnnos); // A.3 remove unused attributes. let deleteUnusedAnnoAttributes = DBConn.createAsyncStatement( `DELETE FROM moz_anno_attributes WHERE id IN ( SELECT id FROM moz_anno_attributes n WHERE NOT EXISTS (SELECT id FROM moz_annos WHERE anno_attribute_id = n.id LIMIT 1) AND NOT EXISTS (SELECT id FROM moz_items_annos WHERE anno_attribute_id = n.id LIMIT 1) )`); cleanupStatements.push(deleteUnusedAnnoAttributes); // MOZ_ANNOS // B.1 remove annos with an invalid attribute let deleteInvalidAttributeAnnos = DBConn.createAsyncStatement( `DELETE FROM moz_annos WHERE id IN ( SELECT id FROM moz_annos a WHERE NOT EXISTS (SELECT id FROM moz_anno_attributes WHERE id = a.anno_attribute_id LIMIT 1) )`); cleanupStatements.push(deleteInvalidAttributeAnnos); // B.2 remove orphan annos let deleteOrphanAnnos = DBConn.createAsyncStatement( `DELETE FROM moz_annos WHERE id IN ( SELECT id FROM moz_annos a WHERE NOT EXISTS (SELECT id FROM moz_places WHERE id = a.place_id LIMIT 1) )`); cleanupStatements.push(deleteOrphanAnnos); // Bookmarks roots // C.1 fix missing Places root // Bug 477739 shows a case where the root could be wrongly removed // due to an endianness issue. We try to fix broken roots here. let selectPlacesRoot = DBConn.createStatement( "SELECT id FROM moz_bookmarks WHERE id = :places_root"); selectPlacesRoot.params["places_root"] = PlacesUtils.placesRootId; if (!selectPlacesRoot.executeStep()) { // We are missing the root, try to recreate it. let createPlacesRoot = DBConn.createAsyncStatement( `INSERT INTO moz_bookmarks (id, type, fk, parent, position, title, guid) VALUES (:places_root, 2, NULL, 0, 0, :title, :guid)`); createPlacesRoot.params["places_root"] = PlacesUtils.placesRootId; createPlacesRoot.params["title"] = ""; createPlacesRoot.params["guid"] = PlacesUtils.bookmarks.rootGuid; cleanupStatements.push(createPlacesRoot); // Now ensure that other roots are children of Places root. let fixPlacesRootChildren = DBConn.createAsyncStatement( `UPDATE moz_bookmarks SET parent = :places_root WHERE guid IN ( :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid )`); fixPlacesRootChildren.params["places_root"] = PlacesUtils.placesRootId; fixPlacesRootChildren.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; fixPlacesRootChildren.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; fixPlacesRootChildren.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; fixPlacesRootChildren.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; cleanupStatements.push(fixPlacesRootChildren); } selectPlacesRoot.finalize(); // C.2 fix roots titles // some alpha version has wrong roots title, and this also fixes them if // locale has changed. let updateRootTitleSql = `UPDATE moz_bookmarks SET title = :title WHERE id = :root_id AND title <> :title`; // root let fixPlacesRootTitle = DBConn.createAsyncStatement(updateRootTitleSql); fixPlacesRootTitle.params["root_id"] = PlacesUtils.placesRootId; fixPlacesRootTitle.params["title"] = ""; cleanupStatements.push(fixPlacesRootTitle); // bookmarks menu let fixBookmarksMenuTitle = DBConn.createAsyncStatement(updateRootTitleSql); fixBookmarksMenuTitle.params["root_id"] = PlacesUtils.bookmarksMenuFolderId; fixBookmarksMenuTitle.params["title"] = PlacesUtils.getString("BookmarksMenuFolderTitle"); cleanupStatements.push(fixBookmarksMenuTitle); // bookmarks toolbar let fixBookmarksToolbarTitle = DBConn.createAsyncStatement(updateRootTitleSql); fixBookmarksToolbarTitle.params["root_id"] = PlacesUtils.toolbarFolderId; fixBookmarksToolbarTitle.params["title"] = PlacesUtils.getString("BookmarksToolbarFolderTitle"); cleanupStatements.push(fixBookmarksToolbarTitle); // unsorted bookmarks let fixUnsortedBookmarksTitle = DBConn.createAsyncStatement(updateRootTitleSql); fixUnsortedBookmarksTitle.params["root_id"] = PlacesUtils.unfiledBookmarksFolderId; fixUnsortedBookmarksTitle.params["title"] = PlacesUtils.getString("UnsortedBookmarksFolderTitle"); cleanupStatements.push(fixUnsortedBookmarksTitle); // tags let fixTagsRootTitle = DBConn.createAsyncStatement(updateRootTitleSql); fixTagsRootTitle.params["root_id"] = PlacesUtils.tagsFolderId; fixTagsRootTitle.params["title"] = PlacesUtils.getString("TagsFolderTitle"); cleanupStatements.push(fixTagsRootTitle); // MOZ_BOOKMARKS // D.1 remove items without a valid place // if fk IS NULL we fix them in D.7 let deleteNoPlaceItems = DBConn.createAsyncStatement( `DELETE FROM moz_bookmarks WHERE guid NOT IN ( :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ ) AND id IN ( SELECT b.id FROM moz_bookmarks b WHERE fk NOT NULL AND b.type = :bookmark_type AND NOT EXISTS (SELECT url FROM moz_places WHERE id = b.fk LIMIT 1) )`); deleteNoPlaceItems.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK; deleteNoPlaceItems.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; deleteNoPlaceItems.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; deleteNoPlaceItems.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; deleteNoPlaceItems.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; deleteNoPlaceItems.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; cleanupStatements.push(deleteNoPlaceItems); // D.2 remove items that are not uri bookmarks from tag containers let deleteBogusTagChildren = DBConn.createAsyncStatement( `DELETE FROM moz_bookmarks WHERE guid NOT IN ( :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ ) AND id IN ( SELECT b.id FROM moz_bookmarks b WHERE b.parent IN (SELECT id FROM moz_bookmarks WHERE parent = :tags_folder) AND b.type <> :bookmark_type )`); deleteBogusTagChildren.params["tags_folder"] = PlacesUtils.tagsFolderId; deleteBogusTagChildren.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK; deleteBogusTagChildren.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; deleteBogusTagChildren.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; deleteBogusTagChildren.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; deleteBogusTagChildren.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; deleteBogusTagChildren.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; cleanupStatements.push(deleteBogusTagChildren); // D.3 remove empty tags let deleteEmptyTags = DBConn.createAsyncStatement( `DELETE FROM moz_bookmarks WHERE guid NOT IN ( :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ ) AND id IN ( SELECT b.id FROM moz_bookmarks b WHERE b.id IN (SELECT id FROM moz_bookmarks WHERE parent = :tags_folder) AND NOT EXISTS (SELECT id from moz_bookmarks WHERE parent = b.id LIMIT 1) )`); deleteEmptyTags.params["tags_folder"] = PlacesUtils.tagsFolderId; deleteEmptyTags.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; deleteEmptyTags.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; deleteEmptyTags.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; deleteEmptyTags.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; deleteEmptyTags.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; cleanupStatements.push(deleteEmptyTags); // D.4 move orphan items to unsorted folder let fixOrphanItems = DBConn.createAsyncStatement( `UPDATE moz_bookmarks SET parent = :unsorted_folder WHERE guid NOT IN ( :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ ) AND id IN ( SELECT b.id FROM moz_bookmarks b WHERE NOT EXISTS (SELECT id FROM moz_bookmarks WHERE id = b.parent LIMIT 1) )`); fixOrphanItems.params["unsorted_folder"] = PlacesUtils.unfiledBookmarksFolderId; fixOrphanItems.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; fixOrphanItems.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; fixOrphanItems.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; fixOrphanItems.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; fixOrphanItems.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; cleanupStatements.push(fixOrphanItems); // D.6 fix wrong item types // Folders and separators should not have an fk. // If they have a valid fk convert them to bookmarks. Later in D.9 we // will move eventual children to unsorted bookmarks. let fixBookmarksAsFolders = DBConn.createAsyncStatement( `UPDATE moz_bookmarks SET type = :bookmark_type WHERE guid NOT IN ( :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ ) AND id IN ( SELECT id FROM moz_bookmarks b WHERE type IN (:folder_type, :separator_type) AND fk NOTNULL )`); fixBookmarksAsFolders.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK; fixBookmarksAsFolders.params["folder_type"] = PlacesUtils.bookmarks.TYPE_FOLDER; fixBookmarksAsFolders.params["separator_type"] = PlacesUtils.bookmarks.TYPE_SEPARATOR; fixBookmarksAsFolders.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; fixBookmarksAsFolders.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; fixBookmarksAsFolders.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; fixBookmarksAsFolders.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; fixBookmarksAsFolders.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; cleanupStatements.push(fixBookmarksAsFolders); // D.7 fix wrong item types // Bookmarks should have an fk, if they don't have any, convert them to // folders. let fixFoldersAsBookmarks = DBConn.createAsyncStatement( `UPDATE moz_bookmarks SET type = :folder_type WHERE guid NOT IN ( :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ ) AND id IN ( SELECT id FROM moz_bookmarks b WHERE type = :bookmark_type AND fk IS NULL )`); fixFoldersAsBookmarks.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK; fixFoldersAsBookmarks.params["folder_type"] = PlacesUtils.bookmarks.TYPE_FOLDER; fixFoldersAsBookmarks.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; fixFoldersAsBookmarks.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; fixFoldersAsBookmarks.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; fixFoldersAsBookmarks.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; fixFoldersAsBookmarks.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; cleanupStatements.push(fixFoldersAsBookmarks); // D.9 fix wrong parents // Items cannot have separators or other bookmarks // as parent, if they have bad parent move them to unsorted bookmarks. let fixInvalidParents = DBConn.createAsyncStatement( `UPDATE moz_bookmarks SET parent = :unsorted_folder WHERE guid NOT IN ( :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ ) AND id IN ( SELECT id FROM moz_bookmarks b WHERE EXISTS (SELECT id FROM moz_bookmarks WHERE id = b.parent AND type IN (:bookmark_type, :separator_type) LIMIT 1) )`); fixInvalidParents.params["unsorted_folder"] = PlacesUtils.unfiledBookmarksFolderId; fixInvalidParents.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK; fixInvalidParents.params["separator_type"] = PlacesUtils.bookmarks.TYPE_SEPARATOR; fixInvalidParents.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; fixInvalidParents.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; fixInvalidParents.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; fixInvalidParents.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; fixInvalidParents.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; cleanupStatements.push(fixInvalidParents); // D.10 recalculate positions // This requires multiple related statements. // We can detect a folder with bad position values comparing the sum of // all distinct position values (+1 since position is 0-based) with the // triangular numbers obtained by the number of children (n). // SUM(DISTINCT position + 1) == (n * (n + 1) / 2). cleanupStatements.push(DBConn.createAsyncStatement( `CREATE TEMP TABLE IF NOT EXISTS moz_bm_reindex_temp ( id INTEGER PRIMARY_KEY , parent INTEGER , position INTEGER )` )); cleanupStatements.push(DBConn.createAsyncStatement( `INSERT INTO moz_bm_reindex_temp SELECT id, parent, 0 FROM moz_bookmarks b WHERE parent IN ( SELECT parent FROM moz_bookmarks GROUP BY parent HAVING (SUM(DISTINCT position + 1) - (count(*) * (count(*) + 1) / 2)) <> 0 ) ORDER BY parent ASC, position ASC, ROWID ASC` )); cleanupStatements.push(DBConn.createAsyncStatement( `CREATE INDEX IF NOT EXISTS moz_bm_reindex_temp_index ON moz_bm_reindex_temp(parent)` )); cleanupStatements.push(DBConn.createAsyncStatement( `UPDATE moz_bm_reindex_temp SET position = ( ROWID - (SELECT MIN(t.ROWID) FROM moz_bm_reindex_temp t WHERE t.parent = moz_bm_reindex_temp.parent) )` )); cleanupStatements.push(DBConn.createAsyncStatement( `CREATE TEMP TRIGGER IF NOT EXISTS moz_bm_reindex_temp_trigger BEFORE DELETE ON moz_bm_reindex_temp FOR EACH ROW BEGIN UPDATE moz_bookmarks SET position = OLD.position WHERE id = OLD.id; END` )); cleanupStatements.push(DBConn.createAsyncStatement( "DELETE FROM moz_bm_reindex_temp " )); cleanupStatements.push(DBConn.createAsyncStatement( "DROP INDEX moz_bm_reindex_temp_index " )); cleanupStatements.push(DBConn.createAsyncStatement( "DROP TRIGGER moz_bm_reindex_temp_trigger " )); cleanupStatements.push(DBConn.createAsyncStatement( "DROP TABLE moz_bm_reindex_temp " )); // D.12 Fix empty-named tags. // Tags were allowed to have empty names due to a UI bug. Fix them // replacing their title with "(notitle)". let fixEmptyNamedTags = DBConn.createAsyncStatement( `UPDATE moz_bookmarks SET title = :empty_title WHERE length(title) = 0 AND type = :folder_type AND parent = :tags_folder` ); fixEmptyNamedTags.params["empty_title"] = "(notitle)"; fixEmptyNamedTags.params["folder_type"] = PlacesUtils.bookmarks.TYPE_FOLDER; fixEmptyNamedTags.params["tags_folder"] = PlacesUtils.tagsFolderId; cleanupStatements.push(fixEmptyNamedTags); // MOZ_FAVICONS // E.1 remove orphan icons let deleteOrphanIcons = DBConn.createAsyncStatement( `DELETE FROM moz_favicons WHERE id IN ( SELECT id FROM moz_favicons f WHERE NOT EXISTS (SELECT id FROM moz_places WHERE favicon_id = f.id LIMIT 1) )`); cleanupStatements.push(deleteOrphanIcons); // MOZ_HISTORYVISITS // F.1 remove orphan visits let deleteOrphanVisits = DBConn.createAsyncStatement( `DELETE FROM moz_historyvisits WHERE id IN ( SELECT id FROM moz_historyvisits v WHERE NOT EXISTS (SELECT id FROM moz_places WHERE id = v.place_id LIMIT 1) )`); cleanupStatements.push(deleteOrphanVisits); // MOZ_INPUTHISTORY // G.1 remove orphan input history let deleteOrphanInputHistory = DBConn.createAsyncStatement( `DELETE FROM moz_inputhistory WHERE place_id IN ( SELECT place_id FROM moz_inputhistory i WHERE NOT EXISTS (SELECT id FROM moz_places WHERE id = i.place_id LIMIT 1) )`); cleanupStatements.push(deleteOrphanInputHistory); // MOZ_ITEMS_ANNOS // H.1 remove item annos with an invalid attribute let deleteInvalidAttributeItemsAnnos = DBConn.createAsyncStatement( `DELETE FROM moz_items_annos WHERE id IN ( SELECT id FROM moz_items_annos t WHERE NOT EXISTS (SELECT id FROM moz_anno_attributes WHERE id = t.anno_attribute_id LIMIT 1) )`); cleanupStatements.push(deleteInvalidAttributeItemsAnnos); // H.2 remove orphan item annos let deleteOrphanItemsAnnos = DBConn.createAsyncStatement( `DELETE FROM moz_items_annos WHERE id IN ( SELECT id FROM moz_items_annos t WHERE NOT EXISTS (SELECT id FROM moz_bookmarks WHERE id = t.item_id LIMIT 1) )`); cleanupStatements.push(deleteOrphanItemsAnnos); // MOZ_KEYWORDS // I.1 remove unused keywords let deleteUnusedKeywords = DBConn.createAsyncStatement( `DELETE FROM moz_keywords WHERE id IN ( SELECT id FROM moz_keywords k WHERE NOT EXISTS (SELECT 1 FROM moz_places h WHERE k.place_id = h.id) )`); cleanupStatements.push(deleteUnusedKeywords); // MOZ_PLACES // L.1 fix wrong favicon ids let fixInvalidFaviconIds = DBConn.createAsyncStatement( `UPDATE moz_places SET favicon_id = NULL WHERE id IN ( SELECT id FROM moz_places h WHERE favicon_id NOT NULL AND NOT EXISTS (SELECT id FROM moz_favicons WHERE id = h.favicon_id LIMIT 1) )`); cleanupStatements.push(fixInvalidFaviconIds); // L.2 recalculate visit_count and last_visit_date let fixVisitStats = DBConn.createAsyncStatement( `UPDATE moz_places SET visit_count = (SELECT count(*) FROM moz_historyvisits WHERE place_id = moz_places.id AND visit_type NOT IN (0,4,7,8)), last_visit_date = (SELECT MAX(visit_date) FROM moz_historyvisits WHERE place_id = moz_places.id) WHERE id IN ( SELECT h.id FROM moz_places h WHERE visit_count <> (SELECT count(*) FROM moz_historyvisits v WHERE v.place_id = h.id AND visit_type NOT IN (0,4,7,8)) OR last_visit_date <> (SELECT MAX(visit_date) FROM moz_historyvisits v WHERE v.place_id = h.id) )`); cleanupStatements.push(fixVisitStats); // L.3 recalculate hidden for redirects. let fixRedirectsHidden = DBConn.createAsyncStatement( `UPDATE moz_places SET hidden = 1 WHERE id IN ( SELECT h.id FROM moz_places h JOIN moz_historyvisits src ON src.place_id = h.id JOIN moz_historyvisits dst ON dst.from_visit = src.id AND dst.visit_type IN (5,6) LEFT JOIN moz_bookmarks on fk = h.id AND fk ISNULL GROUP BY src.place_id HAVING count(*) = visit_count )`); cleanupStatements.push(fixRedirectsHidden); // L.4 recalculate foreign_count. let fixForeignCount = DBConn.createAsyncStatement( `UPDATE moz_places SET foreign_count = (SELECT count(*) FROM moz_bookmarks WHERE fk = moz_places.id )`); cleanupStatements.push(fixForeignCount); // MAINTENANCE STATEMENTS SHOULD GO ABOVE THIS POINT! return cleanupStatements; }, /** * Tries to vacuum the database. * * @param [optional] aTasks * Tasks object to execute. */ vacuum: function PDBU_vacuum(aTasks) { let tasks = new Tasks(aTasks); tasks.log("> Vacuum"); let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); DBFile.append("places.sqlite"); tasks.log("Initial database size is " + parseInt(DBFile.fileSize / 1024) + " KiB"); let stmt = DBConn.createAsyncStatement("VACUUM"); stmt.executeAsync({ handleError: PlacesDBUtils._handleError, handleResult: function () {}, handleCompletion: function (aReason) { if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { tasks.log("+ The database has been vacuumed"); let vacuumedDBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); vacuumedDBFile.append("places.sqlite"); tasks.log("Final database size is " + parseInt(vacuumedDBFile.fileSize / 1024) + " KiB"); } else { tasks.log("- Unable to vacuum database"); tasks.clear(); } PlacesDBUtils._executeTasks(tasks); } }); stmt.finalize(); }, /** * Forces a full expiration on the database. * * @param [optional] aTasks * Tasks object to execute. */ expire: function PDBU_expire(aTasks) { let tasks = new Tasks(aTasks); tasks.log("> Orphans expiration"); let expiration = Cc["@mozilla.org/places/expiration;1"]. getService(Ci.nsIObserver); Services.obs.addObserver(function (aSubject, aTopic, aData) { Services.obs.removeObserver(arguments.callee, aTopic); tasks.log("+ Database cleaned up"); PlacesDBUtils._executeTasks(tasks); }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false); // Force an orphans expiration step. expiration.observe(null, "places-debug-start-expiration", 0); }, /** * Collects statistical data on the database. * * @param [optional] aTasks * Tasks object to execute. */ stats: function PDBU_stats(aTasks) { let tasks = new Tasks(aTasks); tasks.log("> Statistics"); let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); DBFile.append("places.sqlite"); tasks.log("Database size is " + parseInt(DBFile.fileSize / 1024) + " KiB"); [ "user_version" , "page_size" , "cache_size" , "journal_mode" , "synchronous" ].forEach(function (aPragma) { let stmt = DBConn.createStatement("PRAGMA " + aPragma); stmt.executeStep(); tasks.log(aPragma + " is " + stmt.getString(0)); stmt.finalize(); }); // Get maximum number of unique URIs. try { let limitURIs = Services.prefs.getIntPref( "places.history.expiration.transient_current_max_pages"); tasks.log("History can store a maximum of " + limitURIs + " unique pages"); } catch(ex) {} let stmt = DBConn.createStatement( "SELECT name FROM sqlite_master WHERE type = :type"); stmt.params.type = "table"; while (stmt.executeStep()) { let tableName = stmt.getString(0); let countStmt = DBConn.createStatement( `SELECT count(*) FROM ${tableName}`); countStmt.executeStep(); tasks.log("Table " + tableName + " has " + countStmt.getInt32(0) + " records"); countStmt.finalize(); } stmt.reset(); stmt.params.type = "index"; while (stmt.executeStep()) { tasks.log("Index " + stmt.getString(0)); } stmt.reset(); stmt.params.type = "trigger"; while (stmt.executeStep()) { tasks.log("Trigger " + stmt.getString(0)); } stmt.finalize(); PlacesDBUtils._executeTasks(tasks); }, /** * Collects telemetry data. * * There are essentially two modes of collection and the mode is * determined by the presence of aHealthReportCallback. If * aHealthReportCallback is not defined (the default) then we are in * "Telemetry" mode. Results will be reported to Telemetry. If we are * in "Health Report" mode only the probes with a true healthreport * flag will be collected and the results will be reported to the * aHealthReportCallback. * * @param [optional] aTasks * Tasks object to execute. * @param [optional] aHealthReportCallback * Function to receive data relevant for Firefox Health Report. */ telemetry: function PDBU_telemetry(aTasks, aHealthReportCallback=null) { let tasks = new Tasks(aTasks); let isTelemetry = !aHealthReportCallback; // This will be populated with one integer property for each probe result, // using the histogram name as key. let probeValues = {}; // The following array contains an ordered list of entries that are // processed to collect telemetry data. Each entry has these properties: // // histogram: Name of the telemetry histogram to update. // query: This is optional. If present, contains a database command // that will be executed asynchronously, and whose result will // be added to the telemetry histogram. // callback: This is optional. If present, contains a function that must // return the value that will be added to the telemetry // histogram. If a query is also present, its result is passed // as the first argument of the function. If the function // raises an exception, no data is added to the histogram. // healthreport: Boolean indicating whether this probe is relevant // to Firefox Health Report. // // Since all queries are executed in order by the database backend, the // callbacks can also use the result of previous queries stored in the // probeValues object. let probes = [ { histogram: "PLACES_PAGES_COUNT", healthreport: true, query: "SELECT count(*) FROM moz_places" }, { histogram: "PLACES_BOOKMARKS_COUNT", healthreport: true, query: `SELECT count(*) FROM moz_bookmarks b JOIN moz_bookmarks t ON t.id = b.parent AND t.parent <> :tags_folder WHERE b.type = :type_bookmark` }, { histogram: "PLACES_TAGS_COUNT", query: `SELECT count(*) FROM moz_bookmarks WHERE parent = :tags_folder` }, { histogram: "PLACES_KEYWORDS_COUNT", query: "SELECT count(*) FROM moz_keywords" }, { histogram: "PLACES_SORTED_BOOKMARKS_PERC", query: `SELECT IFNULL(ROUND(( SELECT count(*) FROM moz_bookmarks b JOIN moz_bookmarks t ON t.id = b.parent AND t.parent <> :tags_folder AND t.parent > :places_root WHERE b.type = :type_bookmark ) * 100 / ( SELECT count(*) FROM moz_bookmarks b JOIN moz_bookmarks t ON t.id = b.parent AND t.parent <> :tags_folder WHERE b.type = :type_bookmark )), 0)` }, { histogram: "PLACES_TAGGED_BOOKMARKS_PERC", query: `SELECT IFNULL(ROUND(( SELECT count(*) FROM moz_bookmarks b JOIN moz_bookmarks t ON t.id = b.parent AND t.parent = :tags_folder ) * 100 / ( SELECT count(*) FROM moz_bookmarks b JOIN moz_bookmarks t ON t.id = b.parent AND t.parent <> :tags_folder WHERE b.type = :type_bookmark )), 0)` }, { histogram: "PLACES_DATABASE_FILESIZE_MB", callback: function () { let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); DBFile.append("places.sqlite"); return parseInt(DBFile.fileSize / BYTES_PER_MEBIBYTE); } }, { histogram: "PLACES_DATABASE_PAGESIZE_B", query: "PRAGMA page_size /* PlacesDBUtils.jsm PAGESIZE_B */" }, { histogram: "PLACES_DATABASE_SIZE_PER_PAGE_B", query: "PRAGMA page_count", callback: function (aDbPageCount) { // Note that the database file size would not be meaningful for this // calculation, because the file grows in fixed-size chunks. let dbPageSize = probeValues.PLACES_DATABASE_PAGESIZE_B; let placesPageCount = probeValues.PLACES_PAGES_COUNT; return Math.round((dbPageSize * aDbPageCount) / placesPageCount); } }, { histogram: "PLACES_ANNOS_BOOKMARKS_COUNT", query: "SELECT count(*) FROM moz_items_annos" }, { histogram: "PLACES_ANNOS_PAGES_COUNT", query: "SELECT count(*) FROM moz_annos" }, { histogram: "PLACES_MAINTENANCE_DAYSFROMLAST", callback: function () { try { let lastMaintenance = Services.prefs.getIntPref("places.database.lastMaintenance"); let nowSeconds = parseInt(Date.now() / 1000); return parseInt((nowSeconds - lastMaintenance) / 86400); } catch (ex) { return 60; } } }, ]; let params = { tags_folder: PlacesUtils.tagsFolderId, type_folder: PlacesUtils.bookmarks.TYPE_FOLDER, type_bookmark: PlacesUtils.bookmarks.TYPE_BOOKMARK, places_root: PlacesUtils.placesRootId }; let outstandingProbes = []; for (let i = 0; i < probes.length; i++) { let probe = probes[i]; if (!isTelemetry && !probe.healthreport) { continue; } let promiseDone = new Promise((resolve, reject) => { if (!("query" in probe)) { resolve([probe]); return; } let stmt = DBConn.createAsyncStatement(probe.query); for (let param in params) { if (probe.query.indexOf(":" + param) > 0) { stmt.params[param] = params[param]; } } try { stmt.executeAsync({ handleError: reject, handleResult: function (aResultSet) { let row = aResultSet.getNextRow(); resolve([probe, row.getResultByIndex(0)]); }, handleCompletion: function () {} }); } finally{ stmt.finalize(); } }); // Report the result of the probe through Telemetry. // The resulting promise cannot reject. promiseDone = promiseDone.then( // On success ([aProbe, aValue]) => { let value = aValue; try { if ("callback" in aProbe) { value = aProbe.callback(value); } probeValues[aProbe.histogram] = value; Services.telemetry.getHistogramById(aProbe.histogram).add(value); } catch (ex) { Components.utils.reportError("Error adding value " + value + " to histogram " + aProbe.histogram + ": " + ex); } }, // On failure this._handleError); outstandingProbes.push(promiseDone); } if (aHealthReportCallback) { Promise.all(outstandingProbes).then(() => aHealthReportCallback(probeValues) ); } PlacesDBUtils._executeTasks(tasks); }, /** * Runs a list of tasks, notifying log messages to the callback. * * @param aTasks * Array of tasks to be executed, in form of pointers to methods in * this module. * @param [optional] aCallback * Callback to be invoked when done. It will receive an array of * log messages. */ runTasks: function PDBU_runTasks(aTasks, aCallback) { let tasks = new Tasks(aTasks); tasks.callback = aCallback; PlacesDBUtils._executeTasks(tasks); } }; /** * LIFO tasks stack. * * @param [optional] aTasks * Array of tasks or another Tasks object to clone. */ function Tasks(aTasks) { if (aTasks) { if (Array.isArray(aTasks)) { this._list = aTasks.slice(0, aTasks.length); } // This supports passing in a Tasks-like object, with a "list" property, // for compatibility reasons. else if (typeof(aTasks) == "object" && (Tasks instanceof Tasks || "list" in aTasks)) { this._list = aTasks.list; this._log = aTasks.messages; this.callback = aTasks.callback; this.scope = aTasks.scope; this._telemetryStart = aTasks._telemetryStart; } } } Tasks.prototype = { _list: [], _log: [], callback: null, scope: null, _telemetryStart: 0, /** * Adds a task to the top of the list. * * @param aNewElt * Task to be added. */ push: function T_push(aNewElt) { this._list.unshift(aNewElt); }, /** * Returns and consumes next task. * * @return next task or undefined if no task is left. */ pop: function T_pop() this._list.shift(), /** * Removes all tasks. */ clear: function T_clear() { this._list.length = 0; }, /** * Returns array of tasks ordered from the next to be run to the latest. */ get list() this._list.slice(0, this._list.length), /** * Adds a message to the log. * * @param aMsg * String message to be added. */ log: function T_log(aMsg) { this._log.push(aMsg); }, /** * Returns array of log messages ordered from oldest to newest. */ get messages() this._log.slice(0, this._log.length), }