diff options
Diffstat (limited to 'toolkit/components/places/PlacesDBUtils.sys.mjs')
-rw-r--r-- | toolkit/components/places/PlacesDBUtils.sys.mjs | 1399 |
1 files changed, 1399 insertions, 0 deletions
diff --git a/toolkit/components/places/PlacesDBUtils.sys.mjs b/toolkit/components/places/PlacesDBUtils.sys.mjs new file mode 100644 index 0000000000..5cc2bfc631 --- /dev/null +++ b/toolkit/components/places/PlacesDBUtils.sys.mjs @@ -0,0 +1,1399 @@ +/* -*- 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 BYTES_PER_MEBIBYTE = 1048576; +const MS_PER_DAY = 86400000; +// Threshold value for removeOldCorruptDBs. +// Corrupt DBs older than this value are removed. +const CORRUPT_DB_RETAIN_DAYS = 14; + +// Seconds between maintenance runs. +const MAINTENANCE_INTERVAL_SECONDS = 7 * 86400; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PlacesPreviews: "resource://gre/modules/PlacesPreviews.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", +}); + +export var PlacesDBUtils = { + _isShuttingDown: false, + + _clearTaskQueue: false, + clearPendingTasks() { + PlacesDBUtils._clearTaskQueue = true; + }, + + /** + * Executes integrity check and common maintenance tasks. + * + * @return a Map[taskName(String) -> Object]. The Object has the following properties: + * - succeeded: boolean + * - logs: an array of strings containing the messages logged by the task. + */ + async maintenanceOnIdle() { + let tasks = [ + this.checkIntegrity, + this.checkCoherence, + this._refreshUI, + this.incrementalVacuum, + this.removeOldCorruptDBs, + this.deleteOrphanPreviews, + ]; + let telemetryStartTime = Date.now(); + let taskStatusMap = await PlacesDBUtils.runTasks(tasks); + + Services.prefs.setIntPref( + "places.database.lastMaintenance", + parseInt(Date.now() / 1000) + ); + Services.telemetry + .getHistogramById("PLACES_IDLE_MAINTENANCE_TIME_MS") + .add(Date.now() - telemetryStartTime); + return taskStatusMap; + }, + + /** + * Executes integrity check, common and advanced maintenance tasks (like + * expiration and vacuum). Will also collect statistics on the database. + * + * Note: although this function isn't actually async, we keep it async to + * allow us to maintain a simple, consistent API for the tasks within this object. + * + * @return {Promise} + * A promise that resolves with a Map[taskName(String) -> Object]. + * The Object has the following properties: + * - succeeded: boolean + * - logs: an array of strings containing the messages logged by the task. + */ + async checkAndFixDatabase() { + let tasks = [ + this.checkIntegrity, + this.checkCoherence, + this.expire, + this.vacuum, + this.stats, + this._refreshUI, + ]; + return PlacesDBUtils.runTasks(tasks); + }, + + /** + * Forces a full refresh of Places views. + * + * Note: although this function isn't actually async, we keep it async to + * allow us to maintain a simple, consistent API for the tasks within this object. + * + * @returns {Array} An empty array. + */ + async _refreshUI() { + PlacesObservers.notifyListeners([new PlacesPurgeCaches()]); + return []; + }, + + /** + * Checks integrity and tries to fix the database through a reindex. + * + * @return {Promise} resolves if database is sane or is made sane. + * @resolves to an array of logs for this task. + * @rejects if we're unable to fix corruption or unable to check status. + */ + async checkIntegrity() { + let logs = []; + + async function check(dbName) { + try { + await integrity(dbName); + logs.push(`The ${dbName} database is sane`); + } catch (ex) { + PlacesDBUtils.clearPendingTasks(); + if (ex.result == Cr.NS_ERROR_FILE_CORRUPTED) { + logs.push(`The ${dbName} database is corrupt`); + Services.prefs.setCharPref( + "places.database.replaceDatabaseOnStartup", + dbName + ); + throw new Error( + `Unable to fix corruption, ${dbName} will be replaced on next startup` + ); + } + throw new Error(`Unable to check ${dbName} integrity: ${ex}`); + } + } + + await check("places.sqlite"); + await check("favicons.sqlite"); + + return logs; + }, + + /** + * Checks data coherence and tries to fix most common errors. + * + * @return {Promise} resolves when coherence is checked. + * @resolves to an array of logs for this task. + * @rejects if database is not coherent. + */ + async checkCoherence() { + let logs = []; + let stmts = await PlacesDBUtils._getCoherenceStatements(); + let coherenceCheck = true; + await lazy.PlacesUtils.withConnectionWrapper( + "PlacesDBUtils: coherence check:", + db => + db.executeTransaction(async () => { + for (let { query, params } of stmts) { + try { + await db.execute(query, params || null); + } catch (ex) { + console.error(ex); + coherenceCheck = false; + } + } + }) + ); + + if (coherenceCheck) { + logs.push("The database is coherent"); + } else { + PlacesDBUtils.clearPendingTasks(); + throw new Error("Unable to complete the coherence check"); + } + return logs; + }, + + /** + * Runs incremental vacuum on databases supporting it. + * + * @return {Promise} resolves when done. + * @resolves to an array of logs for this task. + * @rejects if we were unable to vacuum. + */ + async incrementalVacuum() { + let logs = []; + return lazy.PlacesUtils.withConnectionWrapper( + "PlacesDBUtils: incrementalVacuum", + async db => { + let count = ( + await db.execute("PRAGMA favicons.freelist_count") + )[0].getResultByIndex(0); + if (count < 10) { + logs.push( + `The favicons database has only ${count} free pages, not vacuuming.` + ); + } else { + logs.push( + `The favicons database has ${count} free pages, vacuuming.` + ); + await db.execute("PRAGMA favicons.incremental_vacuum"); + count = ( + await db.execute("PRAGMA favicons.freelist_count") + )[0].getResultByIndex(0); + logs.push( + `The database has been vacuumed and has now ${count} free pages.` + ); + } + return logs; + } + ).catch(ex => { + PlacesDBUtils.clearPendingTasks(); + throw new Error( + "Unable to incrementally vacuum the favicons database " + ex + ); + }); + }, + + /** + * Expire orphan previews that don't have a Places entry anymore. + * + * @return {Promise} resolves when done. + * @resolves to an array of logs for this task. + */ + async deleteOrphanPreviews() { + let logs = []; + try { + let deleted = await lazy.PlacesPreviews.deleteOrphans(); + if (deleted) { + logs.push(`Orphan previews deleted.`); + } + } catch (ex) { + throw new Error("Unable to delete orphan previews " + ex); + } + return logs; + }, + + async _getCoherenceStatements() { + let cleanupStatements = [ + // MOZ_PLACES + // L.1 remove duplicate URLs. + // This task uses a temp table of potential dupes, and a trigger to remove + // them. It runs first because it relies on subsequent tasks to clean up + // orphaned foreign key references. The task works like this: first, we + // insert all rows with the same hash into the temp table. This lets + // SQLite use the `url_hash` index for scanning `moz_places`. Hashes + // aren't unique, so two different URLs might have the same hash. To find + // the actual dupes, we use a unique constraint on the URL in the temp + // table. If that fails, we bump the dupe count. Then, we delete all dupes + // from the table. This fires the cleanup trigger, which updates all + // foreign key references to point to one of the duplicate Places, then + // deletes the others. + { + query: `CREATE TEMP TABLE IF NOT EXISTS moz_places_dupes_temp( + id INTEGER PRIMARY KEY + , hash INTEGER NOT NULL + , url TEXT UNIQUE NOT NULL + , count INTEGER NOT NULL DEFAULT 0 + )`, + }, + { + query: `CREATE TEMP TRIGGER IF NOT EXISTS moz_places_remove_dupes_temp_trigger + AFTER DELETE ON moz_places_dupes_temp + FOR EACH ROW + BEGIN + /* Reassign history visits. */ + UPDATE moz_historyvisits SET + place_id = OLD.id + WHERE place_id IN (SELECT id FROM moz_places + WHERE id <> OLD.id AND + url_hash = OLD.hash AND + url = OLD.url); + + /* Merge autocomplete history entries. */ + INSERT INTO moz_inputhistory(place_id, input, use_count) + SELECT OLD.id, a.input, a.use_count + FROM moz_inputhistory a + JOIN moz_places h ON h.id = a.place_id + WHERE h.id <> OLD.id AND + h.url_hash = OLD.hash AND + h.url = OLD.url + ON CONFLICT(place_id, input) DO UPDATE SET + place_id = excluded.place_id, + use_count = use_count + excluded.use_count; + + /* Merge page annos, ignoring annos with the same name that are + already set on the destination. */ + INSERT OR IGNORE INTO moz_annos(id, place_id, anno_attribute_id, + content, flags, expiration, type, + dateAdded, lastModified) + SELECT (SELECT k.id FROM moz_annos k + WHERE k.place_id = OLD.id AND + k.anno_attribute_id = a.anno_attribute_id), OLD.id, + a.anno_attribute_id, a.content, a.flags, a.expiration, a.type, + a.dateAdded, a.lastModified + FROM moz_annos a + JOIN moz_places h ON h.id = a.place_id + WHERE h.id <> OLD.id AND + url_hash = OLD.hash AND + url = OLD.url; + + /* Reassign bookmarks, and bump the Sync change counter just in case + we have new keywords. */ + UPDATE moz_bookmarks SET + fk = OLD.id, + syncChangeCounter = syncChangeCounter + 1 + WHERE fk IN (SELECT id FROM moz_places + WHERE url_hash = OLD.hash AND + url = OLD.url); + + /* Reassign keywords. */ + UPDATE moz_keywords SET + place_id = OLD.id + WHERE place_id IN (SELECT id FROM moz_places + WHERE id <> OLD.id AND + url_hash = OLD.hash AND + url = OLD.url); + + /* Now that we've updated foreign key references, drop the + conflicting source. */ + DELETE FROM moz_places + WHERE id <> OLD.id AND + url_hash = OLD.hash AND + url = OLD.url; + + /* Recalculate frecency for the destination. */ + UPDATE moz_places SET recalc_frecency = 1, recalc_alt_frecency = 1 + WHERE id = OLD.id; + END`, + }, + { + query: `INSERT INTO moz_places_dupes_temp(id, hash, url, count) + SELECT h.id, h.url_hash, h.url, 1 + FROM moz_places h + JOIN (SELECT url_hash FROM moz_places + GROUP BY url_hash + HAVING count(*) > 1) d ON d.url_hash = h.url_hash + ON CONFLICT(url) DO UPDATE SET + count = count + 1`, + }, + { query: `DELETE FROM moz_places_dupes_temp WHERE count > 1` }, + { query: `DROP TABLE moz_places_dupes_temp` }, + + // 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. + { + query: `DELETE FROM moz_annos + WHERE type = 4 OR anno_attribute_id IN ( + SELECT id FROM moz_anno_attributes + WHERE name = 'downloads/destinationFileName' OR + name BETWEEN 'weave/' AND 'weave0' + )`, + }, + + // A.3 remove unused attributes. + { + query: `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) + )`, + }, + + // MOZ_ANNOS + // B.1 remove annos with an invalid attribute + { + query: `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) + )`, + }, + + // B.2 remove orphan annos + { + query: `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) + )`, + }, + + // D.1 remove items that are not uri bookmarks from tag containers + { + query: `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 + )`, + params: { + tags_folder: lazy.PlacesUtils.tagsFolderId, + bookmark_type: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK, + rootGuid: lazy.PlacesUtils.bookmarks.rootGuid, + menuGuid: lazy.PlacesUtils.bookmarks.menuGuid, + toolbarGuid: lazy.PlacesUtils.bookmarks.toolbarGuid, + unfiledGuid: lazy.PlacesUtils.bookmarks.unfiledGuid, + tagsGuid: lazy.PlacesUtils.bookmarks.tagsGuid, + }, + }, + + // D.2 remove empty tags + { + query: `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) + )`, + params: { + tags_folder: lazy.PlacesUtils.tagsFolderId, + rootGuid: lazy.PlacesUtils.bookmarks.rootGuid, + menuGuid: lazy.PlacesUtils.bookmarks.menuGuid, + toolbarGuid: lazy.PlacesUtils.bookmarks.toolbarGuid, + unfiledGuid: lazy.PlacesUtils.bookmarks.unfiledGuid, + tagsGuid: lazy.PlacesUtils.bookmarks.tagsGuid, + }, + }, + + // D.3 move orphan items to unsorted folder + { + query: `UPDATE moz_bookmarks SET + parent = (SELECT id FROM moz_bookmarks WHERE guid = :unfiledGuid) + 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) + )`, + params: { + rootGuid: lazy.PlacesUtils.bookmarks.rootGuid, + menuGuid: lazy.PlacesUtils.bookmarks.menuGuid, + toolbarGuid: lazy.PlacesUtils.bookmarks.toolbarGuid, + unfiledGuid: lazy.PlacesUtils.bookmarks.unfiledGuid, + tagsGuid: lazy.PlacesUtils.bookmarks.tagsGuid, + }, + }, + + // D.4 Insert tombstones for any synced items with the wrong type. + // Sync doesn't support changing the type of an existing item while + // keeping its GUID. To avoid confusing other clients, we insert + // tombstones for all synced items with the wrong type, so that we + // can reupload them with the correct type and a new GUID. + { + query: `INSERT OR IGNORE INTO moz_bookmarks_deleted(guid, dateRemoved) + SELECT guid, :dateRemoved + FROM moz_bookmarks + WHERE syncStatus <> :syncStatus AND + ((type IN (:folder_type, :separator_type) AND + fk NOTNULL) OR + (type = :bookmark_type AND + fk IS NULL) OR + type IS NULL)`, + params: { + dateRemoved: lazy.PlacesUtils.toPRTime(new Date()), + syncStatus: lazy.PlacesUtils.bookmarks.SYNC_STATUS.NEW, + bookmark_type: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK, + folder_type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER, + separator_type: lazy.PlacesUtils.bookmarks.TYPE_SEPARATOR, + }, + }, + + // D.5 fix wrong item types + // Folders and separators should not have an fk. + // If they have a valid fk, convert them to bookmarks, and give them new + // GUIDs. If the item has children, we'll move them to the unfiled root + // in D.8. If the `fk` doesn't exist in `moz_places`, we'll remove the + // item in D.9. + { + query: `UPDATE moz_bookmarks + SET guid = GENERATE_GUID(), + 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 + )`, + params: { + bookmark_type: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK, + folder_type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER, + separator_type: lazy.PlacesUtils.bookmarks.TYPE_SEPARATOR, + rootGuid: lazy.PlacesUtils.bookmarks.rootGuid, + menuGuid: lazy.PlacesUtils.bookmarks.menuGuid, + toolbarGuid: lazy.PlacesUtils.bookmarks.toolbarGuid, + unfiledGuid: lazy.PlacesUtils.bookmarks.unfiledGuid, + tagsGuid: lazy.PlacesUtils.bookmarks.tagsGuid, + }, + }, + + // D.6 fix wrong item types + // Bookmarks should have an fk, if they don't have any, convert them to + // folders. + { + query: `UPDATE moz_bookmarks + SET guid = GENERATE_GUID(), + 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 + )`, + params: { + bookmark_type: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK, + folder_type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER, + rootGuid: lazy.PlacesUtils.bookmarks.rootGuid, + menuGuid: lazy.PlacesUtils.bookmarks.menuGuid, + toolbarGuid: lazy.PlacesUtils.bookmarks.toolbarGuid, + unfiledGuid: lazy.PlacesUtils.bookmarks.unfiledGuid, + tagsGuid: lazy.PlacesUtils.bookmarks.tagsGuid, + }, + }, + + // D.7 fix wrong item types + // `moz_bookmarks.type` doesn't have a NOT NULL constraint, so it's + // possible for an item to not have a type (bug 1586427). + { + query: `UPDATE moz_bookmarks + SET guid = GENERATE_GUID(), + type = CASE WHEN fk NOT NULL THEN :bookmark_type ELSE :folder_type END + WHERE guid NOT IN ( + :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ + ) AND type IS NULL`, + params: { + bookmark_type: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK, + folder_type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER, + rootGuid: lazy.PlacesUtils.bookmarks.rootGuid, + menuGuid: lazy.PlacesUtils.bookmarks.menuGuid, + toolbarGuid: lazy.PlacesUtils.bookmarks.toolbarGuid, + unfiledGuid: lazy.PlacesUtils.bookmarks.unfiledGuid, + tagsGuid: lazy.PlacesUtils.bookmarks.tagsGuid, + }, + }, + + // D.8 fix wrong parents + // Items cannot have separators or other bookmarks + // as parent, if they have bad parent move them to unsorted bookmarks. + { + query: `UPDATE moz_bookmarks SET + parent = (SELECT id FROM moz_bookmarks WHERE guid = :unfiledGuid) + 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) + )`, + params: { + bookmark_type: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK, + separator_type: lazy.PlacesUtils.bookmarks.TYPE_SEPARATOR, + rootGuid: lazy.PlacesUtils.bookmarks.rootGuid, + menuGuid: lazy.PlacesUtils.bookmarks.menuGuid, + toolbarGuid: lazy.PlacesUtils.bookmarks.toolbarGuid, + unfiledGuid: lazy.PlacesUtils.bookmarks.unfiledGuid, + tagsGuid: lazy.PlacesUtils.bookmarks.tagsGuid, + }, + }, + + // D.9 remove items without a valid place + // We've already converted folders with an `fk` to bookmarks in D.5, + // and bookmarks without an `fk` to folders in D.6. However, the `fk` + // might not reference an existing `moz_places.id`, even if it's + // NOT NULL. This statement takes care of those. + { + query: `DELETE FROM moz_bookmarks AS b + WHERE b.guid NOT IN ( + :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ + ) AND b.fk NOT NULL + AND b.type = :bookmark_type + AND NOT EXISTS (SELECT 1 FROM moz_places h WHERE h.id = b.fk)`, + params: { + bookmark_type: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK, + rootGuid: lazy.PlacesUtils.bookmarks.rootGuid, + menuGuid: lazy.PlacesUtils.bookmarks.menuGuid, + toolbarGuid: lazy.PlacesUtils.bookmarks.toolbarGuid, + unfiledGuid: lazy.PlacesUtils.bookmarks.unfiledGuid, + tagsGuid: lazy.PlacesUtils.bookmarks.tagsGuid, + }, + }, + + // D.10 fix non-consecutive positions. + { + query: ` + WITH positions(item_id, pos, seq) AS ( + SELECT id, position AS pos, + (row_number() OVER (PARTITION BY parent ORDER BY position)) - 1 AS seq + FROM moz_bookmarks + ) + UPDATE moz_bookmarks + SET position = seq + FROM positions + WHERE item_id = moz_bookmarks.id AND seq <> pos`, + }, + + // D.12 Fix empty-named tags. + // Tags were allowed to have empty names due to a UI bug. Fix them by + // replacing their title with "(notitle)", and bumping the change counter + // for all bookmarks with the fixed tags. + { + query: `UPDATE moz_bookmarks SET syncChangeCounter = syncChangeCounter + 1 + WHERE fk IN (SELECT b.fk FROM moz_bookmarks b + JOIN moz_bookmarks p ON p.id = b.parent + WHERE length(p.title) = 0 AND p.type = :folder_type AND + p.parent = :tags_folder)`, + params: { + folder_type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER, + tags_folder: lazy.PlacesUtils.tagsFolderId, + }, + }, + { + query: `UPDATE moz_bookmarks SET title = :empty_title + WHERE length(title) = 0 AND type = :folder_type + AND parent = :tags_folder`, + params: { + empty_title: "(notitle)", + folder_type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER, + tags_folder: lazy.PlacesUtils.tagsFolderId, + }, + }, + + // MOZ_ICONS + // E.1 remove orphan icon entries. + { + query: `DELETE FROM moz_pages_w_icons WHERE page_url_hash NOT IN ( + SELECT url_hash FROM moz_places + )`, + }, + + // Remove icons whose origin is not in moz_origins, unless referenced. + { + query: `DELETE FROM moz_icons WHERE id IN ( + SELECT id FROM moz_icons WHERE root = 0 + UNION ALL + SELECT id FROM moz_icons + WHERE root = 1 + AND get_host_and_port(icon_url) NOT IN (SELECT host FROM moz_origins) + AND fixup_url(get_host_and_port(icon_url)) NOT IN (SELECT host FROM moz_origins) + EXCEPT + SELECT icon_id FROM moz_icons_to_pages + )`, + }, + + // MOZ_HISTORYVISITS + // F.1 remove orphan visits + { + query: `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) + )`, + }, + + // MOZ_INPUTHISTORY + // G.1 remove orphan input history + { + query: `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) + )`, + }, + + // MOZ_KEYWORDS + // I.1 remove unused keywords + { + query: `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) + )`, + }, + + // MOZ_PLACES + // L.2 recalculate visit_count and last_visit_date + { + query: `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,9)), + 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,9)) + OR last_visit_date IS NOT + (SELECT MAX(visit_date) FROM moz_historyvisits v WHERE v.place_id = h.id) + )`, + }, + + // L.3 recalculate hidden for redirects. + { + query: `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 + )`, + }, + + // L.4 recalculate foreign_count. + { + query: `UPDATE moz_places SET foreign_count = + (SELECT count(*) FROM moz_bookmarks WHERE fk = moz_places.id ) + + (SELECT count(*) FROM moz_keywords WHERE place_id = moz_places.id )`, + }, + + // L.5 recalculate missing hashes. + { + query: `UPDATE moz_places SET url_hash = hash(url) WHERE url_hash = 0`, + }, + + // L.6 fix invalid Place GUIDs. + { + query: `UPDATE moz_places + SET guid = GENERATE_GUID() + WHERE guid IS NULL OR + NOT IS_VALID_GUID(guid)`, + }, + + // MOZ_BOOKMARKS + // S.1 fix invalid GUIDs for synced bookmarks. + // This requires multiple related statements. + // First, we insert tombstones for all synced bookmarks with invalid + // GUIDs, so that we can delete them on the server. Second, we add a + // temporary trigger to bump the change counter for the parents of any + // items we update, since Sync stores the list of child GUIDs on the + // parent. Finally, we assign new GUIDs for all items with missing and + // invalid GUIDs, bump their change counters, and reset their sync + // statuses to NEW so that they're considered for deduping. + { + query: `INSERT OR IGNORE INTO moz_bookmarks_deleted(guid, dateRemoved) + SELECT guid, :dateRemoved + FROM moz_bookmarks + WHERE syncStatus <> :syncStatus AND + guid NOT NULL AND + NOT IS_VALID_GUID(guid)`, + params: { + dateRemoved: lazy.PlacesUtils.toPRTime(new Date()), + syncStatus: lazy.PlacesUtils.bookmarks.SYNC_STATUS.NEW, + }, + }, + { + query: `UPDATE moz_bookmarks + SET guid = GENERATE_GUID(), + syncStatus = :syncStatus + WHERE guid IS NULL OR + NOT IS_VALID_GUID(guid)`, + params: { + syncStatus: lazy.PlacesUtils.bookmarks.SYNC_STATUS.NEW, + }, + }, + + // S.2 drop tombstones for bookmarks that aren't deleted. + { + query: `DELETE FROM moz_bookmarks_deleted + WHERE guid IN (SELECT guid FROM moz_bookmarks)`, + }, + + // S.3 set missing added and last modified dates. + { + query: `UPDATE moz_bookmarks + SET dateAdded = COALESCE(NULLIF(dateAdded, 0), NULLIF(lastModified, 0), NULLIF(( + SELECT MIN(visit_date) FROM moz_historyvisits + WHERE place_id = fk + ), 0), STRFTIME('%s', 'now', 'localtime', 'utc') * 1000000), + lastModified = COALESCE(NULLIF(lastModified, 0), NULLIF(dateAdded, 0), NULLIF(( + SELECT MAX(visit_date) FROM moz_historyvisits + WHERE place_id = fk + ), 0), STRFTIME('%s', 'now', 'localtime', 'utc') * 1000000) + WHERE NULLIF(dateAdded, 0) IS NULL OR + NULLIF(lastModified, 0) IS NULL`, + }, + + // S.4 reset added dates that are ahead of last modified dates. + { + query: `UPDATE moz_bookmarks + SET dateAdded = lastModified + WHERE dateAdded > lastModified`, + }, + ]; + + // Create triggers for updating Sync metadata. The "sync change" trigger + // bumps the parent's change counter when we update a GUID or move an item + // to a different folder, since Sync stores the list of child GUIDs on the + // parent. The "sync tombstone" trigger inserts tombstones for deleted + // synced bookmarks. + cleanupStatements.unshift({ + query: `CREATE TEMP TRIGGER IF NOT EXISTS moz_bm_sync_change_temp_trigger + AFTER UPDATE OF guid, parent, position ON moz_bookmarks + FOR EACH ROW + BEGIN + UPDATE moz_bookmarks + SET syncChangeCounter = syncChangeCounter + 1 + WHERE id IN (OLD.parent, NEW.parent, NEW.id); + END`, + }); + cleanupStatements.unshift({ + query: `CREATE TEMP TRIGGER IF NOT EXISTS moz_bm_sync_tombstone_temp_trigger + AFTER DELETE ON moz_bookmarks + FOR EACH ROW WHEN OLD.guid NOT NULL AND + OLD.syncStatus <> 1 + BEGIN + UPDATE moz_bookmarks + SET syncChangeCounter = syncChangeCounter + 1 + WHERE id = OLD.parent; + + INSERT INTO moz_bookmarks_deleted(guid, dateRemoved) + VALUES(OLD.guid, STRFTIME('%s', 'now', 'localtime', 'utc') * 1000000); + END`, + }); + cleanupStatements.push({ + query: `DROP TRIGGER moz_bm_sync_change_temp_trigger`, + }); + cleanupStatements.push({ + query: `DROP TRIGGER moz_bm_sync_tombstone_temp_trigger`, + }); + + return cleanupStatements; + }, + + /** + * Tries to vacuum the database. + * + * Note: although this function isn't actually async, we keep it async to + * allow us to maintain a simple, consistent API for the tasks within this object. + * + * @return {Promise} resolves when database is vacuumed. + * @resolves to an array of logs for this task. + * @rejects if we are unable to vacuum database. + */ + async vacuum() { + let logs = []; + let placesDbPath = PathUtils.join(PathUtils.profileDir, "places.sqlite"); + let info = await IOUtils.stat(placesDbPath); + logs.push(`Initial database size is ${parseInt(info.size / 1024)}KiB`); + return lazy.PlacesUtils.withConnectionWrapper( + "PlacesDBUtils: vacuum", + async db => { + await db.execute("VACUUM"); + logs.push("The database has been vacuumed"); + info = await IOUtils.stat(placesDbPath); + logs.push(`Final database size is ${parseInt(info.size / 1024)}KiB`); + return logs; + } + ).catch(() => { + PlacesDBUtils.clearPendingTasks(); + throw new Error("Unable to vacuum database"); + }); + }, + + /** + * Forces a full expiration on the database. + * + * Note: although this function isn't actually async, we keep it async to + * allow us to maintain a simple, consistent API for the tasks within this object. + * + * @return {Promise} resolves when the database in cleaned up. + * @resolves to an array of logs for this task. + */ + async expire() { + let logs = []; + + let expiration = Cc["@mozilla.org/places/expiration;1"].getService( + Ci.nsIObserver + ); + + let returnPromise = new Promise(res => { + let observer = (subject, topic, data) => { + Services.obs.removeObserver(observer, topic); + logs.push("Database cleaned up"); + res(logs); + }; + Services.obs.addObserver( + observer, + lazy.PlacesUtils.TOPIC_EXPIRATION_FINISHED + ); + }); + + // Force an orphans expiration step. + expiration.observe(null, "places-debug-start-expiration", 0); + return returnPromise; + }, + + /** + * Collects statistical data on the database. + * + * @return {Promise} resolves when statistics are collected. + * @resolves to an array of logs for this task. + * @rejects if we are unable to collect stats for some reason. + */ + async stats() { + let logs = []; + let placesDbPath = PathUtils.join(PathUtils.profileDir, "places.sqlite"); + let info = await IOUtils.stat(placesDbPath); + logs.push(`Places.sqlite size is ${parseInt(info.size / 1024)}KiB`); + let faviconsDbPath = PathUtils.join( + PathUtils.profileDir, + "favicons.sqlite" + ); + info = await IOUtils.stat(faviconsDbPath); + logs.push(`Favicons.sqlite size is ${parseInt(info.size / 1024)}KiB`); + + // Execute each step async. + let pragmas = [ + "user_version", + "page_size", + "cache_size", + "journal_mode", + "synchronous", + ].map(p => `pragma_${p}`); + let pragmaQuery = `SELECT * FROM ${pragmas.join(", ")}`; + await lazy.PlacesUtils.withConnectionWrapper( + "PlacesDBUtils: pragma for stats", + async db => { + let row = (await db.execute(pragmaQuery))[0]; + for (let i = 0; i != pragmas.length; i++) { + logs.push(`${pragmas[i]} is ${row.getResultByIndex(i)}`); + } + } + ).catch(() => { + logs.push("Could not set pragma for stat collection"); + }); + + // Get maximum number of unique URIs. + try { + let limitURIs = await Cc["@mozilla.org/places/expiration;1"] + .getService(Ci.nsISupports) + .wrappedJSObject.getPagesLimit(); + logs.push( + "History can store a maximum of " + limitURIs + " unique pages" + ); + } catch (ex) {} + + let query = "SELECT name FROM sqlite_master WHERE type = :type"; + let params = {}; + let _getTableCount = async tableName => { + let db = await lazy.PlacesUtils.promiseDBConnection(); + let rows = await db.execute(`SELECT count(*) FROM ${tableName}`); + logs.push( + `Table ${tableName} has ${rows[0].getResultByIndex(0)} records` + ); + }; + + try { + params.type = "table"; + let db = await lazy.PlacesUtils.promiseDBConnection(); + await db.execute(query, params, r => + _getTableCount(r.getResultByIndex(0)) + ); + } catch (ex) { + throw new Error("Unable to collect stats."); + } + + let details = await PlacesDBUtils.getEntitiesStats(); + logs.push( + `Pages sequentiality: ${details.get("moz_places").sequentialityPerc}` + ); + let entities = Array.from(details.keys()).sort((a, b) => { + return details.get(a).sizePerc - details.get(b).sizePerc; + }); + for (let key of entities) { + let info = details.get(key); + logs.push( + `${key}: ${info.sizeBytes / 1024}KiB (${info.sizePerc}%), ${ + info.efficiencyPerc + }% eff.` + ); + } + + return logs; + }, + + /** + * Collects telemetry data and reports it to Telemetry. + * + * Note: although this function isn't actually async, we keep it async to + * allow us to maintain a simple, consistent API for the tasks within this object. + * + */ + async telemetry() { + // 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. + // + // 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", + query: "SELECT count(*) FROM moz_places", + }, + + { + histogram: "PLACES_BOOKMARKS_COUNT", + 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`, + params: { + tags_folder: lazy.PlacesUtils.tagsFolderId, + type_bookmark: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + }, + + { + histogram: "PLACES_TAGS_COUNT", + query: `SELECT count(*) FROM moz_bookmarks + WHERE parent = :tags_folder`, + params: { + tags_folder: lazy.PlacesUtils.tagsFolderId, + }, + }, + + { + 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 p ON p.id = b.parent + JOIN moz_bookmarks g ON g.id = p.parent + WHERE g.guid <> :root_guid + AND g.guid <> :tags_guid + AND b.type = :type_bookmark + ) * 100 / ( + SELECT count(*) FROM moz_bookmarks b + JOIN moz_bookmarks p ON p.id = b.parent + JOIN moz_bookmarks g ON g.id = p.parent + AND g.guid <> :tags_guid + WHERE b.type = :type_bookmark + )), 0)`, + params: { + root_guid: lazy.PlacesUtils.bookmarks.rootGuid, + tags_guid: lazy.PlacesUtils.bookmarks.tagsGuid, + type_bookmark: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + }, + + { + 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)`, + params: { + tags_folder: lazy.PlacesUtils.tagsFolderId, + type_bookmark: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + }, + + { + histogram: "PLACES_DATABASE_FILESIZE_MB", + async callback() { + let placesDbPath = PathUtils.join( + PathUtils.profileDir, + "places.sqlite" + ); + let info = await IOUtils.stat(placesDbPath); + return parseInt(info.size / BYTES_PER_MEBIBYTE); + }, + }, + + { + histogram: "PLACES_DATABASE_FAVICONS_FILESIZE_MB", + async callback() { + let faviconsDbPath = PathUtils.join( + PathUtils.profileDir, + "favicons.sqlite" + ); + let info = await IOUtils.stat(faviconsDbPath); + return parseInt(info.size / BYTES_PER_MEBIBYTE); + }, + }, + + { + histogram: "PLACES_ANNOS_PAGES_COUNT", + query: "SELECT count(*) FROM moz_annos", + }, + + { + histogram: "PLACES_MAINTENANCE_DAYSFROMLAST", + callback() { + try { + let lastMaintenance = Services.prefs.getIntPref( + "places.database.lastMaintenance" + ); + let nowSeconds = parseInt(Date.now() / 1000); + return parseInt((nowSeconds - lastMaintenance) / 86400); + } catch (ex) { + return 60; + } + }, + }, + { + scalar: "places.pages_need_frecency_recalculation", + query: "SELECT count(*) FROM moz_places WHERE recalc_frecency = 1", + }, + { + scalar: "places.previousday_visits", + query: `SELECT COUNT(*) from moz_places + WHERE hidden=0 AND last_visit_date < (strftime('%s', 'now', 'start of day') * 1000000) + AND last_visit_date > (strftime('%s', 'now', 'start of day', '-1 day') * 1000000) + AND last_visit_date IS NOT NULL;`, + }, + ]; + + for (let probe of probes) { + let val; + if ("query" in probe) { + let db = await lazy.PlacesUtils.promiseDBConnection(); + val = ( + await db.execute(probe.query, probe.params || {}) + )[0].getResultByIndex(0); + } + // Report the result of the probe through Telemetry. + // The resulting promise cannot reject. + if ("callback" in probe) { + val = await probe.callback(val); + } + probeValues[probe.histogram || probe.scalar] = val; + if (probe.histogram) { + Services.telemetry.getHistogramById(probe.histogram).add(val); + } else if (probe.scalar) { + Services.telemetry.scalarSet(probe.scalar, val); + } else { + throw new Error("Unknwon telemetry probe type"); + } + } + }, + + /** + * Remove old and useless places.sqlite.corrupt files. + * + * @resolves to an array of logs for this task. + * + */ + async removeOldCorruptDBs() { + let logs = []; + logs.push( + "> Cleanup profile from places.sqlite.corrupt files older than " + + CORRUPT_DB_RETAIN_DAYS + + " days." + ); + let re = /places\.sqlite(-\d)?\.corrupt$/; + let currentTime = Date.now(); + let children = await IOUtils.getChildren(PathUtils.profileDir); + try { + for (let entry of children) { + let fileInfo = await IOUtils.stat(entry); + let lastModificationDate; + if (fileInfo.type == "regular" && re.test(entry)) { + lastModificationDate = fileInfo.lastModified; + try { + // Convert milliseconds to days. + let days = Math.ceil( + (currentTime - lastModificationDate) / MS_PER_DAY + ); + if (days >= CORRUPT_DB_RETAIN_DAYS || days < 0) { + await IOUtils.remove(entry); + } + } catch (error) { + logs.push("Could not remove file: " + entry, error); + } + } + } + } catch (error) { + logs.push("removeOldCorruptDBs failed", error); + } + return logs; + }, + + /** + * Gets detailed statistics about database entities like tables and indices. + * @returns {Map} a Map by table name, containing an object with the following + * properties: + * - efficiencyPerc: percentage filling of pages, an high + * efficiency means most pages are filled up almost completely. + * This value is not particularly useful with a low number of + * pages. + * - sizeBytes: size of the entity in bytes + * - pages: number of pages of the entity + * - sizePerc: percentage of the total database size + * - sequentialityPerc: percentage of sequential pages, this is + * a global value of the database, thus it's the same for every + * entity, and it can be used to evaluate fragmentation and the + * need for vacuum. + */ + async getEntitiesStats() { + let db = await lazy.PlacesUtils.promiseDBConnection(); + let rows = await db.execute(` + /* do not warn (bug no): no need for index */ + SELECT name, + round((pgsize - unused) * 100.0 / pgsize, 1) as efficiency_perc, + pgsize as size_bytes, pageno as pages, + round(pgsize * 100.0 / (SELECT sum(pgsize) FROM dbstat WHERE aggregate = TRUE), 1) as size_perc, + round(( + WITH s(row, pageno) AS ( + SELECT row_number() OVER (ORDER BY path), pageno FROM dbstat ORDER BY path + ) + SELECT sum(s1.pageno+1==s2.pageno)*100.0/count(*) + FROM s AS s1, s AS s2 + WHERE s1.row+1=s2.row + ), 1) AS sequentiality_perc + FROM dbstat + WHERE aggregate = TRUE + `); + let entitiesByName = new Map(); + for (let row of rows) { + let details = { + efficiencyPerc: row.getResultByName("efficiency_perc"), + pages: row.getResultByName("pages"), + sizeBytes: row.getResultByName("size_bytes"), + sizePerc: row.getResultByName("size_perc"), + sequentialityPerc: row.getResultByName("sequentiality_perc"), + }; + entitiesByName.set(row.getResultByName("name"), details); + } + return entitiesByName; + }, + + /** + * Gets detailed statistics about database entities and their respective row + * counts. + * @returns {Array} An array that augments each object returned by + * {@link getEntitiesStats} with the following extra properties: + * - entity: name of the entity + * - count: row count of the entity + */ + async getEntitiesStatsAndCounts() { + let stats = await PlacesDBUtils.getEntitiesStats(); + let data = []; + let db = await lazy.PlacesUtils.promiseDBConnection(); + for (let [entity, value] of stats) { + let count = "-"; + try { + if ( + entity.startsWith("moz_") && + !entity.endsWith("index") && + entity != "moz_places_visitcount" /* bug in index name */ + ) { + count = ( + await db.execute(`SELECT count(*) FROM ${entity}`) + )[0].getResultByIndex(0); + } + } catch (ex) { + console.error(ex); + } + data.push(Object.assign(value, { entity, count })); + } + return data; + }, + + /** + * Runs a list of tasks, returning a Map when done. + * + * @param tasks + * Array of tasks to be executed, in form of pointers to methods in + * this module. + * @return {Promise} + * A promise that resolves with a Map[taskName(String) -> Object]. + * The Object has the following properties: + * - succeeded: boolean + * - logs: an array of strings containing the messages logged by the task + */ + async runTasks(tasks) { + if (!this._registeredShutdownObserver) { + this._registeredShutdownObserver = true; + lazy.PlacesUtils.registerShutdownFunction(() => { + this._isShuttingDown = true; + }); + } + PlacesDBUtils._clearTaskQueue = false; + let tasksMap = new Map(); + for (let task of tasks) { + if (PlacesDBUtils._isShuttingDown) { + tasksMap.set(task.name, { + succeeded: false, + logs: ["Shutting down, will not schedule the task."], + }); + continue; + } + + if (PlacesDBUtils._clearTaskQueue) { + tasksMap.set(task.name, { + succeeded: false, + logs: ["The task queue was cleared by an error in another task."], + }); + continue; + } + + let result = await task() + .then((logs = [`${task.name} complete`]) => ({ succeeded: true, logs })) + .catch(err => ({ succeeded: false, logs: [err.message] })); + tasksMap.set(task.name, result); + } + return tasksMap; + }, +}; + +async function integrity(dbName) { + async function check(db) { + let row; + await db.execute("PRAGMA integrity_check", null, (r, cancel) => { + row = r; + cancel(); + }); + return row.getResultByIndex(0) === "ok"; + } + + // Create a new connection for this check, so we can operate independently + // from a broken Places service. + // openConnection returns an exception with .result == Cr.NS_ERROR_FILE_CORRUPTED, + // we should do the same everywhere we want maintenance to try replacing the + // database on next startup. + let path = PathUtils.join(PathUtils.profileDir, dbName); + let db = await lazy.Sqlite.openConnection({ path }); + try { + if (await check(db)) { + return; + } + + // We stopped due to an integrity corruption, try to fix it if possible. + // First, try to reindex, this often fixes simple indices problems. + try { + await db.execute("REINDEX"); + } catch (ex) { + throw new Components.Exception( + "Impossible to reindex database", + Cr.NS_ERROR_FILE_CORRUPTED + ); + } + + // Check again. + if (!(await check(db))) { + throw new Components.Exception( + "The database is still corrupt", + Cr.NS_ERROR_FILE_CORRUPTED + ); + } + } finally { + await db.close(); + } +} + +export function PlacesDBUtilsIdleMaintenance() {} + +PlacesDBUtilsIdleMaintenance.prototype = { + observe(subject, topic, data) { + switch (topic) { + case "idle-daily": + // Once a week run places.sqlite maintenance tasks. + let lastMaintenance = Services.prefs.getIntPref( + "places.database.lastMaintenance", + 0 + ); + let nowSeconds = parseInt(Date.now() / 1000); + if (lastMaintenance < nowSeconds - MAINTENANCE_INTERVAL_SECONDS) { + PlacesDBUtils.maintenanceOnIdle(); + } + break; + default: + throw new Error("Trying to handle an unknown category."); + } + }, + classID: Components.ID("d38926e0-29c1-11eb-8588-0800200c9a66"), + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), +}; |