diff options
Diffstat (limited to 'toolkit/components/contentprefs')
41 files changed, 6508 insertions, 0 deletions
diff --git a/toolkit/components/contentprefs/ContentPrefInstance.jsm b/toolkit/components/contentprefs/ContentPrefInstance.jsm new file mode 100644 index 000000000..395569995 --- /dev/null +++ b/toolkit/components/contentprefs/ContentPrefInstance.jsm @@ -0,0 +1,75 @@ +/* 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/. */ + +'use strict'; + +const Cc = Components.classes; +const Ci = Components.interfaces; + +this.EXPORTED_SYMBOLS = ['ContentPrefInstance']; + +// This is a wrapper for nsIContentPrefService that alleviates the need to pass +// an nsILoadContext argument to every method. Pass the context to the constructor +// instead and continue on your way in blissful ignorance. + +this.ContentPrefInstance = function ContentPrefInstance(aContext) { + this._contentPrefSvc = Cc["@mozilla.org/content-pref/service;1"]. + getService(Ci.nsIContentPrefService); + this._context = aContext; +}; + +ContentPrefInstance.prototype = { + getPref: function ContentPrefInstance_init(aName, aGroup, aCallback) { + return this._contentPrefSvc.getPref(aName, aGroup, this._context, aCallback); + }, + + setPref: function ContentPrefInstance_setPref(aGroup, aName, aValue, aContext) { + return this._contentPrefSvc.setPref(aGroup, aName, aValue, + aContext ? aContext : this._context); + }, + + hasPref: function ContentPrefInstance_hasPref(aGroup, aName) { + return this._contentPrefSvc.hasPref(aGroup, aName, this._context); + }, + + hasCachedPref: function ContentPrefInstance_hasCachedPref(aGroup, aName) { + return this._contentPrefSvc.hasCachedPref(aGroup, aName, this._context); + }, + + removePref: function ContentPrefInstance_removePref(aGroup, aName) { + return this._contentPrefSvc.removePref(aGroup, aName, this._context); + }, + + removeGroupedPrefs: function ContentPrefInstance_removeGroupedPrefs() { + return this._contentPrefSvc.removeGroupedPrefs(this._context); + }, + + removePrefsByName: function ContentPrefInstance_removePrefsByName(aName) { + return this._contentPrefSvc.removePrefsByName(aName, this._context); + }, + + getPrefs: function ContentPrefInstance_getPrefs(aGroup) { + return this._contentPrefSvc.getPrefs(aGroup, this._context); + }, + + getPrefsByName: function ContentPrefInstance_getPrefsByName(aName) { + return this._contentPrefSvc.getPrefsByName(aName, this._context); + }, + + addObserver: function ContentPrefInstance_addObserver(aName, aObserver) { + return this._contentPrefSvc.addObserver(aName, aObserver); + }, + + removeObserver: function ContentPrefInstance_removeObserver(aName, aObserver) { + return this._contentPrefSvc.removeObserver(aName, aObserver); + }, + + get grouper() { + return this._contentPrefSvc.grouper; + }, + + get DBConnection() { + return this._contentPrefSvc.DBConnection; + } +}; diff --git a/toolkit/components/contentprefs/ContentPrefService2.jsm b/toolkit/components/contentprefs/ContentPrefService2.jsm new file mode 100644 index 000000000..87063d170 --- /dev/null +++ b/toolkit/components/contentprefs/ContentPrefService2.jsm @@ -0,0 +1,885 @@ +/* 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/. */ + +// This file is an XPCOM component that implements nsIContentPrefService2. +// Although it's a JSM, it's not intended to be imported by consumers like JSMs +// are usually imported. It's only a JSM so that nsContentPrefService.js can +// easily use it. Consumers should access this component with the usual XPCOM +// rigmarole: +// +// Cc["@mozilla.org/content-pref/service;1"]. +// getService(Ci.nsIContentPrefService2); +// +// That contract ID actually belongs to nsContentPrefService.js, which, when +// QI'ed to nsIContentPrefService2, returns an instance of this component. +// +// The plan is to eventually remove nsIContentPrefService and its +// implementation, nsContentPrefService.js. At such time this file can stop +// being a JSM, and the "_cps" parts that ContentPrefService2 relies on and +// NSGetFactory and all the other XPCOM initialization goop in +// nsContentPrefService.js can be moved here. +// +// See https://bugzilla.mozilla.org/show_bug.cgi?id=699859 + +var EXPORTED_SYMBOLS = [ + "ContentPrefService2", +]; + +const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/ContentPrefUtils.jsm"); +Cu.import("resource://gre/modules/ContentPrefStore.jsm"); + +const GROUP_CLAUSE = ` + SELECT id + FROM groups + WHERE name = :group OR + (:includeSubdomains AND name LIKE :pattern ESCAPE '/') +`; + +function ContentPrefService2(cps) { + this._cps = cps; + this._cache = cps._cache; + this._pbStore = cps._privModeStorage; +} + +ContentPrefService2.prototype = { + + getByName: function CPS2_getByName(name, context, callback) { + checkNameArg(name); + checkCallbackArg(callback, true); + + // Some prefs may be in both the database and the private browsing store. + // Notify the caller of such prefs only once, using the values from private + // browsing. + let pbPrefs = new ContentPrefStore(); + if (context && context.usePrivateBrowsing) { + for (let [sgroup, sname, val] of this._pbStore) { + if (sname == name) { + pbPrefs.set(sgroup, sname, val); + } + } + } + + let stmt1 = this._stmt(` + SELECT groups.name AS grp, prefs.value AS value + FROM prefs + JOIN settings ON settings.id = prefs.settingID + JOIN groups ON groups.id = prefs.groupID + WHERE settings.name = :name + `); + stmt1.params.name = name; + + let stmt2 = this._stmt(` + SELECT NULL AS grp, prefs.value AS value + FROM prefs + JOIN settings ON settings.id = prefs.settingID + WHERE settings.name = :name AND prefs.groupID ISNULL + `); + stmt2.params.name = name; + + this._execStmts([stmt1, stmt2], { + onRow: function onRow(row) { + let grp = row.getResultByName("grp"); + let val = row.getResultByName("value"); + this._cache.set(grp, name, val); + if (!pbPrefs.has(grp, name)) + cbHandleResult(callback, new ContentPref(grp, name, val)); + }, + onDone: function onDone(reason, ok, gotRow) { + if (ok) { + for (let [pbGroup, pbName, pbVal] of pbPrefs) { + cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal)); + } + } + cbHandleCompletion(callback, reason); + }, + onError: function onError(nsresult) { + cbHandleError(callback, nsresult); + } + }); + }, + + getByDomainAndName: function CPS2_getByDomainAndName(group, name, context, + callback) { + checkGroupArg(group); + this._get(group, name, false, context, callback); + }, + + getBySubdomainAndName: function CPS2_getBySubdomainAndName(group, name, + context, + callback) { + checkGroupArg(group); + this._get(group, name, true, context, callback); + }, + + getGlobal: function CPS2_getGlobal(name, context, callback) { + this._get(null, name, false, context, callback); + }, + + _get: function CPS2__get(group, name, includeSubdomains, context, callback) { + group = this._parseGroup(group); + checkNameArg(name); + checkCallbackArg(callback, true); + + // Some prefs may be in both the database and the private browsing store. + // Notify the caller of such prefs only once, using the values from private + // browsing. + let pbPrefs = new ContentPrefStore(); + if (context && context.usePrivateBrowsing) { + for (let [sgroup, val] of + this._pbStore.match(group, name, includeSubdomains)) { + pbPrefs.set(sgroup, name, val); + } + } + + this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], { + onRow: function onRow(row) { + let grp = row.getResultByName("grp"); + let val = row.getResultByName("value"); + this._cache.set(grp, name, val); + if (!pbPrefs.has(group, name)) + cbHandleResult(callback, new ContentPref(grp, name, val)); + }, + onDone: function onDone(reason, ok, gotRow) { + if (ok) { + if (!gotRow) + this._cache.set(group, name, undefined); + for (let [pbGroup, pbName, pbVal] of pbPrefs) { + cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal)); + } + } + cbHandleCompletion(callback, reason); + }, + onError: function onError(nsresult) { + cbHandleError(callback, nsresult); + } + }); + }, + + _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) { + let stmt = group ? + this._stmtWithGroupClause(group, includeSubdomains, ` + SELECT groups.name AS grp, prefs.value AS value + FROM prefs + JOIN settings ON settings.id = prefs.settingID + JOIN groups ON groups.id = prefs.groupID + WHERE settings.name = :name AND prefs.groupID IN (${GROUP_CLAUSE}) + `) : + this._stmt(` + SELECT NULL AS grp, prefs.value AS value + FROM prefs + JOIN settings ON settings.id = prefs.settingID + WHERE settings.name = :name AND prefs.groupID ISNULL + `); + stmt.params.name = name; + return stmt; + }, + + _stmtWithGroupClause: function CPS2__stmtWithGroupClause(group, + includeSubdomains, + sql) { + let stmt = this._stmt(sql); + stmt.params.group = group; + stmt.params.includeSubdomains = includeSubdomains || false; + stmt.params.pattern = "%." + stmt.escapeStringForLIKE(group, "/"); + return stmt; + }, + + getCachedByDomainAndName: function CPS2_getCachedByDomainAndName(group, + name, + context) { + checkGroupArg(group); + let prefs = this._getCached(group, name, false, context); + return prefs[0] || null; + }, + + getCachedBySubdomainAndName: function CPS2_getCachedBySubdomainAndName(group, + name, + context, + len) { + checkGroupArg(group); + let prefs = this._getCached(group, name, true, context); + if (len) + len.value = prefs.length; + return prefs; + }, + + getCachedGlobal: function CPS2_getCachedGlobal(name, context) { + let prefs = this._getCached(null, name, false, context); + return prefs[0] || null; + }, + + _getCached: function CPS2__getCached(group, name, includeSubdomains, + context) { + group = this._parseGroup(group); + checkNameArg(name); + + let storesToCheck = [this._cache]; + if (context && context.usePrivateBrowsing) + storesToCheck.push(this._pbStore); + + let outStore = new ContentPrefStore(); + storesToCheck.forEach(function (store) { + for (let [sgroup, val] of store.match(group, name, includeSubdomains)) { + outStore.set(sgroup, name, val); + } + }); + + let prefs = []; + for (let [sgroup, sname, val] of outStore) { + prefs.push(new ContentPref(sgroup, sname, val)); + } + return prefs; + }, + + set: function CPS2_set(group, name, value, context, callback) { + checkGroupArg(group); + this._set(group, name, value, context, callback); + }, + + setGlobal: function CPS2_setGlobal(name, value, context, callback) { + this._set(null, name, value, context, callback); + }, + + _set: function CPS2__set(group, name, value, context, callback) { + group = this._parseGroup(group); + checkNameArg(name); + checkValueArg(value); + checkCallbackArg(callback, false); + + if (context && context.usePrivateBrowsing) { + this._pbStore.set(group, name, value); + this._schedule(function () { + cbHandleCompletion(callback, Ci.nsIContentPrefCallback2.COMPLETE_OK); + this._cps._notifyPrefSet(group, name, value, context.usePrivateBrowsing); + }); + return; + } + + // Invalidate the cached value so consumers accessing the cache between now + // and when the operation finishes don't get old data. + this._cache.remove(group, name); + + let stmts = []; + + // Create the setting if it doesn't exist. + let stmt = this._stmt(` + INSERT OR IGNORE INTO settings (id, name) + VALUES((SELECT id FROM settings WHERE name = :name), :name) + `); + stmt.params.name = name; + stmts.push(stmt); + + // Create the group if it doesn't exist. + if (group) { + stmt = this._stmt(` + INSERT OR IGNORE INTO groups (id, name) + VALUES((SELECT id FROM groups WHERE name = :group), :group) + `); + stmt.params.group = group; + stmts.push(stmt); + } + + // Finally create or update the pref. + if (group) { + stmt = this._stmt(` + INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp) + VALUES( + (SELECT prefs.id + FROM prefs + JOIN groups ON groups.id = prefs.groupID + JOIN settings ON settings.id = prefs.settingID + WHERE groups.name = :group AND settings.name = :name), + (SELECT id FROM groups WHERE name = :group), + (SELECT id FROM settings WHERE name = :name), + :value, + :now + ) + `); + stmt.params.group = group; + } + else { + stmt = this._stmt(` + INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp) + VALUES( + (SELECT prefs.id + FROM prefs + JOIN settings ON settings.id = prefs.settingID + WHERE prefs.groupID IS NULL AND settings.name = :name), + NULL, + (SELECT id FROM settings WHERE name = :name), + :value, + :now + ) + `); + } + stmt.params.name = name; + stmt.params.value = value; + stmt.params.now = Date.now() / 1000; + stmts.push(stmt); + + this._execStmts(stmts, { + onDone: function onDone(reason, ok) { + if (ok) + this._cache.setWithCast(group, name, value); + cbHandleCompletion(callback, reason); + if (ok) + this._cps._notifyPrefSet(group, name, value, context && context.usePrivateBrowsing); + }, + onError: function onError(nsresult) { + cbHandleError(callback, nsresult); + } + }); + }, + + removeByDomainAndName: function CPS2_removeByDomainAndName(group, name, + context, + callback) { + checkGroupArg(group); + this._remove(group, name, false, context, callback); + }, + + removeBySubdomainAndName: function CPS2_removeBySubdomainAndName(group, name, + context, + callback) { + checkGroupArg(group); + this._remove(group, name, true, context, callback); + }, + + removeGlobal: function CPS2_removeGlobal(name, context, callback) { + this._remove(null, name, false, context, callback); + }, + + _remove: function CPS2__remove(group, name, includeSubdomains, context, + callback) { + group = this._parseGroup(group); + checkNameArg(name); + checkCallbackArg(callback, false); + + // Invalidate the cached values so consumers accessing the cache between now + // and when the operation finishes don't get old data. + for (let sgroup of this._cache.matchGroups(group, includeSubdomains)) { + this._cache.remove(sgroup, name); + } + + let stmts = []; + + // First get the matching prefs. + stmts.push(this._commonGetStmt(group, name, includeSubdomains)); + + // Delete the matching prefs. + let stmt = this._stmtWithGroupClause(group, includeSubdomains, ` + DELETE FROM prefs + WHERE settingID = (SELECT id FROM settings WHERE name = :name) AND + CASE typeof(:group) + WHEN 'null' THEN prefs.groupID IS NULL + ELSE prefs.groupID IN (${GROUP_CLAUSE}) + END + `); + stmt.params.name = name; + stmts.push(stmt); + + stmts = stmts.concat(this._settingsAndGroupsCleanupStmts()); + + let prefs = new ContentPrefStore(); + + let isPrivate = context && context.usePrivateBrowsing; + this._execStmts(stmts, { + onRow: function onRow(row) { + let grp = row.getResultByName("grp"); + prefs.set(grp, name, undefined); + this._cache.set(grp, name, undefined); + }, + onDone: function onDone(reason, ok) { + if (ok) { + this._cache.set(group, name, undefined); + if (isPrivate) { + for (let [sgroup, ] of + this._pbStore.match(group, name, includeSubdomains)) { + prefs.set(sgroup, name, undefined); + this._pbStore.remove(sgroup, name); + } + } + } + cbHandleCompletion(callback, reason); + if (ok) { + for (let [sgroup, , ] of prefs) { + this._cps._notifyPrefRemoved(sgroup, name, isPrivate); + } + } + }, + onError: function onError(nsresult) { + cbHandleError(callback, nsresult); + } + }); + }, + + // Deletes settings and groups that are no longer used. + _settingsAndGroupsCleanupStmts: function() { + // The NOTNULL term in the subquery of the second statment is needed because of + // SQLite's weird IN behavior vis-a-vis NULLs. See http://sqlite.org/lang_expr.html. + return [ + this._stmt(` + DELETE FROM settings + WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs) + `), + this._stmt(` + DELETE FROM groups WHERE id NOT IN ( + SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL + ) + `) + ]; + }, + + removeByDomain: function CPS2_removeByDomain(group, context, callback) { + checkGroupArg(group); + this._removeByDomain(group, false, context, callback); + }, + + removeBySubdomain: function CPS2_removeBySubdomain(group, context, callback) { + checkGroupArg(group); + this._removeByDomain(group, true, context, callback); + }, + + removeAllGlobals: function CPS2_removeAllGlobals(context, callback) { + this._removeByDomain(null, false, context, callback); + }, + + _removeByDomain: function CPS2__removeByDomain(group, includeSubdomains, + context, callback) { + group = this._parseGroup(group); + checkCallbackArg(callback, false); + + // Invalidate the cached values so consumers accessing the cache between now + // and when the operation finishes don't get old data. + for (let sgroup of this._cache.matchGroups(group, includeSubdomains)) { + this._cache.removeGroup(sgroup); + } + + let stmts = []; + + // First get the matching prefs, then delete groups and prefs that reference + // deleted groups. + if (group) { + stmts.push(this._stmtWithGroupClause(group, includeSubdomains, ` + SELECT groups.name AS grp, settings.name AS name + FROM prefs + JOIN settings ON settings.id = prefs.settingID + JOIN groups ON groups.id = prefs.groupID + WHERE prefs.groupID IN (${GROUP_CLAUSE}) + `)); + stmts.push(this._stmtWithGroupClause(group, includeSubdomains, + `DELETE FROM groups WHERE id IN (${GROUP_CLAUSE})` + )); + stmts.push(this._stmt(` + DELETE FROM prefs + WHERE groupID NOTNULL AND groupID NOT IN (SELECT id FROM groups) + `)); + } + else { + stmts.push(this._stmt(` + SELECT NULL AS grp, settings.name AS name + FROM prefs + JOIN settings ON settings.id = prefs.settingID + WHERE prefs.groupID IS NULL + `)); + stmts.push(this._stmt( + "DELETE FROM prefs WHERE groupID IS NULL" + )); + } + + // Finally delete settings that are no longer referenced. + stmts.push(this._stmt(` + DELETE FROM settings + WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs) + `)); + + let prefs = new ContentPrefStore(); + + let isPrivate = context && context.usePrivateBrowsing; + this._execStmts(stmts, { + onRow: function onRow(row) { + let grp = row.getResultByName("grp"); + let name = row.getResultByName("name"); + prefs.set(grp, name, undefined); + this._cache.set(grp, name, undefined); + }, + onDone: function onDone(reason, ok) { + if (ok && isPrivate) { + for (let [sgroup, sname, ] of this._pbStore) { + if (!group || + (!includeSubdomains && group == sgroup) || + (includeSubdomains && sgroup && this._pbStore.groupsMatchIncludingSubdomains(group, sgroup))) { + prefs.set(sgroup, sname, undefined); + this._pbStore.remove(sgroup, sname); + } + } + } + cbHandleCompletion(callback, reason); + if (ok) { + for (let [sgroup, sname, ] of prefs) { + this._cps._notifyPrefRemoved(sgroup, sname, isPrivate); + } + } + }, + onError: function onError(nsresult) { + cbHandleError(callback, nsresult); + } + }); + }, + + _removeAllDomainsSince: function CPS2__removeAllDomainsSince(since, context, callback) { + checkCallbackArg(callback, false); + + since /= 1000; + + // Invalidate the cached values so consumers accessing the cache between now + // and when the operation finishes don't get old data. + // Invalidate all the group cache because we don't know which groups will be removed. + this._cache.removeAllGroups(); + + let stmts = []; + + // Get prefs that are about to be removed to notify about their removal. + let stmt = this._stmt(` + SELECT groups.name AS grp, settings.name AS name + FROM prefs + JOIN settings ON settings.id = prefs.settingID + JOIN groups ON groups.id = prefs.groupID + WHERE timestamp >= :since + `); + stmt.params.since = since; + stmts.push(stmt); + + // Do the actual remove. + stmt = this._stmt(` + DELETE FROM prefs WHERE groupID NOTNULL AND timestamp >= :since + `); + stmt.params.since = since; + stmts.push(stmt); + + // Cleanup no longer used values. + stmts = stmts.concat(this._settingsAndGroupsCleanupStmts()); + + let prefs = new ContentPrefStore(); + let isPrivate = context && context.usePrivateBrowsing; + this._execStmts(stmts, { + onRow: function onRow(row) { + let grp = row.getResultByName("grp"); + let name = row.getResultByName("name"); + prefs.set(grp, name, undefined); + this._cache.set(grp, name, undefined); + }, + onDone: function onDone(reason, ok) { + // This nukes all the groups in _pbStore since we don't have their timestamp + // information. + if (ok && isPrivate) { + for (let [sgroup, sname, ] of this._pbStore) { + if (sgroup) { + prefs.set(sgroup, sname, undefined); + } + } + this._pbStore.removeAllGroups(); + } + cbHandleCompletion(callback, reason); + if (ok) { + for (let [sgroup, sname, ] of prefs) { + this._cps._notifyPrefRemoved(sgroup, sname, isPrivate); + } + } + }, + onError: function onError(nsresult) { + cbHandleError(callback, nsresult); + } + }); + }, + + removeAllDomainsSince: function CPS2_removeAllDomainsSince(since, context, callback) { + this._removeAllDomainsSince(since, context, callback); + }, + + removeAllDomains: function CPS2_removeAllDomains(context, callback) { + this._removeAllDomainsSince(0, context, callback); + }, + + removeByName: function CPS2_removeByName(name, context, callback) { + checkNameArg(name); + checkCallbackArg(callback, false); + + // Invalidate the cached values so consumers accessing the cache between now + // and when the operation finishes don't get old data. + for (let [group, sname, ] of this._cache) { + if (sname == name) + this._cache.remove(group, name); + } + + let stmts = []; + + // First get the matching prefs. Include null if any of those prefs are + // global. + let stmt = this._stmt(` + SELECT groups.name AS grp + FROM prefs + JOIN settings ON settings.id = prefs.settingID + JOIN groups ON groups.id = prefs.groupID + WHERE settings.name = :name + UNION + SELECT NULL AS grp + WHERE EXISTS ( + SELECT prefs.id + FROM prefs + JOIN settings ON settings.id = prefs.settingID + WHERE settings.name = :name AND prefs.groupID IS NULL + ) + `); + stmt.params.name = name; + stmts.push(stmt); + + // Delete the target settings. + stmt = this._stmt( + "DELETE FROM settings WHERE name = :name" + ); + stmt.params.name = name; + stmts.push(stmt); + + // Delete prefs and groups that are no longer used. + stmts.push(this._stmt( + "DELETE FROM prefs WHERE settingID NOT IN (SELECT id FROM settings)" + )); + stmts.push(this._stmt(` + DELETE FROM groups WHERE id NOT IN ( + SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL + ) + `)); + + let prefs = new ContentPrefStore(); + let isPrivate = context && context.usePrivateBrowsing; + + this._execStmts(stmts, { + onRow: function onRow(row) { + let grp = row.getResultByName("grp"); + prefs.set(grp, name, undefined); + this._cache.set(grp, name, undefined); + }, + onDone: function onDone(reason, ok) { + if (ok && isPrivate) { + for (let [sgroup, sname, ] of this._pbStore) { + if (sname === name) { + prefs.set(sgroup, name, undefined); + this._pbStore.remove(sgroup, name); + } + } + } + cbHandleCompletion(callback, reason); + if (ok) { + for (let [sgroup, , ] of prefs) { + this._cps._notifyPrefRemoved(sgroup, name, isPrivate); + } + } + }, + onError: function onError(nsresult) { + cbHandleError(callback, nsresult); + } + }); + }, + + destroy: function CPS2_destroy() { + if (this._statements) { + for (let sql in this._statements) { + let stmt = this._statements[sql]; + stmt.finalize(); + } + } + }, + + /** + * Returns the cached mozIStorageAsyncStatement for the given SQL. If no such + * statement is cached, one is created and cached. + * + * @param sql The SQL query string. + * @return The cached, possibly new, statement. + */ + _stmt: function CPS2__stmt(sql) { + if (!this._statements) + this._statements = {}; + if (!this._statements[sql]) + this._statements[sql] = this._cps._dbConnection.createAsyncStatement(sql); + return this._statements[sql]; + }, + + /** + * Executes some async statements. + * + * @param stmts An array of mozIStorageAsyncStatements. + * @param callbacks An object with the following methods: + * onRow(row) (optional) + * Called once for each result row. + * row: A mozIStorageRow. + * onDone(reason, reasonOK, didGetRow) (required) + * Called when done. + * reason: A nsIContentPrefService2.COMPLETE_* value. + * reasonOK: reason == nsIContentPrefService2.COMPLETE_OK. + * didGetRow: True if onRow was ever called. + * onError(nsresult) (optional) + * Called on error. + * nsresult: The error code. + */ + _execStmts: function CPS2__execStmts(stmts, callbacks) { + let self = this; + let gotRow = false; + this._cps._dbConnection.executeAsync(stmts, stmts.length, { + handleResult: function handleResult(results) { + try { + let row = null; + while ((row = results.getNextRow())) { + gotRow = true; + if (callbacks.onRow) + callbacks.onRow.call(self, row); + } + } + catch (err) { + Cu.reportError(err); + } + }, + handleCompletion: function handleCompletion(reason) { + try { + let ok = reason == Ci.mozIStorageStatementCallback.REASON_FINISHED; + callbacks.onDone.call(self, + ok ? Ci.nsIContentPrefCallback2.COMPLETE_OK : + Ci.nsIContentPrefCallback2.COMPLETE_ERROR, + ok, gotRow); + } + catch (err) { + Cu.reportError(err); + } + }, + handleError: function handleError(error) { + try { + if (callbacks.onError) + callbacks.onError.call(self, Cr.NS_ERROR_FAILURE); + } + catch (err) { + Cu.reportError(err); + } + } + }); + }, + + /** + * Parses the domain (the "group", to use the database's term) from the given + * string. + * + * @param groupStr Assumed to be either a string or falsey. + * @return If groupStr is a valid URL string, returns the domain of + * that URL. If groupStr is some other nonempty string, + * returns groupStr itself. Otherwise returns null. + */ + _parseGroup: function CPS2__parseGroup(groupStr) { + if (!groupStr) + return null; + try { + var groupURI = Services.io.newURI(groupStr, null, null); + } + catch (err) { + return groupStr; + } + return this._cps._grouper.group(groupURI); + }, + + _schedule: function CPS2__schedule(fn) { + Services.tm.mainThread.dispatch(fn.bind(this), + Ci.nsIThread.DISPATCH_NORMAL); + }, + + addObserverForName: function CPS2_addObserverForName(name, observer) { + this._cps._addObserver(name, observer); + }, + + removeObserverForName: function CPS2_removeObserverForName(name, observer) { + this._cps._removeObserver(name, observer); + }, + + extractDomain: function CPS2_extractDomain(str) { + return this._parseGroup(str); + }, + + /** + * Tests use this as a backchannel by calling it directly. + * + * @param subj This value depends on topic. + * @param topic The backchannel "method" name. + * @param data This value depends on topic. + */ + observe: function CPS2_observe(subj, topic, data) { + switch (topic) { + case "test:reset": + let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get(); + this._reset(fn); + break; + case "test:db": + let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get(); + obj.value = this._cps._dbConnection; + break; + } + }, + + /** + * Removes all state from the service. Used by tests. + * + * @param callback A function that will be called when done. + */ + _reset: function CPS2__reset(callback) { + this._pbStore.removeAll(); + this._cache.removeAll(); + + let cps = this._cps; + cps._observers = {}; + cps._genericObservers = []; + + let tables = ["prefs", "groups", "settings"]; + let stmts = tables.map(t => this._stmt(`DELETE FROM ${t}`)); + this._execStmts(stmts, { onDone: () => callback() }); + }, + + QueryInterface: function CPS2_QueryInterface(iid) { + let supportedIIDs = [ + Ci.nsIContentPrefService2, + Ci.nsIObserver, + Ci.nsISupports, + ]; + if (supportedIIDs.some(i => iid.equals(i))) + return this; + if (iid.equals(Ci.nsIContentPrefService)) + return this._cps; + throw Cr.NS_ERROR_NO_INTERFACE; + }, +}; + +function checkGroupArg(group) { + if (!group || typeof(group) != "string") + throw invalidArg("domain must be nonempty string."); +} + +function checkNameArg(name) { + if (!name || typeof(name) != "string") + throw invalidArg("name must be nonempty string."); +} + +function checkValueArg(value) { + if (value === undefined) + throw invalidArg("value must not be undefined."); +} + +function checkCallbackArg(callback, required) { + if (callback && !(callback instanceof Ci.nsIContentPrefCallback2)) + throw invalidArg("callback must be an nsIContentPrefCallback2."); + if (!callback && required) + throw invalidArg("callback must be given."); +} + +function invalidArg(msg) { + return Components.Exception(msg, Cr.NS_ERROR_INVALID_ARG); +} diff --git a/toolkit/components/contentprefs/ContentPrefServiceChild.jsm b/toolkit/components/contentprefs/ContentPrefServiceChild.jsm new file mode 100644 index 000000000..7faca8970 --- /dev/null +++ b/toolkit/components/contentprefs/ContentPrefServiceChild.jsm @@ -0,0 +1,182 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ "ContentPrefServiceChild" ]; + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/ContentPrefUtils.jsm"); +Cu.import("resource://gre/modules/ContentPrefStore.jsm"); + +// We only need one bit of information out of the context. +function contextArg(context) { + return (context && context.usePrivateBrowsing) ? + { usePrivateBrowsing: true } : + null; +} + +function NYI() { + throw new Error("Do not add any new users of these functions"); +} + +function CallbackCaller(callback) { + this._callback = callback; +} + +CallbackCaller.prototype = { + handleResult: function(contentPref) { + cbHandleResult(this._callback, + new ContentPref(contentPref.domain, + contentPref.name, + contentPref.value)); + }, + + handleError: function(result) { + cbHandleError(this._callback, result); + }, + + handleCompletion: function(reason) { + cbHandleCompletion(this._callback, reason); + }, +}; + +var ContentPrefServiceChild = { + QueryInterface: XPCOMUtils.generateQI([ Ci.nsIContentPrefService2 ]), + + // Map from pref name -> set of observers + _observers: new Map(), + + _mm: Cc["@mozilla.org/childprocessmessagemanager;1"] + .getService(Ci.nsIMessageSender), + + _getRandomId: function() { + return Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator).generateUUID().toString(); + }, + + // Map from random ID string -> CallbackCaller, per request + _requests: new Map(), + + init: function() { + this._mm.addMessageListener("ContentPrefs:HandleResult", this); + this._mm.addMessageListener("ContentPrefs:HandleError", this); + this._mm.addMessageListener("ContentPrefs:HandleCompletion", this); + }, + + receiveMessage: function(msg) { + let data = msg.data; + let callback; + switch (msg.name) { + case "ContentPrefs:HandleResult": + callback = this._requests.get(data.requestId); + callback.handleResult(data.contentPref); + break; + + case "ContentPrefs:HandleError": + callback = this._requests.get(data.requestId); + callback.handleError(data.error); + break; + + case "ContentPrefs:HandleCompletion": + callback = this._requests.get(data.requestId); + this._requests.delete(data.requestId); + callback.handleCompletion(data.reason); + break; + + case "ContentPrefs:NotifyObservers": { + let observerList = this._observers.get(data.name); + if (!observerList) + break; + + for (let observer of observerList) { + safeCallback(observer, data.callback, data.args); + } + + break; + } + } + }, + + _callFunction: function(call, args, callback) { + let requestId = this._getRandomId(); + let data = { call: call, args: args, requestId: requestId }; + + this._mm.sendAsyncMessage("ContentPrefs:FunctionCall", data); + + this._requests.set(requestId, new CallbackCaller(callback)); + }, + + getCachedByDomainAndName: NYI, + getCachedBySubdomainAndName: NYI, + getCachedGlobal: NYI, + + addObserverForName: function(name, observer) { + let set = this._observers.get(name); + if (!set) { + set = new Set(); + if (this._observers.size === 0) { + // This is the first observer of any kind. Start listening for changes. + this._mm.addMessageListener("ContentPrefs:NotifyObservers", this); + } + + // This is the first observer for this name. Start listening for changes + // to it. + this._mm.sendAsyncMessage("ContentPrefs:AddObserverForName", { name: name }); + this._observers.set(name, set); + } + + set.add(observer); + }, + + removeObserverForName: function(name, observer) { + let set = this._observers.get(name); + if (!set) + return; + + set.delete(observer); + if (set.size === 0) { + // This was the last observer for this name. Stop listening for changes. + this._mm.sendAsyncMessage("ContentPrefs:RemoveObserverForName", { name: name }); + + this._observers.delete(name); + if (this._observers.size === 0) { + // This was the last observer for this process. Stop listing for all + // changes. + this._mm.removeMessageListener("ContentPrefs:NotifyObservers", this); + } + } + }, + + extractDomain: NYI +}; + +function forwardMethodToParent(method, signature, ...args) { + // Ignore superfluous arguments + args = args.slice(0, signature.length); + + // Process context argument for forwarding + let contextIndex = signature.indexOf("context"); + if (contextIndex > -1) { + args[contextIndex] = contextArg(args[contextIndex]); + } + // Take out the callback argument, if present. + let callbackIndex = signature.indexOf("callback"); + let callback = null; + if (callbackIndex > -1 && args.length > callbackIndex) { + callback = args.splice(callbackIndex, 1)[0]; + } + this._callFunction(method, args, callback); +} + +for (let [method, signature] of _methodsCallableFromChild) { + ContentPrefServiceChild[method] = forwardMethodToParent.bind(ContentPrefServiceChild, method, signature); +} + +ContentPrefServiceChild.init(); diff --git a/toolkit/components/contentprefs/ContentPrefServiceParent.jsm b/toolkit/components/contentprefs/ContentPrefServiceParent.jsm new file mode 100644 index 000000000..32e31a789 --- /dev/null +++ b/toolkit/components/contentprefs/ContentPrefServiceParent.jsm @@ -0,0 +1,137 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ "ContentPrefServiceParent" ]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/ContentPrefUtils.jsm"); + +var ContentPrefServiceParent = { + _cps2: null, + + init: function() { + let globalMM = Cc["@mozilla.org/parentprocessmessagemanager;1"] + .getService(Ci.nsIMessageListenerManager); + + this._cps2 = Cc["@mozilla.org/content-pref/service;1"] + .getService(Ci.nsIContentPrefService2); + + globalMM.addMessageListener("ContentPrefs:FunctionCall", this); + + let observerChangeHandler = this.handleObserverChange.bind(this); + globalMM.addMessageListener("ContentPrefs:AddObserverForName", observerChangeHandler); + globalMM.addMessageListener("ContentPrefs:RemoveObserverForName", observerChangeHandler); + globalMM.addMessageListener("child-process-shutdown", observerChangeHandler); + }, + + // Map from message manager -> content pref observer. + _observers: new Map(), + + handleObserverChange: function(msg) { + let observer = this._observers.get(msg.target); + if (msg.name === "child-process-shutdown") { + // If we didn't have any observers for this child process, don't do + // anything. + if (!observer) + return; + + for (let i of observer._names) { + this._cps2.removeObserverForName(i, observer); + } + + this._observers.delete(msg.target); + return; + } + + let prefName = msg.data.name; + if (msg.name === "ContentPrefs:AddObserverForName") { + // The child process is responsible for not adding multiple parent + // observers for the same name. + if (!observer) { + observer = { + onContentPrefSet: function(group, name, value, isPrivate) { + msg.target.sendAsyncMessage("ContentPrefs:NotifyObservers", + { name: name, callback: "onContentPrefSet", + args: [ group, name, value, isPrivate ] }); + }, + + onContentPrefRemoved: function(group, name, isPrivate) { + msg.target.sendAsyncMessage("ContentPrefs:NotifyObservers", + { name: name, callback: "onContentPrefRemoved", + args: [ group, name, isPrivate ] }); + }, + + // The names we're using this observer object for, used to keep track + // of the number of names we care about as well as for removing this + // observer if its associated process goes away. + _names: new Set() + }; + + this._observers.set(msg.target, observer); + } + + observer._names.add(prefName); + + this._cps2.addObserverForName(prefName, observer); + } else { + // RemoveObserverForName + + // We must have an observer. + this._cps2.removeObserverForName(prefName, observer); + + observer._names.delete(prefName); + if (observer._names.size === 0) { + // This was the last use for this observer. + this._observers.delete(msg.target); + } + } + }, + + receiveMessage: function(msg) { + let data = msg.data; + + if (!_methodsCallableFromChild.some(([method, args]) => method == data.call)) { + throw new Error(`Can't call ${data.call} from child!`); + } + + let args = data.args; + let requestId = data.requestId; + + let listener = { + handleResult: function(pref) { + msg.target.sendAsyncMessage("ContentPrefs:HandleResult", + { requestId: requestId, + contentPref: { + domain: pref.domain, + name: pref.name, + value: pref.value + } + }); + }, + + handleError: function(error) { + msg.target.sendAsyncMessage("ContentPrefs:HandleError", + { requestId: requestId, + error: error }); + }, + handleCompletion: function(reason) { + msg.target.sendAsyncMessage("ContentPrefs:HandleCompletion", + { requestId: requestId, + reason: reason }); + } + }; + + // Push our special listener. + args.push(listener); + + // And call the function. + this._cps2[data.call](...args); + } +}; diff --git a/toolkit/components/contentprefs/ContentPrefStore.jsm b/toolkit/components/contentprefs/ContentPrefStore.jsm new file mode 100644 index 000000000..7a552662f --- /dev/null +++ b/toolkit/components/contentprefs/ContentPrefStore.jsm @@ -0,0 +1,123 @@ +/* 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/. */ + +var EXPORTED_SYMBOLS = [ + "ContentPrefStore", +]; + +function ContentPrefStore() { + this._groups = new Map(); + this._globalNames = new Map(); +} + +ContentPrefStore.prototype = { + + set: function CPS_set(group, name, val) { + if (group) { + if (!this._groups.has(group)) + this._groups.set(group, new Map()); + this._groups.get(group).set(name, val); + } + else { + this._globalNames.set(name, val); + } + }, + + setWithCast: function CPS_setWithCast(group, name, val) { + if (typeof(val) == "boolean") + val = val ? 1 : 0; + else if (val === undefined) + val = null; + this.set(group, name, val); + }, + + has: function CPS_has(group, name) { + if (group) { + return this._groups.has(group) && + this._groups.get(group).has(name); + } + return this._globalNames.has(name); + }, + + get: function CPS_get(group, name) { + if (group && this._groups.has(group)) + return this._groups.get(group).get(name); + return this._globalNames.get(name); + }, + + remove: function CPS_remove(group, name) { + if (group) { + if (this._groups.has(group)) { + this._groups.get(group).delete(name); + if (this._groups.get(group).size == 0) + this._groups.delete(group); + } + } + else { + this._globalNames.delete(name); + } + }, + + removeGroup: function CPS_removeGroup(group) { + if (group) { + this._groups.delete(group); + } + else { + this._globalNames.clear(); + } + }, + + removeAllGroups: function CPS_removeAllGroups() { + this._groups.clear(); + }, + + removeAll: function CPS_removeAll() { + this.removeAllGroups(); + this._globalNames.clear(); + }, + + groupsMatchIncludingSubdomains: function CPS_groupsMatchIncludingSubdomains(group, group2) { + let idx = group2.indexOf(group); + return (idx == group2.length - group.length && + (idx == 0 || group2[idx - 1] == ".")); + }, + + * [Symbol.iterator]() { + for (let [group, names] of this._groups) { + for (let [name, val] of names) { + yield [group, name, val]; + } + } + for (let [name, val] of this._globalNames) { + yield [null, name, val]; + } + }, + + * match(group, name, includeSubdomains) { + for (let sgroup of this.matchGroups(group, includeSubdomains)) { + if (this.has(sgroup, name)) + yield [sgroup, this.get(sgroup, name)]; + } + }, + + * matchGroups(group, includeSubdomains) { + if (group) { + if (includeSubdomains) { + for (let [sgroup, , ] of this) { + if (sgroup) { + if (this.groupsMatchIncludingSubdomains(group, sgroup)) { + yield sgroup; + } + } + } + } + else if (this._groups.has(group)) { + yield group; + } + } + else if (this._globalNames.size) { + yield null; + } + }, +}; diff --git a/toolkit/components/contentprefs/ContentPrefUtils.jsm b/toolkit/components/contentprefs/ContentPrefUtils.jsm new file mode 100644 index 000000000..557872e1a --- /dev/null +++ b/toolkit/components/contentprefs/ContentPrefUtils.jsm @@ -0,0 +1,70 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "ContentPref", + "cbHandleResult", + "cbHandleError", + "cbHandleCompletion", + "safeCallback", + "_methodsCallableFromChild", +]; + +const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function ContentPref(domain, name, value) { + this.domain = domain; + this.name = name; + this.value = value; +} + +ContentPref.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPref]), +}; + +function cbHandleResult(callback, pref) { + safeCallback(callback, "handleResult", [pref]); +} + +function cbHandleCompletion(callback, reason) { + safeCallback(callback, "handleCompletion", [reason]); +} + +function cbHandleError(callback, nsresult) { + safeCallback(callback, "handleError", [nsresult]); +} + +function safeCallback(callbackObj, methodName, args) { + if (!callbackObj || typeof(callbackObj[methodName]) != "function") + return; + try { + callbackObj[methodName].apply(callbackObj, args); + } + catch (err) { + Cu.reportError(err); + } +} + +const _methodsCallableFromChild = Object.freeze([ + ["getByName", ["name", "context", "callback"]], + ["getByDomainAndName", ["domain", "name", "context", "callback"]], + ["getBySubdomainAndName", ["domain", "name", "context", "callback"]], + ["getGlobal", ["name", "context", "callback"]], + ["set", ["domain", "name", "value", "context", "callback"]], + ["setGlobal", ["name", "value", "context", "callback"]], + ["removeByDomainAndName", ["domain", "name", "context", "callback"]], + ["removeBySubdomainAndName", ["domain", "name", "context", "callback"]], + ["removeGlobal", ["name", "context", "callback"]], + ["removeByDomain", ["domain", "context", "callback"]], + ["removeBySubdomain", ["domain", "context", "callback"]], + ["removeByName", ["name", "context", "callback"]], + ["removeAllDomains", ["context", "callback"]], + ["removeAllGlobals", ["context", "callback"]], +]); diff --git a/toolkit/components/contentprefs/moz.build b/toolkit/components/contentprefs/moz.build new file mode 100644 index 000000000..24bb296f1 --- /dev/null +++ b/toolkit/components/contentprefs/moz.build @@ -0,0 +1,31 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += [ + 'tests/unit/xpcshell.ini', + 'tests/unit_cps2/xpcshell.ini', +] + +MOCHITEST_MANIFESTS += [ + 'tests/mochitest/mochitest.ini' +] + +EXTRA_COMPONENTS += [ + 'nsContentPrefService.js', + 'nsContentPrefService.manifest', +] + +EXTRA_JS_MODULES += [ + 'ContentPrefInstance.jsm', + 'ContentPrefService2.jsm', + 'ContentPrefServiceChild.jsm', + 'ContentPrefServiceParent.jsm', + 'ContentPrefStore.jsm', + 'ContentPrefUtils.jsm', +] + +with Files('**'): + BUG_COMPONENT = ('Toolkit', 'Preferences') diff --git a/toolkit/components/contentprefs/nsContentPrefService.js b/toolkit/components/contentprefs/nsContentPrefService.js new file mode 100644 index 000000000..6360134a5 --- /dev/null +++ b/toolkit/components/contentprefs/nsContentPrefService.js @@ -0,0 +1,1332 @@ +/* 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 Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; +const Cu = Components.utils; + +const CACHE_MAX_GROUP_ENTRIES = 100; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function ContentPrefService() { + if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) { + return Cu.import("resource://gre/modules/ContentPrefServiceChild.jsm") + .ContentPrefServiceChild; + } + + // If this throws an exception, it causes the getService call to fail, + // but the next time a consumer tries to retrieve the service, we'll try + // to initialize the database again, which might work if the failure + // was due to a temporary condition (like being out of disk space). + this._dbInit(); + + this._observerSvc.addObserver(this, "last-pb-context-exited", false); + + // Observe shutdown so we can shut down the database connection. + this._observerSvc.addObserver(this, "xpcom-shutdown", false); +} + +Cu.import("resource://gre/modules/ContentPrefStore.jsm"); +const cache = new ContentPrefStore(); +cache.set = function CPS_cache_set(group, name, val) { + Object.getPrototypeOf(this).set.apply(this, arguments); + let groupCount = this._groups.size; + if (groupCount >= CACHE_MAX_GROUP_ENTRIES) { + // Clean half of the entries + for (let [group, name, ] of this) { + this.remove(group, name); + groupCount--; + if (groupCount < CACHE_MAX_GROUP_ENTRIES / 2) + break; + } + } +}; + +const privModeStorage = new ContentPrefStore(); + +ContentPrefService.prototype = { + // XPCOM Plumbing + + classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"), + + QueryInterface: function CPS_QueryInterface(iid) { + let supportedIIDs = [ + Ci.nsIContentPrefService, + Ci.nsISupports, + ]; + if (supportedIIDs.some(i => iid.equals(i))) + return this; + if (iid.equals(Ci.nsIContentPrefService2)) { + if (!this._contentPrefService2) { + let s = {}; + Cu.import("resource://gre/modules/ContentPrefService2.jsm", s); + this._contentPrefService2 = new s.ContentPrefService2(this); + } + return this._contentPrefService2; + } + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + // Convenience Getters + + // Observer Service + __observerSvc: null, + get _observerSvc() { + if (!this.__observerSvc) + this.__observerSvc = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + return this.__observerSvc; + }, + + // Console Service + __consoleSvc: null, + get _consoleSvc() { + if (!this.__consoleSvc) + this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + return this.__consoleSvc; + }, + + // Preferences Service + __prefSvc: null, + get _prefSvc() { + if (!this.__prefSvc) + this.__prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + return this.__prefSvc; + }, + + + // Destruction + + _destroy: function ContentPrefService__destroy() { + this._observerSvc.removeObserver(this, "xpcom-shutdown"); + this._observerSvc.removeObserver(this, "last-pb-context-exited"); + + // Finalize statements which may have been used asynchronously. + // FIXME(696499): put them in an object cache like other components. + if (this.__stmtSelectPrefID) { + this.__stmtSelectPrefID.finalize(); + this.__stmtSelectPrefID = null; + } + if (this.__stmtSelectGlobalPrefID) { + this.__stmtSelectGlobalPrefID.finalize(); + this.__stmtSelectGlobalPrefID = null; + } + if (this.__stmtInsertPref) { + this.__stmtInsertPref.finalize(); + this.__stmtInsertPref = null; + } + if (this.__stmtInsertGroup) { + this.__stmtInsertGroup.finalize(); + this.__stmtInsertGroup = null; + } + if (this.__stmtInsertSetting) { + this.__stmtInsertSetting.finalize(); + this.__stmtInsertSetting = null; + } + if (this.__stmtSelectGroupID) { + this.__stmtSelectGroupID.finalize(); + this.__stmtSelectGroupID = null; + } + if (this.__stmtSelectSettingID) { + this.__stmtSelectSettingID.finalize(); + this.__stmtSelectSettingID = null; + } + if (this.__stmtSelectPref) { + this.__stmtSelectPref.finalize(); + this.__stmtSelectPref = null; + } + if (this.__stmtSelectGlobalPref) { + this.__stmtSelectGlobalPref.finalize(); + this.__stmtSelectGlobalPref = null; + } + if (this.__stmtSelectPrefsByName) { + this.__stmtSelectPrefsByName.finalize(); + this.__stmtSelectPrefsByName = null; + } + if (this.__stmtDeleteSettingIfUnused) { + this.__stmtDeleteSettingIfUnused.finalize(); + this.__stmtDeleteSettingIfUnused = null; + } + if (this.__stmtSelectPrefs) { + this.__stmtSelectPrefs.finalize(); + this.__stmtSelectPrefs = null; + } + if (this.__stmtDeleteGroupIfUnused) { + this.__stmtDeleteGroupIfUnused.finalize(); + this.__stmtDeleteGroupIfUnused = null; + } + if (this.__stmtDeletePref) { + this.__stmtDeletePref.finalize(); + this.__stmtDeletePref = null; + } + if (this.__stmtUpdatePref) { + this.__stmtUpdatePref.finalize(); + this.__stmtUpdatePref = null; + } + + if (this._contentPrefService2) + this._contentPrefService2.destroy(); + + this._dbConnection.asyncClose(); + + // Delete references to XPCOM components to make sure we don't leak them + // (although we haven't observed leakage in tests). Also delete references + // in _observers and _genericObservers to avoid cycles with those that + // refer to us and don't remove themselves from those observer pools. + delete this._observers; + delete this._genericObservers; + delete this.__consoleSvc; + delete this.__grouper; + delete this.__observerSvc; + delete this.__prefSvc; + }, + + + // nsIObserver + + observe: function ContentPrefService_observe(subject, topic, data) { + switch (topic) { + case "xpcom-shutdown": + this._destroy(); + break; + case "last-pb-context-exited": + this._privModeStorage.removeAll(); + break; + } + }, + + + // in-memory cache and private-browsing stores + + _cache: cache, + _privModeStorage: privModeStorage, + + // nsIContentPrefService + + getPref: function ContentPrefService_getPref(aGroup, aName, aContext, aCallback) { + warnDeprecated(); + + if (!aName) + throw Components.Exception("aName cannot be null or an empty string", + Cr.NS_ERROR_ILLEGAL_VALUE); + + var group = this._parseGroupParam(aGroup); + + if (aContext && aContext.usePrivateBrowsing) { + if (this._privModeStorage.has(group, aName)) { + let value = this._privModeStorage.get(group, aName); + if (aCallback) { + this._scheduleCallback(function() { aCallback.onResult(value); }); + return undefined; + } + return value; + } + // if we don't have a pref specific to this private mode browsing + // session, to try to get one from normal mode + } + + if (group == null) + return this._selectGlobalPref(aName, aCallback); + return this._selectPref(group, aName, aCallback); + }, + + setPref: function ContentPrefService_setPref(aGroup, aName, aValue, aContext) { + warnDeprecated(); + + // If the pref is already set to the value, there's nothing more to do. + var currentValue = this.getPref(aGroup, aName, aContext); + if (typeof currentValue != "undefined") { + if (currentValue == aValue) + return; + } + + var group = this._parseGroupParam(aGroup); + + if (aContext && aContext.usePrivateBrowsing) { + this._privModeStorage.setWithCast(group, aName, aValue); + this._notifyPrefSet(group, aName, aValue, aContext.usePrivateBrowsing); + return; + } + + var settingID = this._selectSettingID(aName) || this._insertSetting(aName); + var groupID, prefID; + if (group == null) { + groupID = null; + prefID = this._selectGlobalPrefID(settingID); + } + else { + groupID = this._selectGroupID(group) || this._insertGroup(group); + prefID = this._selectPrefID(groupID, settingID); + } + + // Update the existing record, if any, or create a new one. + if (prefID) + this._updatePref(prefID, aValue); + else + this._insertPref(groupID, settingID, aValue); + + this._cache.setWithCast(group, aName, aValue); + + this._notifyPrefSet(group, aName, aValue, + aContext ? aContext.usePrivateBrowsing : false); + }, + + hasPref: function ContentPrefService_hasPref(aGroup, aName, aContext) { + warnDeprecated(); + + // XXX If consumers end up calling this method regularly, then we should + // optimize this to query the database directly. + return (typeof this.getPref(aGroup, aName, aContext) != "undefined"); + }, + + hasCachedPref: function ContentPrefService_hasCachedPref(aGroup, aName, aContext) { + warnDeprecated(); + + if (!aName) + throw Components.Exception("aName cannot be null or an empty string", + Cr.NS_ERROR_ILLEGAL_VALUE); + + let group = this._parseGroupParam(aGroup); + let storage = aContext && aContext.usePrivateBrowsing ? this._privModeStorage: this._cache; + return storage.has(group, aName); + }, + + removePref: function ContentPrefService_removePref(aGroup, aName, aContext) { + warnDeprecated(); + + // If there's no old value, then there's nothing to remove. + if (!this.hasPref(aGroup, aName, aContext)) + return; + + var group = this._parseGroupParam(aGroup); + + if (aContext && aContext.usePrivateBrowsing) { + this._privModeStorage.remove(group, aName); + this._notifyPrefRemoved(group, aName, true); + return; + } + + var settingID = this._selectSettingID(aName); + var groupID, prefID; + if (group == null) { + groupID = null; + prefID = this._selectGlobalPrefID(settingID); + } + else { + groupID = this._selectGroupID(group); + prefID = this._selectPrefID(groupID, settingID); + } + + this._deletePref(prefID); + + // Get rid of extraneous records that are no longer being used. + this._deleteSettingIfUnused(settingID); + if (groupID) + this._deleteGroupIfUnused(groupID); + + this._cache.remove(group, aName); + this._notifyPrefRemoved(group, aName, false); + }, + + removeGroupedPrefs: function ContentPrefService_removeGroupedPrefs(aContext) { + warnDeprecated(); + + // will not delete global preferences + if (aContext && aContext.usePrivateBrowsing) { + // keep only global prefs + this._privModeStorage.removeAllGroups(); + } + this._cache.removeAllGroups(); + this._dbConnection.beginTransaction(); + try { + this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE groupID IS NOT NULL"); + this._dbConnection.executeSimpleSQL("DELETE FROM groups"); + this._dbConnection.executeSimpleSQL(` + DELETE FROM settings + WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs) + `); + this._dbConnection.commitTransaction(); + } + catch (ex) { + this._dbConnection.rollbackTransaction(); + throw ex; + } + }, + + removePrefsByName: function ContentPrefService_removePrefsByName(aName, aContext) { + warnDeprecated(); + + if (!aName) + throw Components.Exception("aName cannot be null or an empty string", + Cr.NS_ERROR_ILLEGAL_VALUE); + + if (aContext && aContext.usePrivateBrowsing) { + for (let [group, name, ] of this._privModeStorage) { + if (name === aName) { + this._privModeStorage.remove(group, aName); + this._notifyPrefRemoved(group, aName, true); + } + } + } + + var settingID = this._selectSettingID(aName); + if (!settingID) + return; + + var selectGroupsStmt = this._dbCreateStatement(` + SELECT groups.id AS groupID, groups.name AS groupName + FROM prefs + JOIN groups ON prefs.groupID = groups.id + WHERE prefs.settingID = :setting + `); + + var groupNames = []; + var groupIDs = []; + try { + selectGroupsStmt.params.setting = settingID; + + while (selectGroupsStmt.executeStep()) { + groupIDs.push(selectGroupsStmt.row["groupID"]); + groupNames.push(selectGroupsStmt.row["groupName"]); + } + } + finally { + selectGroupsStmt.reset(); + } + + if (this.hasPref(null, aName)) { + groupNames.push(null); + } + + this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE settingID = " + settingID); + this._dbConnection.executeSimpleSQL("DELETE FROM settings WHERE id = " + settingID); + + for (var i = 0; i < groupNames.length; i++) { + this._cache.remove(groupNames[i], aName); + if (groupNames[i]) // ie. not null, which will be last (and i == groupIDs.length) + this._deleteGroupIfUnused(groupIDs[i]); + if (!aContext || !aContext.usePrivateBrowsing) { + this._notifyPrefRemoved(groupNames[i], aName, false); + } + } + }, + + getPrefs: function ContentPrefService_getPrefs(aGroup, aContext) { + warnDeprecated(); + + var group = this._parseGroupParam(aGroup); + if (aContext && aContext.usePrivateBrowsing) { + let prefs = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + for (let [sgroup, sname, sval] of this._privModeStorage) { + if (sgroup === group) + prefs.setProperty(sname, sval); + } + return prefs; + } + + if (group == null) + return this._selectGlobalPrefs(); + return this._selectPrefs(group); + }, + + getPrefsByName: function ContentPrefService_getPrefsByName(aName, aContext) { + warnDeprecated(); + + if (!aName) + throw Components.Exception("aName cannot be null or an empty string", + Cr.NS_ERROR_ILLEGAL_VALUE); + + if (aContext && aContext.usePrivateBrowsing) { + let prefs = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + for (let [sgroup, sname, sval] of this._privModeStorage) { + if (sname === aName) + prefs.setProperty(sgroup, sval); + } + return prefs; + } + + return this._selectPrefsByName(aName); + }, + + // A hash of arrays of observers, indexed by setting name. + _observers: {}, + + // An array of generic observers, which observe all settings. + _genericObservers: [], + + addObserver: function ContentPrefService_addObserver(aName, aObserver) { + warnDeprecated(); + this._addObserver.apply(this, arguments); + }, + + _addObserver: function ContentPrefService__addObserver(aName, aObserver) { + var observers; + if (aName) { + if (!this._observers[aName]) + this._observers[aName] = []; + observers = this._observers[aName]; + } + else + observers = this._genericObservers; + + if (observers.indexOf(aObserver) == -1) + observers.push(aObserver); + }, + + removeObserver: function ContentPrefService_removeObserver(aName, aObserver) { + warnDeprecated(); + this._removeObserver.apply(this, arguments); + }, + + _removeObserver: function ContentPrefService__removeObserver(aName, aObserver) { + var observers; + if (aName) { + if (!this._observers[aName]) + return; + observers = this._observers[aName]; + } + else + observers = this._genericObservers; + + if (observers.indexOf(aObserver) != -1) + observers.splice(observers.indexOf(aObserver), 1); + }, + + /** + * Construct a list of observers to notify about a change to some setting, + * putting setting-specific observers before before generic ones, so observers + * that initialize individual settings (like the page style controller) + * execute before observers that display multiple settings and depend on them + * being initialized first (like the content prefs sidebar). + */ + _getObservers: function ContentPrefService__getObservers(aName) { + var observers = []; + + if (aName && this._observers[aName]) + observers = observers.concat(this._observers[aName]); + observers = observers.concat(this._genericObservers); + + return observers; + }, + + /** + * Notify all observers about the removal of a preference. + */ + _notifyPrefRemoved: function ContentPrefService__notifyPrefRemoved(aGroup, aName, aIsPrivate) { + for (var observer of this._getObservers(aName)) { + try { + observer.onContentPrefRemoved(aGroup, aName, aIsPrivate); + } + catch (ex) { + Cu.reportError(ex); + } + } + }, + + /** + * Notify all observers about a preference change. + */ + _notifyPrefSet: function ContentPrefService__notifyPrefSet(aGroup, aName, aValue, aIsPrivate) { + for (var observer of this._getObservers(aName)) { + try { + observer.onContentPrefSet(aGroup, aName, aValue, aIsPrivate); + } + catch (ex) { + Cu.reportError(ex); + } + } + }, + + get grouper() { + warnDeprecated(); + return this._grouper; + }, + __grouper: null, + get _grouper() { + if (!this.__grouper) + this.__grouper = Cc["@mozilla.org/content-pref/hostname-grouper;1"]. + getService(Ci.nsIContentURIGrouper); + return this.__grouper; + }, + + get DBConnection() { + warnDeprecated(); + return this._dbConnection; + }, + + + // Data Retrieval & Modification + + __stmtSelectPref: null, + get _stmtSelectPref() { + if (!this.__stmtSelectPref) + this.__stmtSelectPref = this._dbCreateStatement(` + SELECT prefs.value AS value + FROM prefs + JOIN groups ON prefs.groupID = groups.id + JOIN settings ON prefs.settingID = settings.id + WHERE groups.name = :group + AND settings.name = :setting + `); + + return this.__stmtSelectPref; + }, + + _scheduleCallback: function(func) { + let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager); + tm.mainThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL); + }, + + _selectPref: function ContentPrefService__selectPref(aGroup, aSetting, aCallback) { + let value = undefined; + if (this._cache.has(aGroup, aSetting)) { + value = this._cache.get(aGroup, aSetting); + if (aCallback) { + this._scheduleCallback(function() { aCallback.onResult(value); }); + return undefined; + } + return value; + } + + try { + this._stmtSelectPref.params.group = aGroup; + this._stmtSelectPref.params.setting = aSetting; + + if (aCallback) { + let cache = this._cache; + new AsyncStatement(this._stmtSelectPref).execute({onResult: function(aResult) { + cache.set(aGroup, aSetting, aResult); + aCallback.onResult(aResult); + }}); + } + else { + if (this._stmtSelectPref.executeStep()) { + value = this._stmtSelectPref.row["value"]; + } + this._cache.set(aGroup, aSetting, value); + } + } + finally { + this._stmtSelectPref.reset(); + } + + return value; + }, + + __stmtSelectGlobalPref: null, + get _stmtSelectGlobalPref() { + if (!this.__stmtSelectGlobalPref) + this.__stmtSelectGlobalPref = this._dbCreateStatement(` + SELECT prefs.value AS value + FROM prefs + JOIN settings ON prefs.settingID = settings.id + WHERE prefs.groupID IS NULL + AND settings.name = :name + `); + + return this.__stmtSelectGlobalPref; + }, + + _selectGlobalPref: function ContentPrefService__selectGlobalPref(aName, aCallback) { + let value = undefined; + if (this._cache.has(null, aName)) { + value = this._cache.get(null, aName); + if (aCallback) { + this._scheduleCallback(function() { aCallback.onResult(value); }); + return undefined; + } + return value; + } + + try { + this._stmtSelectGlobalPref.params.name = aName; + + if (aCallback) { + let cache = this._cache; + new AsyncStatement(this._stmtSelectGlobalPref).execute({onResult: function(aResult) { + cache.set(null, aName, aResult); + aCallback.onResult(aResult); + }}); + } + else { + if (this._stmtSelectGlobalPref.executeStep()) { + value = this._stmtSelectGlobalPref.row["value"]; + } + this._cache.set(null, aName, value); + } + } + finally { + this._stmtSelectGlobalPref.reset(); + } + + return value; + }, + + __stmtSelectGroupID: null, + get _stmtSelectGroupID() { + if (!this.__stmtSelectGroupID) + this.__stmtSelectGroupID = this._dbCreateStatement(` + SELECT groups.id AS id + FROM groups + WHERE groups.name = :name + `); + + return this.__stmtSelectGroupID; + }, + + _selectGroupID: function ContentPrefService__selectGroupID(aName) { + var id; + + try { + this._stmtSelectGroupID.params.name = aName; + + if (this._stmtSelectGroupID.executeStep()) + id = this._stmtSelectGroupID.row["id"]; + } + finally { + this._stmtSelectGroupID.reset(); + } + + return id; + }, + + __stmtInsertGroup: null, + get _stmtInsertGroup() { + if (!this.__stmtInsertGroup) + this.__stmtInsertGroup = this._dbCreateStatement( + "INSERT INTO groups (name) VALUES (:name)" + ); + + return this.__stmtInsertGroup; + }, + + _insertGroup: function ContentPrefService__insertGroup(aName) { + this._stmtInsertGroup.params.name = aName; + this._stmtInsertGroup.execute(); + return this._dbConnection.lastInsertRowID; + }, + + __stmtSelectSettingID: null, + get _stmtSelectSettingID() { + if (!this.__stmtSelectSettingID) + this.__stmtSelectSettingID = this._dbCreateStatement( + "SELECT id FROM settings WHERE name = :name" + ); + + return this.__stmtSelectSettingID; + }, + + _selectSettingID: function ContentPrefService__selectSettingID(aName) { + var id; + + try { + this._stmtSelectSettingID.params.name = aName; + + if (this._stmtSelectSettingID.executeStep()) + id = this._stmtSelectSettingID.row["id"]; + } + finally { + this._stmtSelectSettingID.reset(); + } + + return id; + }, + + __stmtInsertSetting: null, + get _stmtInsertSetting() { + if (!this.__stmtInsertSetting) + this.__stmtInsertSetting = this._dbCreateStatement( + "INSERT INTO settings (name) VALUES (:name)" + ); + + return this.__stmtInsertSetting; + }, + + _insertSetting: function ContentPrefService__insertSetting(aName) { + this._stmtInsertSetting.params.name = aName; + this._stmtInsertSetting.execute(); + return this._dbConnection.lastInsertRowID; + }, + + __stmtSelectPrefID: null, + get _stmtSelectPrefID() { + if (!this.__stmtSelectPrefID) + this.__stmtSelectPrefID = this._dbCreateStatement( + "SELECT id FROM prefs WHERE groupID = :groupID AND settingID = :settingID" + ); + + return this.__stmtSelectPrefID; + }, + + _selectPrefID: function ContentPrefService__selectPrefID(aGroupID, aSettingID) { + var id; + + try { + this._stmtSelectPrefID.params.groupID = aGroupID; + this._stmtSelectPrefID.params.settingID = aSettingID; + + if (this._stmtSelectPrefID.executeStep()) + id = this._stmtSelectPrefID.row["id"]; + } + finally { + this._stmtSelectPrefID.reset(); + } + + return id; + }, + + __stmtSelectGlobalPrefID: null, + get _stmtSelectGlobalPrefID() { + if (!this.__stmtSelectGlobalPrefID) + this.__stmtSelectGlobalPrefID = this._dbCreateStatement( + "SELECT id FROM prefs WHERE groupID IS NULL AND settingID = :settingID" + ); + + return this.__stmtSelectGlobalPrefID; + }, + + _selectGlobalPrefID: function ContentPrefService__selectGlobalPrefID(aSettingID) { + var id; + + try { + this._stmtSelectGlobalPrefID.params.settingID = aSettingID; + + if (this._stmtSelectGlobalPrefID.executeStep()) + id = this._stmtSelectGlobalPrefID.row["id"]; + } + finally { + this._stmtSelectGlobalPrefID.reset(); + } + + return id; + }, + + __stmtInsertPref: null, + get _stmtInsertPref() { + if (!this.__stmtInsertPref) + this.__stmtInsertPref = this._dbCreateStatement(` + INSERT INTO prefs (groupID, settingID, value) + VALUES (:groupID, :settingID, :value) + `); + + return this.__stmtInsertPref; + }, + + _insertPref: function ContentPrefService__insertPref(aGroupID, aSettingID, aValue) { + this._stmtInsertPref.params.groupID = aGroupID; + this._stmtInsertPref.params.settingID = aSettingID; + this._stmtInsertPref.params.value = aValue; + this._stmtInsertPref.execute(); + return this._dbConnection.lastInsertRowID; + }, + + __stmtUpdatePref: null, + get _stmtUpdatePref() { + if (!this.__stmtUpdatePref) + this.__stmtUpdatePref = this._dbCreateStatement( + "UPDATE prefs SET value = :value WHERE id = :id" + ); + + return this.__stmtUpdatePref; + }, + + _updatePref: function ContentPrefService__updatePref(aPrefID, aValue) { + this._stmtUpdatePref.params.id = aPrefID; + this._stmtUpdatePref.params.value = aValue; + this._stmtUpdatePref.execute(); + }, + + __stmtDeletePref: null, + get _stmtDeletePref() { + if (!this.__stmtDeletePref) + this.__stmtDeletePref = this._dbCreateStatement( + "DELETE FROM prefs WHERE id = :id" + ); + + return this.__stmtDeletePref; + }, + + _deletePref: function ContentPrefService__deletePref(aPrefID) { + this._stmtDeletePref.params.id = aPrefID; + this._stmtDeletePref.execute(); + }, + + __stmtDeleteSettingIfUnused: null, + get _stmtDeleteSettingIfUnused() { + if (!this.__stmtDeleteSettingIfUnused) + this.__stmtDeleteSettingIfUnused = this._dbCreateStatement(` + DELETE FROM settings WHERE id = :id + AND id NOT IN (SELECT DISTINCT settingID FROM prefs) + `); + + return this.__stmtDeleteSettingIfUnused; + }, + + _deleteSettingIfUnused: function ContentPrefService__deleteSettingIfUnused(aSettingID) { + this._stmtDeleteSettingIfUnused.params.id = aSettingID; + this._stmtDeleteSettingIfUnused.execute(); + }, + + __stmtDeleteGroupIfUnused: null, + get _stmtDeleteGroupIfUnused() { + if (!this.__stmtDeleteGroupIfUnused) + this.__stmtDeleteGroupIfUnused = this._dbCreateStatement(` + DELETE FROM groups WHERE id = :id + AND id NOT IN (SELECT DISTINCT groupID FROM prefs) + `); + + return this.__stmtDeleteGroupIfUnused; + }, + + _deleteGroupIfUnused: function ContentPrefService__deleteGroupIfUnused(aGroupID) { + this._stmtDeleteGroupIfUnused.params.id = aGroupID; + this._stmtDeleteGroupIfUnused.execute(); + }, + + __stmtSelectPrefs: null, + get _stmtSelectPrefs() { + if (!this.__stmtSelectPrefs) + this.__stmtSelectPrefs = this._dbCreateStatement(` + SELECT settings.name AS name, prefs.value AS value + FROM prefs + JOIN groups ON prefs.groupID = groups.id + JOIN settings ON prefs.settingID = settings.id + WHERE groups.name = :group + `); + + return this.__stmtSelectPrefs; + }, + + _selectPrefs: function ContentPrefService__selectPrefs(aGroup) { + var prefs = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + + try { + this._stmtSelectPrefs.params.group = aGroup; + + while (this._stmtSelectPrefs.executeStep()) + prefs.setProperty(this._stmtSelectPrefs.row["name"], + this._stmtSelectPrefs.row["value"]); + } + finally { + this._stmtSelectPrefs.reset(); + } + + return prefs; + }, + + __stmtSelectGlobalPrefs: null, + get _stmtSelectGlobalPrefs() { + if (!this.__stmtSelectGlobalPrefs) + this.__stmtSelectGlobalPrefs = this._dbCreateStatement(` + SELECT settings.name AS name, prefs.value AS value + FROM prefs + JOIN settings ON prefs.settingID = settings.id + WHERE prefs.groupID IS NULL + `); + + return this.__stmtSelectGlobalPrefs; + }, + + _selectGlobalPrefs: function ContentPrefService__selectGlobalPrefs() { + var prefs = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + + try { + while (this._stmtSelectGlobalPrefs.executeStep()) + prefs.setProperty(this._stmtSelectGlobalPrefs.row["name"], + this._stmtSelectGlobalPrefs.row["value"]); + } + finally { + this._stmtSelectGlobalPrefs.reset(); + } + + return prefs; + }, + + __stmtSelectPrefsByName: null, + get _stmtSelectPrefsByName() { + if (!this.__stmtSelectPrefsByName) + this.__stmtSelectPrefsByName = this._dbCreateStatement(` + SELECT groups.name AS groupName, prefs.value AS value + FROM prefs + JOIN groups ON prefs.groupID = groups.id + JOIN settings ON prefs.settingID = settings.id + WHERE settings.name = :setting + `); + + return this.__stmtSelectPrefsByName; + }, + + _selectPrefsByName: function ContentPrefService__selectPrefsByName(aName) { + var prefs = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + + try { + this._stmtSelectPrefsByName.params.setting = aName; + + while (this._stmtSelectPrefsByName.executeStep()) + prefs.setProperty(this._stmtSelectPrefsByName.row["groupName"], + this._stmtSelectPrefsByName.row["value"]); + } + finally { + this._stmtSelectPrefsByName.reset(); + } + + var global = this._selectGlobalPref(aName); + if (typeof global != "undefined") { + prefs.setProperty(null, global); + } + + return prefs; + }, + + + // Database Creation & Access + + _dbVersion: 4, + + _dbSchema: { + tables: { + groups: "id INTEGER PRIMARY KEY, \ + name TEXT NOT NULL", + + settings: "id INTEGER PRIMARY KEY, \ + name TEXT NOT NULL", + + prefs: "id INTEGER PRIMARY KEY, \ + groupID INTEGER REFERENCES groups(id), \ + settingID INTEGER NOT NULL REFERENCES settings(id), \ + value BLOB, \ + timestamp INTEGER NOT NULL DEFAULT 0" // Storage in seconds, API in ms. 0 for migrated values. + }, + indices: { + groups_idx: { + table: "groups", + columns: ["name"] + }, + settings_idx: { + table: "settings", + columns: ["name"] + }, + prefs_idx: { + table: "prefs", + columns: ["timestamp", "groupID", "settingID"] + } + } + }, + + _dbConnection: null, + + _dbCreateStatement: function ContentPrefService__dbCreateStatement(aSQLString) { + try { + var statement = this._dbConnection.createStatement(aSQLString); + } + catch (ex) { + Cu.reportError("error creating statement " + aSQLString + ": " + + this._dbConnection.lastError + " - " + + this._dbConnection.lastErrorString); + throw ex; + } + + return statement; + }, + + // _dbInit and the methods it calls (_dbCreate, _dbMigrate, and version- + // specific migration methods) must be careful not to call any method + // of the service that assumes the database connection has already been + // initialized, since it won't be initialized until at the end of _dbInit. + + _dbInit: function ContentPrefService__dbInit() { + var dirService = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + var dbFile = dirService.get("ProfD", Ci.nsIFile); + dbFile.append("content-prefs.sqlite"); + + var dbService = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + + var dbConnection; + + if (!dbFile.exists()) + dbConnection = this._dbCreate(dbService, dbFile); + else { + try { + dbConnection = dbService.openDatabase(dbFile); + } + // If the connection isn't ready after we open the database, that means + // the database has been corrupted, so we back it up and then recreate it. + catch (e) { + if (e.result != Cr.NS_ERROR_FILE_CORRUPTED) + throw e; + dbConnection = this._dbBackUpAndRecreate(dbService, dbFile, + dbConnection); + } + + // Get the version of the schema in the file. + var version = dbConnection.schemaVersion; + + // Try to migrate the schema in the database to the current schema used by + // the service. If migration fails, back up the database and recreate it. + if (version != this._dbVersion) { + try { + this._dbMigrate(dbConnection, version, this._dbVersion); + } + catch (ex) { + Cu.reportError("error migrating DB: " + ex + "; backing up and recreating"); + dbConnection = this._dbBackUpAndRecreate(dbService, dbFile, dbConnection); + } + } + } + + // Turn off disk synchronization checking to reduce disk churn and speed up + // operations when prefs are changed rapidly (such as when a user repeatedly + // changes the value of the browser zoom setting for a site). + // + // Note: this could cause database corruption if the OS crashes or machine + // loses power before the data gets written to disk, but this is considered + // a reasonable risk for the not-so-critical data stored in this database. + // + // If you really don't want to take this risk, however, just set the + // toolkit.storage.synchronous pref to 1 (NORMAL synchronization) or 2 + // (FULL synchronization), in which case mozStorageConnection::Initialize + // will use that value, and we won't override it here. + if (!this._prefSvc.prefHasUserValue("toolkit.storage.synchronous")) + dbConnection.executeSimpleSQL("PRAGMA synchronous = OFF"); + + this._dbConnection = dbConnection; + }, + + _dbCreate: function ContentPrefService__dbCreate(aDBService, aDBFile) { + var dbConnection = aDBService.openDatabase(aDBFile); + + try { + this._dbCreateSchema(dbConnection); + dbConnection.schemaVersion = this._dbVersion; + } + catch (ex) { + // If we failed to create the database (perhaps because the disk ran out + // of space), then remove the database file so we don't leave it in some + // half-created state from which we won't know how to recover. + dbConnection.close(); + aDBFile.remove(false); + throw ex; + } + + return dbConnection; + }, + + _dbCreateSchema: function ContentPrefService__dbCreateSchema(aDBConnection) { + this._dbCreateTables(aDBConnection); + this._dbCreateIndices(aDBConnection); + }, + + _dbCreateTables: function ContentPrefService__dbCreateTables(aDBConnection) { + for (let name in this._dbSchema.tables) + aDBConnection.createTable(name, this._dbSchema.tables[name]); + }, + + _dbCreateIndices: function ContentPrefService__dbCreateIndices(aDBConnection) { + for (let name in this._dbSchema.indices) { + let index = this._dbSchema.indices[name]; + let statement = ` + CREATE INDEX IF NOT EXISTS ${name} ON ${index.table} + (${index.columns.join(", ")}) + `; + aDBConnection.executeSimpleSQL(statement); + } + }, + + _dbBackUpAndRecreate: function ContentPrefService__dbBackUpAndRecreate(aDBService, + aDBFile, + aDBConnection) { + aDBService.backupDatabaseFile(aDBFile, "content-prefs.sqlite.corrupt"); + + // Close the database, ignoring the "already closed" exception, if any. + // It'll be open if we're here because of a migration failure but closed + // if we're here because of database corruption. + try { aDBConnection.close() } catch (ex) {} + + aDBFile.remove(false); + + let dbConnection = this._dbCreate(aDBService, aDBFile); + + return dbConnection; + }, + + _dbMigrate: function ContentPrefService__dbMigrate(aDBConnection, aOldVersion, aNewVersion) { + /** + * Migrations should follow the template rules in bug 1074817 comment 3 which are: + * 1. Migration should be incremental and non-breaking. + * 2. It should be idempotent because one can downgrade an upgrade again. + * On downgrade: + * 1. Decrement schema version so that upgrade runs the migrations again. + */ + aDBConnection.beginTransaction(); + + try { + /** + * If the schema version is 0, that means it was never set, which means + * the database was somehow created without the schema being applied, perhaps + * because the system ran out of disk space (although we check for this + * in _createDB) or because some other code created the database file without + * applying the schema. In any case, recover by simply reapplying the schema. + */ + if (aOldVersion == 0) { + this._dbCreateSchema(aDBConnection); + } else { + for (let i = aOldVersion; i < aNewVersion; i++) { + let migrationName = "_dbMigrate" + i + "To" + (i + 1); + if (typeof this[migrationName] != 'function') { + throw ("no migrator function from version " + aOldVersion + " to version " + aNewVersion); + } + this[migrationName](aDBConnection); + } + } + aDBConnection.schemaVersion = aNewVersion; + aDBConnection.commitTransaction(); + } catch (ex) { + aDBConnection.rollbackTransaction(); + throw ex; + } + }, + + _dbMigrate1To2: function ContentPrefService___dbMigrate1To2(aDBConnection) { + aDBConnection.executeSimpleSQL("ALTER TABLE groups RENAME TO groupsOld"); + aDBConnection.createTable("groups", this._dbSchema.tables.groups); + aDBConnection.executeSimpleSQL(` + INSERT INTO groups (id, name) + SELECT id, name FROM groupsOld + `); + + aDBConnection.executeSimpleSQL("DROP TABLE groupers"); + aDBConnection.executeSimpleSQL("DROP TABLE groupsOld"); + }, + + _dbMigrate2To3: function ContentPrefService__dbMigrate2To3(aDBConnection) { + this._dbCreateIndices(aDBConnection); + }, + + _dbMigrate3To4: function ContentPrefService__dbMigrate3To4(aDBConnection) { + // Add timestamp column if it does not exist yet. This operation is idempotent. + try { + let stmt = aDBConnection.createStatement("SELECT timestamp FROM prefs"); + stmt.finalize(); + } catch (e) { + aDBConnection.executeSimpleSQL("ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0"); + } + + // To modify prefs_idx drop it and create again. + aDBConnection.executeSimpleSQL("DROP INDEX IF EXISTS prefs_idx"); + this._dbCreateIndices(aDBConnection); + }, + + _parseGroupParam: function ContentPrefService__parseGroupParam(aGroup) { + if (aGroup == null) + return null; + if (aGroup.constructor.name == "String") + return aGroup.toString(); + if (aGroup instanceof Ci.nsIURI) + return this.grouper.group(aGroup); + + throw Components.Exception("aGroup is not a string, nsIURI or null", + Cr.NS_ERROR_ILLEGAL_VALUE); + }, +}; + +function warnDeprecated() { + let Deprecated = Cu.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated; + Deprecated.warning("nsIContentPrefService is deprecated. Please use nsIContentPrefService2 instead.", + "https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIContentPrefService2", + Components.stack.caller); +} + + +function HostnameGrouper() {} + +HostnameGrouper.prototype = { + // XPCOM Plumbing + + classID: Components.ID("{8df290ae-dcaa-4c11-98a5-2429a4dc97bb}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentURIGrouper]), + + // nsIContentURIGrouper + + group: function HostnameGrouper_group(aURI) { + var group; + + try { + // Accessing the host property of the URI will throw an exception + // if the URI is of a type that doesn't have a host property. + // Otherwise, we manually throw an exception if the host is empty, + // since the effect is the same (we can't derive a group from it). + + group = aURI.host; + if (!group) + throw ("can't derive group from host; no host in URI"); + } + catch (ex) { + // If we don't have a host, then use the entire URI (minus the query, + // reference, and hash, if possible) as the group. This means that URIs + // like about:mozilla and about:blank will be considered separate groups, + // but at least they'll be grouped somehow. + + // This also means that each individual file: URL will be considered + // its own group. This seems suboptimal, but so does treating the entire + // file: URL space as a single group (especially if folks start setting + // group-specific capabilities prefs). + + // XXX Is there something better we can do here? + + try { + var url = aURI.QueryInterface(Ci.nsIURL); + group = aURI.prePath + url.filePath; + } + catch (ex) { + group = aURI.spec; + } + } + + return group; + } +}; + +function AsyncStatement(aStatement) { + this.stmt = aStatement; +} + +AsyncStatement.prototype = { + execute: function AsyncStmt_execute(aCallback) { + let stmt = this.stmt; + stmt.executeAsync({ + _callback: aCallback, + _hadResult: false, + handleResult: function(aResult) { + this._hadResult = true; + if (this._callback) { + let row = aResult.getNextRow(); + this._callback.onResult(row.getResultByName("value")); + } + }, + handleCompletion: function(aReason) { + if (!this._hadResult && this._callback && + aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) + this._callback.onResult(undefined); + }, + handleError: function(aError) {} + }); + } +}; + +// XPCOM Plumbing + +var components = [ContentPrefService, HostnameGrouper]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/toolkit/components/contentprefs/nsContentPrefService.manifest b/toolkit/components/contentprefs/nsContentPrefService.manifest new file mode 100644 index 000000000..b6bc15721 --- /dev/null +++ b/toolkit/components/contentprefs/nsContentPrefService.manifest @@ -0,0 +1,5 @@ +component {e3f772f3-023f-4b32-b074-36cf0fd5d414} nsContentPrefService.js +contract @mozilla.org/content-pref/service;1 {e3f772f3-023f-4b32-b074-36cf0fd5d414} +component {8df290ae-dcaa-4c11-98a5-2429a4dc97bb} nsContentPrefService.js +contract @mozilla.org/content-pref/hostname-grouper;1 {8df290ae-dcaa-4c11-98a5-2429a4dc97bb} + diff --git a/toolkit/components/contentprefs/tests/mochitest/.eslintrc.js b/toolkit/components/contentprefs/tests/mochitest/.eslintrc.js new file mode 100644 index 000000000..64a4eda73 --- /dev/null +++ b/toolkit/components/contentprefs/tests/mochitest/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/mochitest/mochitest.eslintrc.js" + ] +}; diff --git a/toolkit/components/contentprefs/tests/mochitest/mochitest.ini b/toolkit/components/contentprefs/tests/mochitest/mochitest.ini new file mode 100644 index 000000000..ec4f05945 --- /dev/null +++ b/toolkit/components/contentprefs/tests/mochitest/mochitest.ini @@ -0,0 +1,4 @@ +[DEFAULT] + +[test_remoteContentPrefs.html] +skip-if = toolkit == 'android' || e10s # bug 783513 diff --git a/toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html b/toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html new file mode 100644 index 000000000..d14e85a25 --- /dev/null +++ b/toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html @@ -0,0 +1,311 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for nsIContentPrefService2 in child processes</title> + <script type="application/javascript" + src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + + <script type="application/javascript;version=1.8"> + "use strict"; + + SimpleTest.waitForExplicitFinish(); + + const childFrameURL = + "data:text/html,<!DOCTYPE HTML><html><body></body></html>"; + + function childFrameScript(isFramePrivate) { + "use strict"; + + function Tester(resultArray) { + this.results = []; + } + + Tester.prototype.is = + function(a, b, note) { + this.results.push([a === b, note + " (" + a + ", " + b + ")"]); + }; + Tester.prototype.ok = + function(b, note) { + this.results.push([b != false, note]); + }; + + var cps = Components.classes["@mozilla.org/content-pref/service;1"] + .getService(Components.interfaces.nsIContentPrefService2); + + let test = null; + function* test1(message) { + let tester = new Tester(); + + tester.ok(cps !== null, "got the content pref service"); + + cps.setGlobal("testing", 42, null, { + handleCompletion: function(reason) { + tester.is(reason, 0, "set a pref?"); + test.next(); + } + }); + + yield; + + let numResults = 0; + cps.getGlobal("testing", null, { + handleResult: function(pref) { + numResults++; + tester.is(pref.name, "testing", "pref has the right name"); + tester.is(pref.value, 42, "pref has the right value"); + }, + + handleCompletion: function(reason) { + tester.is(reason, 0, "get a pref?"); + tester.is(numResults, 1, "got the right number of prefs"); + tester.is(test.next().done, true, "done with test1"); + message.target.sendAsyncMessage("testRemoteContentPrefs:test1Finished", + { results: tester.results }); + } + }); + + yield; + } + + function* test2(message) { + let tester = new Tester(); + + let observer; + let removed = false; + cps.addObserverForName("testName", observer = { + onContentPrefSet: function(group, name, value, isPrivate) { + if (removed) { + message.target.sendAsyncMessage("testRemoteContentPrefs:fail", + { reason: "unexpected notification" }); + } + tester.is(group, null, "group should be null"); + tester.is(name, "testName", "should only see testName"); + tester.is(value, 42, "value should be correct"); + tester.is(isPrivate, isFramePrivate, "privacy should match"); + + message.target.sendAsyncMessage("testRemoteContentPrefs:test2poke2", {}) + }, + + onContentPrefRemoved: function(group, name, isPrivate) { + tester.is(group, null, "group should be null"); + tester.is(name, "testName"); + tester.is(isPrivate, isFramePrivate, "privacy should match"); + tester.is(test.next().done, true, "should be done with test2"); + + cps.removeObserverForName("testName", observer); + removed = true; + + message.target.sendAsyncMessage("testRemoteContentPrefs:test2Finished", + { results: tester.results }); + } + }); + + message.target.sendAsyncMessage("testRemoteContentPrefs:test2poke", {}); + yield; + } + + function* test3(message) { + let tester = new Tester(); + + cps.setGlobal("testName", 42, null, { + handleCompletion: function(reason) { + tester.is(reason, 0, "set a pref"); + cps.set("http://mochi.test", "testpref", "str", null, { + handleCompletion: function(reason) { + tester.is(reason, 0, "set a pref"); + test.next(); + } + }); + } + }); + + yield; + + cps.removeByDomain("http://mochi.test", null, { + handleCompletion: function(reason) { + tester.is(reason, 0, "remove succeeded"); + cps.getByDomainAndName("http://mochi.test", "testpref", null, { + handleResult: function() { + message.target.sendAsyncMessage("testRemoteContentPrefs:fail", + { reason: "got removed pref in test3" }); + }, + handleCompletion: function() { + test.next(); + } + }); + } + }); + + yield; + + message.target.sendAsyncMessage("testRemoteContentPrefs:test3Finished", + { results: tester.results }); + } + + function* test4(message) { + let tester = new Tester(); + + let prefObserver = { + onContentPrefSet: function(group, name, value, isPrivate) { + test.next({ group: group, name: name, value: value, isPrivate: isPrivate }); + }, + onContentPrefRemoved: function(group, name, isPrivate) { + test.next({ group: group, name: name, isPrivate: isPrivate }); + } + }; + + addMessageListener("testRemoteContentPrefs:prefResults", (msg) => { + test.next(msg.data.results); + }); + + cps.addObserverForName("test", prefObserver); + + cps.set("http://mochi.test", "test", 42, { usePrivateBrowsing: true }); + let event = yield; + tester.is(event.name, "test"); + tester.is(event.isPrivate, true); + + message.target.sendAsyncMessage("testRemoteContentPrefs:getPref", + { group: "http://mochi.test", name: "test" }); + + let results = yield; + tester.is(results.length, 0, "should not have seen the pb pref"); + + message.target.sendAsyncMessage("testRemoteContentPrefs:test4Finished", + { results: tester.results }); + } + + addMessageListener("testRemoteContentPrefs:test1", function(message) { + test = test1(message); + test.next(); + }); + addMessageListener("testRemoteContentPrefs:test2", function(message) { + test = test2(message); + test.next(); + }); + addMessageListener("testRemoteContentPrefs:test3", function(message) { + test = test3(message); + test.next(); + }); + addMessageListener("testRemoteContentPrefs:test4", function(message) { + test = test4(message); + test.next(); + }); + } + + function processResults(results) { + for (let i of results) { + ok(...i); + } + } + + let test; + function* testStructure(mm, isPrivate, callback) { + let lastResult; + + function testDone(msg) { + test.next(msg.data); + } + + mm.addMessageListener("testRemoteContentPrefs:test1Finished", testDone); + mm.addMessageListener("testRemoteContentPrefs:test2Finished", testDone); + mm.addMessageListener("testRemoteContentPrefs:test3Finished", testDone); + mm.addMessageListener("testRemoteContentPrefs:test4Finished", testDone); + + mm.addMessageListener("testRemoteContentPrefs:fail", function(msg) { + ok(false, msg.data.reason); + }); + + mm.sendAsyncMessage("testRemoteContentPrefs:test1", {}); + lastResult = yield; + processResults(lastResult.results); + + var cps = SpecialPowers.Cc["@mozilla.org/content-pref/service;1"] + .getService(SpecialPowers.Ci.nsIContentPrefService2); + mm.sendAsyncMessage("testRemoteContentPrefs:test2", {}); + mm.addMessageListener("testRemoteContentPrefs:test2poke", function() { + cps.setGlobal("testName", 42, {usePrivateBrowsing: isPrivate}); + }); + mm.addMessageListener("testRemoteContentPrefs:test2poke2", function() { + cps.removeGlobal("testName", {usePrivateBrowsing: isPrivate}); + }); + + lastResult = yield; + processResults(lastResult.results); + + mm.sendAsyncMessage("testRemoteContentPrefs:test3", {}); + lastResult = yield; + processResults(lastResult.results); + + mm.addMessageListener("testRemoteContentPrefs:getPref", function(msg) { + let results = []; + cps.getByDomainAndName(msg.data.group, msg.data.name, null, { + handleResult: function(pref) { + results.push(pref); + }, + handleCompletion: function(reason) { + mm.sendAsyncMessage("testRemoteContentPrefs:prefResults", + { results: results }); + } + }); + }); + + mm.sendAsyncMessage("testRemoteContentPrefs:test4", {}); + lastResult = yield; + processResults(lastResult.results); + + document.getElementById('iframe').remove(); + setTimeout(callback, 0); + } + + function runTest(isPrivate, callback) { + info("testing with isPrivate=" + isPrivate); + let iframe = document.createElement("iframe"); + SpecialPowers.wrap(iframe).mozbrowser = true; + if (isPrivate) { + SpecialPowers.wrap(iframe).mozprivatebrowsing = true; + } + iframe.id = "iframe"; + iframe.src = childFrameURL; + + iframe.addEventListener("mozbrowserloadend", function() { + info("Got iframe load event."); + let mm = SpecialPowers.getBrowserFrameMessageManager(iframe); + mm.loadFrameScript("data:,(" + childFrameScript.toString() + ")(" + isPrivate + ");", + false); + + test = testStructure(mm, isPrivate, callback); + test.next(); + }); + + document.body.appendChild(iframe); + } + + function runTests() { + info("Browser prefs set."); + runTest(false, function() { + runTest(true, function() { + SimpleTest.finish(); + }); + }); + } + + addEventListener("load", function() { + info("Got load event."); + + SpecialPowers.addPermission("browser", true, document); + SpecialPowers.pushPrefEnv({ + "set": [ + ["dom.ipc.browser_frames.oop_by_default", true], + ["dom.mozBrowserFramesEnabled", true], + ["browser.pagethumbnails.capturing_disabled", true] + ] + }, runTests); + }); + </script> +</body> +</html> diff --git a/toolkit/components/contentprefs/tests/unit/.eslintrc.js b/toolkit/components/contentprefs/tests/unit/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/contentprefs/tests/unit/head_contentPrefs.js b/toolkit/components/contentprefs/tests/unit/head_contentPrefs.js new file mode 100644 index 000000000..84ca1bebf --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit/head_contentPrefs.js @@ -0,0 +1,162 @@ +/* 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/. */ + +// Inspired by the Places infrastructure in head_bookmarks.js + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; +var Cu = Components.utils; + +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/ContentPrefInstance.jsm'); + +const CONTENT_PREFS_DB_FILENAME = "content-prefs.sqlite"; +const CONTENT_PREFS_BACKUP_DB_FILENAME = "content-prefs.sqlite.corrupt"; + +var ContentPrefTest = { + // Convenience Getters + + __dirSvc: null, + get _dirSvc() { + if (!this.__dirSvc) + this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + return this.__dirSvc; + }, + + __consoleSvc: null, + get _consoleSvc() { + if (!this.__consoleSvc) + this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + return this.__consoleSvc; + }, + + __ioSvc: null, + get _ioSvc() { + if (!this.__ioSvc) + this.__ioSvc = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + return this.__ioSvc; + }, + + + // nsISupports + + interfaces: [Ci.nsIDirectoryServiceProvider, Ci.nsISupports], + + QueryInterface: function ContentPrefTest_QueryInterface(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + }, + + + // nsIDirectoryServiceProvider + + getFile: function ContentPrefTest_getFile(property, persistent) { + persistent.value = true; + + if (property == "ProfD") + return this._dirSvc.get("CurProcD", Ci.nsIFile); + + // This causes extraneous errors to show up in the log when the directory + // service asks us first for CurProcD and MozBinD. I wish there was a way + // to suppress those errors. + throw Cr.NS_ERROR_FAILURE; + }, + + + // Utilities + + getURI: function ContentPrefTest_getURI(spec) { + return this._ioSvc.newURI(spec, null, null); + }, + + /** + * Get the profile directory. + */ + getProfileDir: function ContentPrefTest_getProfileDir() { + // do_get_profile can be only called from a parent process + if (runningInParent) { + return do_get_profile(); + } + // if running in a content process, this just returns the path + // profile was initialized in the ipc head file + let env = Components.classes["@mozilla.org/process/environment;1"] + .getService(Components.interfaces.nsIEnvironment); + // the python harness sets this in the environment for us + let profd = env.get("XPCSHELL_TEST_PROFILE_DIR"); + let file = Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsILocalFile); + file.initWithPath(profd); + return file; + }, + + /** + * Delete the content pref service's persistent datastore. We do this before + * and after running tests to make sure we start from scratch each time. We + * also do it during the database creation, schema migration, and backup tests. + */ + deleteDatabase: function ContentPrefTest_deleteDatabase() { + var file = this.getProfileDir(); + file.append(CONTENT_PREFS_DB_FILENAME); + if (file.exists()) + try { file.remove(false); } catch (e) { /* stupid windows box */ } + return file; + }, + + /** + * Delete the backup of the content pref service's persistent datastore. + * We do this during the database creation, schema migration, and backup tests. + */ + deleteBackupDatabase: function ContentPrefTest_deleteBackupDatabase() { + var file = this.getProfileDir(); + file.append(CONTENT_PREFS_BACKUP_DB_FILENAME); + if (file.exists()) + file.remove(false); + return file; + }, + + /** + * Log a message to the console and the test log. + */ + log: function ContentPrefTest_log(message) { + message = "*** ContentPrefTest: " + message; + this._consoleSvc.logStringMessage(message); + print(message); + } + +}; + +var gInPrivateBrowsing = false; +function enterPBMode() { + gInPrivateBrowsing = true; +} +function exitPBMode() { + gInPrivateBrowsing = false; + Services.obs.notifyObservers(null, "last-pb-context-exited", null); +} + +ContentPrefTest.deleteDatabase(); + +function inChildProcess() { + var appInfo = Cc["@mozilla.org/xre/app-info;1"]; + if (!appInfo || appInfo.getService(Ci.nsIXULRuntime).processType == + Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { + return false; + } + return true; +} + +// Turn on logging for the content preferences service so we can troubleshoot +// problems with the tests. Note that we cannot do this in a child process +// without crashing (but we don't need it anyhow) +if (!inChildProcess()) { + var prefBranch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefBranch.setBoolPref("browser.preferences.content.log", true); +} + diff --git a/toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js b/toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js new file mode 100644 index 000000000..f3c95dac8 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js @@ -0,0 +1,6 @@ +/* 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/. */ + +ContentPrefTest.deleteDatabase(); +ContentPrefTest.__dirSvc = null; diff --git a/toolkit/components/contentprefs/tests/unit/test_bug248970.js b/toolkit/components/contentprefs/tests/unit/test_bug248970.js new file mode 100644 index 000000000..5f4aa25c5 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit/test_bug248970.js @@ -0,0 +1,42 @@ +/* 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/. */ + +function run_test() { + let loadContext = { get usePrivateBrowsing() { return gInPrivateBrowsing; } }; + + ContentPrefTest.deleteDatabase(); + var cp = new ContentPrefInstance(loadContext); + do_check_neq(cp, null, "Retrieving the content prefs service failed"); + + try { + const uri1 = ContentPrefTest.getURI("http://www.example.com/"); + const uri2 = ContentPrefTest.getURI("http://www.anotherexample.com/"); + const pref_name = "browser.content.full-zoom"; + const zoomA = 1.5, zoomA_new = 0.8, zoomB = 1.3; + // save Zoom-A + cp.setPref(uri1, pref_name, zoomA); + // make sure Zoom-A is retrievable + do_check_eq(cp.getPref(uri1, pref_name), zoomA); + // enter private browsing mode + enterPBMode(); + // make sure Zoom-A is retrievable + do_check_eq(cp.getPref(uri1, pref_name), zoomA); + // save Zoom-B + cp.setPref(uri2, pref_name, zoomB); + // make sure Zoom-B is retrievable + do_check_eq(cp.getPref(uri2, pref_name), zoomB); + // update Zoom-A + cp.setPref(uri1, pref_name, zoomA_new); + // make sure Zoom-A has changed + do_check_eq(cp.getPref(uri1, pref_name), zoomA_new); + // exit private browsing mode + exitPBMode(); + // make sure Zoom-A change has not persisted + do_check_eq(cp.getPref(uri1, pref_name), zoomA); + // make sure Zoom-B change has not persisted + do_check_eq(cp.hasPref(uri2, pref_name), false); + } catch (e) { + do_throw("Unexpected exception: " + e); + } +} diff --git a/toolkit/components/contentprefs/tests/unit/test_bug503971.js b/toolkit/components/contentprefs/tests/unit/test_bug503971.js new file mode 100644 index 000000000..ccfe1d02b --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit/test_bug503971.js @@ -0,0 +1,35 @@ +/* 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/. */ + +function run_test() { + var cps = new ContentPrefInstance(null); + + var uri = ContentPrefTest.getURI("http://www.example.com/"); + + do_check_thrown(function () { cps.setPref(uri, null, 8); }); + do_check_thrown(function () { cps.hasPref(uri, null); }); + do_check_thrown(function () { cps.getPref(uri, null); }); + do_check_thrown(function () { cps.removePref(uri, null); }); + do_check_thrown(function () { cps.getPrefsByName(null); }); + do_check_thrown(function () { cps.removePrefsByName(null); }); + + do_check_thrown(function () { cps.setPref(uri, "", 21); }); + do_check_thrown(function () { cps.hasPref(uri, ""); }); + do_check_thrown(function () { cps.getPref(uri, ""); }); + do_check_thrown(function () { cps.removePref(uri, ""); }); + do_check_thrown(function () { cps.getPrefsByName(""); }); + do_check_thrown(function () { cps.removePrefsByName(""); }); +} + +function do_check_thrown (aCallback) { + var exThrown = false; + try { + aCallback(); + do_throw("NS_ERROR_ILLEGAL_VALUE should have been thrown here"); + } catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + exThrown = true; + } + do_check_true(exThrown); +} diff --git a/toolkit/components/contentprefs/tests/unit/test_bug679784.js b/toolkit/components/contentprefs/tests/unit/test_bug679784.js new file mode 100644 index 000000000..97251d87b --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit/test_bug679784.js @@ -0,0 +1,103 @@ +/* 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/. */ + + +var prefObserver = { + setCalledNum: 0, + onContentPrefSet: function(aGroup, aName, aValue) { + this.setCalledNum++; + }, + removedCalledNum: 0, + onContentPrefRemoved: function(aGroup, aName) { + this.removedCalledNum++; + } +}; + +function run_test() { + let loadContext = { get usePrivateBrowsing() { return gInPrivateBrowsing; } }; + + var cps = new ContentPrefInstance(loadContext); + cps.removeGroupedPrefs(); + + var uri = ContentPrefTest.getURI("http://www.example.com/"); + var group = cps.grouper.group(uri); + + // first, set a pref in normal mode + cps.setPref(uri, "value", "foo"); + cps.setPref(null, "value-global", "foo-global"); + + var num; + cps.addObserver("value", prefObserver); + cps.addObserver("value-global", prefObserver); + + enterPBMode(); + + // test setPref + num = prefObserver.setCalledNum; + cps.setPref(uri, "value", "foo-private-browsing"); + do_check_eq(cps.hasPref(uri, "value"), true); + do_check_eq(cps.getPref(uri, "value"), "foo-private-browsing"); + do_check_eq(prefObserver.setCalledNum, num + 1); + + num = prefObserver.setCalledNum; + cps.setPref(null, "value-global", "foo-private-browsing-global"); + do_check_eq(cps.hasPref(null, "value-global"), true); + do_check_eq(cps.getPref(null, "value-global"), "foo-private-browsing-global"); + do_check_eq(prefObserver.setCalledNum, num + 1); + + // test removePref + num = prefObserver.removedCalledNum; + cps.removePref(uri, "value"); + do_check_eq(cps.hasPref(uri, "value"), true); + // fallback to non private mode value + do_check_eq(cps.getPref(uri, "value"), "foo"); + do_check_eq(prefObserver.removedCalledNum, num + 1); + + num = prefObserver.removedCalledNum; + cps.removePref(null, "value-global"); + do_check_eq(cps.hasPref(null, "value-global"), true); + // fallback to non private mode value + do_check_eq(cps.getPref(null, "value-global"), "foo-global") ; + do_check_eq(prefObserver.removedCalledNum, num + 1); + + // test removeGroupedPrefs + cps.setPref(uri, "value", "foo-private-browsing"); + cps.removeGroupedPrefs(); + do_check_eq(cps.hasPref(uri, "value"), false); + do_check_eq(cps.getPref(uri, "value"), undefined); + + cps.setPref(null, "value-global", "foo-private-browsing-global"); + cps.removeGroupedPrefs(); + do_check_eq(cps.hasPref(null, "value-global"), true); + do_check_eq(cps.getPref(null, "value-global"), "foo-private-browsing-global"); + + // test removePrefsByName + num = prefObserver.removedCalledNum; + cps.setPref(uri, "value", "foo-private-browsing"); + cps.removePrefsByName("value"); + do_check_eq(cps.hasPref(uri, "value"), false); + do_check_eq(cps.getPref(uri, "value"), undefined); + do_check_true(prefObserver.removedCalledNum > num); + + num = prefObserver.removedCalledNum; + cps.setPref(null, "value-global", "foo-private-browsing"); + cps.removePrefsByName("value-global"); + do_check_eq(cps.hasPref(null, "value-global"), false); + do_check_eq(cps.getPref(null, "value-global"), undefined); + do_check_true(prefObserver.removedCalledNum > num); + + // test getPrefs + cps.setPref(uri, "value", "foo-private-browsing"); + do_check_eq(cps.getPrefs(uri).getProperty("value"), "foo-private-browsing"); + + cps.setPref(null, "value-global", "foo-private-browsing-global"); + do_check_eq(cps.getPrefs(null).getProperty("value-global"), "foo-private-browsing-global"); + + // test getPrefsByName + do_check_eq(cps.getPrefsByName("value").getProperty(group), "foo-private-browsing"); + do_check_eq(cps.getPrefsByName("value-global").getProperty(null), "foo-private-browsing-global"); + + cps.removeObserver("value", prefObserver); + cps.removeObserver("value-global", prefObserver); +} diff --git a/toolkit/components/contentprefs/tests/unit/test_contentPrefs.js b/toolkit/components/contentprefs/tests/unit/test_contentPrefs.js new file mode 100644 index 000000000..f7e99ea9d --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit/test_contentPrefs.js @@ -0,0 +1,463 @@ +/* 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/. */ + +function run_test() { + // Database Creation, Schema Migration, and Backup + + // Note: in these tests we use createInstance instead of getService + // so we can instantiate the service multiple times and make it run + // its database initialization code each time. + + // Create a new database. + { + ContentPrefTest.deleteDatabase(); + + // Get the service and make sure it has a ready database connection. + let cps = Cc["@mozilla.org/content-pref/service;1"]. + createInstance(Ci.nsIContentPrefService); + do_check_true(cps.DBConnection.connectionReady); + cps.DBConnection.close(); + } + + // Open an existing database. + { + let dbFile = ContentPrefTest.deleteDatabase(); + + let cps = Cc["@mozilla.org/content-pref/service;1"]. + createInstance(Ci.nsIContentPrefService); + cps.DBConnection.close(); + do_check_true(dbFile.exists()); + + // Get the service and make sure it has a ready database connection. + cps = Cc["@mozilla.org/content-pref/service;1"]. + createInstance(Ci.nsIContentPrefService); + do_check_true(cps.DBConnection.connectionReady); + cps.DBConnection.close(); + } + + // Open an empty database. + { + let dbFile = ContentPrefTest.deleteDatabase(); + + // Create an empty database. + let dbService = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + let dbConnection = dbService.openDatabase(dbFile); + do_check_eq(dbConnection.schemaVersion, 0); + dbConnection.close(); + do_check_true(dbFile.exists()); + + // Get the service and make sure it has created the schema. + let cps = Cc["@mozilla.org/content-pref/service;1"]. + createInstance(Ci.nsIContentPrefService); + do_check_neq(cps.DBConnection.schemaVersion, 0); + cps.DBConnection.close(); + } + + // Open a corrupted database. + { + let dbFile = ContentPrefTest.deleteDatabase(); + let backupDBFile = ContentPrefTest.deleteBackupDatabase(); + + // Create a corrupted database. + let foStream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + foStream.init(dbFile, 0x02 | 0x08 | 0x20, 0o666, 0); + let garbageData = "garbage that makes SQLite think the file is corrupted"; + foStream.write(garbageData, garbageData.length); + foStream.close(); + + // Get the service and make sure it backs up and recreates the database. + let cps = Cc["@mozilla.org/content-pref/service;1"]. + createInstance(Ci.nsIContentPrefService); + do_check_true(backupDBFile.exists()); + do_check_true(cps.DBConnection.connectionReady); + + cps.DBConnection.close(); + } + + // Open a database with a corrupted schema. + { + let dbFile = ContentPrefTest.deleteDatabase(); + let backupDBFile = ContentPrefTest.deleteBackupDatabase(); + + // Create an empty database and set the schema version to a number + // that will trigger a schema migration that will fail. + let dbService = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + let dbConnection = dbService.openDatabase(dbFile); + dbConnection.schemaVersion = -1; + dbConnection.close(); + do_check_true(dbFile.exists()); + + // Get the service and make sure it backs up and recreates the database. + let cps = Cc["@mozilla.org/content-pref/service;1"]. + createInstance(Ci.nsIContentPrefService); + do_check_true(backupDBFile.exists()); + do_check_true(cps.DBConnection.connectionReady); + + cps.DBConnection.close(); + } + + + // Now get the content pref service for real for use by the rest of the tests. + let cps = new ContentPrefInstance(null); + + var uri = ContentPrefTest.getURI("http://www.example.com/"); + + // Make sure disk synchronization checking is turned off by default. + var statement = cps.DBConnection.createStatement("PRAGMA synchronous"); + statement.executeStep(); + do_check_eq(0, statement.getInt32(0)); + + // Nonexistent Pref + + do_check_eq(cps.getPref(uri, "test.nonexistent.getPref"), undefined); + do_check_eq(cps.setPref(uri, "test.nonexistent.setPref", 5), undefined); + do_check_false(cps.hasPref(uri, "test.nonexistent.hasPref")); + do_check_eq(cps.removePref(uri, "test.nonexistent.removePref"), undefined); + + + // Existing Pref + + cps.setPref(uri, "test.existing", 5); + + // getPref should return the pref value + do_check_eq(cps.getPref(uri, "test.existing"), 5); + + // setPref should return undefined and change the value of the pref + do_check_eq(cps.setPref(uri, "test.existing", 6), undefined); + do_check_eq(cps.getPref(uri, "test.existing"), 6); + + // hasPref should return true + do_check_true(cps.hasPref(uri, "test.existing")); + + // removePref should return undefined and remove the pref + do_check_eq(cps.removePref(uri, "test.existing"), undefined); + do_check_false(cps.hasPref(uri, "test.existing")); + + + // Round-Trip Data Integrity + + // Make sure pref values remain the same from setPref to getPref. + + cps.setPref(uri, "test.data-integrity.integer", 5); + do_check_eq(cps.getPref(uri, "test.data-integrity.integer"), 5); + + cps.setPref(uri, "test.data-integrity.float", 5.5); + do_check_eq(cps.getPref(uri, "test.data-integrity.float"), 5.5); + + cps.setPref(uri, "test.data-integrity.boolean", true); + do_check_eq(cps.getPref(uri, "test.data-integrity.boolean"), true); + + cps.setPref(uri, "test.data-integrity.string", "test"); + do_check_eq(cps.getPref(uri, "test.data-integrity.string"), "test"); + + cps.setPref(uri, "test.data-integrity.null", null); + do_check_eq(cps.getPref(uri, "test.data-integrity.null"), null); + + // XXX Test arbitrary binary data. + + // Make sure hasPref and removePref work on all data types. + + do_check_true(cps.hasPref(uri, "test.data-integrity.integer")); + do_check_true(cps.hasPref(uri, "test.data-integrity.float")); + do_check_true(cps.hasPref(uri, "test.data-integrity.boolean")); + do_check_true(cps.hasPref(uri, "test.data-integrity.string")); + do_check_true(cps.hasPref(uri, "test.data-integrity.null")); + + do_check_eq(cps.removePref(uri, "test.data-integrity.integer"), undefined); + do_check_eq(cps.removePref(uri, "test.data-integrity.float"), undefined); + do_check_eq(cps.removePref(uri, "test.data-integrity.boolean"), undefined); + do_check_eq(cps.removePref(uri, "test.data-integrity.string"), undefined); + do_check_eq(cps.removePref(uri, "test.data-integrity.null"), undefined); + + do_check_false(cps.hasPref(uri, "test.data-integrity.integer")); + do_check_false(cps.hasPref(uri, "test.data-integrity.float")); + do_check_false(cps.hasPref(uri, "test.data-integrity.boolean")); + do_check_false(cps.hasPref(uri, "test.data-integrity.string")); + do_check_false(cps.hasPref(uri, "test.data-integrity.null")); + + + // getPrefs + + cps.setPref(uri, "test.getPrefs.a", 1); + cps.setPref(uri, "test.getPrefs.b", 2); + cps.setPref(uri, "test.getPrefs.c", 3); + + var prefs = cps.getPrefs(uri); + do_check_true(prefs.hasKey("test.getPrefs.a")); + do_check_eq(prefs.get("test.getPrefs.a"), 1); + do_check_true(prefs.hasKey("test.getPrefs.b")); + do_check_eq(prefs.get("test.getPrefs.b"), 2); + do_check_true(prefs.hasKey("test.getPrefs.c")); + do_check_eq(prefs.get("test.getPrefs.c"), 3); + + + // Site-Specificity + + { + // These are all different sites, and setting a pref for one of them + // shouldn't set it for the others. + let uri1 = ContentPrefTest.getURI("http://www.domain1.com/"); + let uri2 = ContentPrefTest.getURI("http://foo.domain1.com/"); + let uri3 = ContentPrefTest.getURI("http://domain1.com/"); + let uri4 = ContentPrefTest.getURI("http://www.domain2.com/"); + + cps.setPref(uri1, "test.site-specificity.uri1", 5); + do_check_false(cps.hasPref(uri2, "test.site-specificity.uri1")); + do_check_false(cps.hasPref(uri3, "test.site-specificity.uri1")); + do_check_false(cps.hasPref(uri4, "test.site-specificity.uri1")); + + cps.setPref(uri2, "test.site-specificity.uri2", 5); + do_check_false(cps.hasPref(uri1, "test.site-specificity.uri2")); + do_check_false(cps.hasPref(uri3, "test.site-specificity.uri2")); + do_check_false(cps.hasPref(uri4, "test.site-specificity.uri2")); + + cps.setPref(uri3, "test.site-specificity.uri3", 5); + do_check_false(cps.hasPref(uri1, "test.site-specificity.uri3")); + do_check_false(cps.hasPref(uri2, "test.site-specificity.uri3")); + do_check_false(cps.hasPref(uri4, "test.site-specificity.uri3")); + + cps.setPref(uri4, "test.site-specificity.uri4", 5); + do_check_false(cps.hasPref(uri1, "test.site-specificity.uri4")); + do_check_false(cps.hasPref(uri2, "test.site-specificity.uri4")); + do_check_false(cps.hasPref(uri3, "test.site-specificity.uri4")); + } + + // Observers + + var specificObserver = { + interfaces: [Ci.nsIContentPrefObserver, Ci.nsISupports], + + QueryInterface: function ContentPrefTest_QueryInterface(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + }, + + numTimesSetCalled: 0, + onContentPrefSet: function specificObserver_onContentPrefSet(group, name, value) { + ++this.numTimesSetCalled; + do_check_eq(group, "www.example.com"); + do_check_eq(name, "test.observer.1"); + do_check_eq(value, "test value"); + }, + + numTimesRemovedCalled: 0, + onContentPrefRemoved: function specificObserver_onContentPrefRemoved(group, name) { + ++this.numTimesRemovedCalled; + do_check_eq(group, "www.example.com"); + do_check_eq(name, "test.observer.1"); + } + + }; + + var genericObserver = { + interfaces: [Ci.nsIContentPrefObserver, Ci.nsISupports], + + QueryInterface: function ContentPrefTest_QueryInterface(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + }, + + numTimesSetCalled: 0, + onContentPrefSet: function genericObserver_onContentPrefSet(group, name, value, isPrivate) { + ++this.numTimesSetCalled; + do_check_eq(group, "www.example.com"); + if (name == "test.observer.private") + do_check_true(isPrivate); + else if (name == "test.observer.normal") + do_check_false(isPrivate); + else if (name != "test.observer.1" && name != "test.observer.2") + do_throw("genericObserver.onContentPrefSet: " + + "name not in (test.observer.(1|2|normal|private))"); + do_check_eq(value, "test value"); + }, + + numTimesRemovedCalled: 0, + onContentPrefRemoved: function genericObserver_onContentPrefRemoved(group, name, isPrivate) { + ++this.numTimesRemovedCalled; + do_check_eq(group, "www.example.com"); + if (name == "test.observer.private") + do_check_true(isPrivate); + else if (name == "test.observer.normal") + do_check_false(isPrivate); + if (name != "test.observer.1" && name != "test.observer.2" && + name != "test.observer.normal" && name != "test.observer.private") { + do_throw("genericObserver.onContentPrefSet: " + + "name not in (test.observer.(1|2|normal|private))"); + } + } + + }; + + // Make sure we can add observers, observers get notified about changes, + // specific observers only get notified about changes to the specific setting, + // and generic observers get notified about changes to all settings. + cps.addObserver("test.observer.1", specificObserver); + cps.addObserver(null, genericObserver); + cps.setPref(uri, "test.observer.1", "test value"); + cps.setPref(uri, "test.observer.2", "test value"); + cps.removePref(uri, "test.observer.1"); + cps.removePref(uri, "test.observer.2"); + do_check_eq(specificObserver.numTimesSetCalled, 1); + do_check_eq(genericObserver.numTimesSetCalled, 2); + do_check_eq(specificObserver.numTimesRemovedCalled, 1); + do_check_eq(genericObserver.numTimesRemovedCalled, 2); + + // Make sure information about private context is properly + // retrieved by the observer. + cps.setPref(uri, "test.observer.private", "test value", {usePrivateBrowsing: true}); + cps.setPref(uri, "test.observer.normal", "test value", {usePrivateBrowsing: false}); + cps.removePref(uri, "test.observer.private"); + cps.removePref(uri, "test.observer.normal"); + + // Make sure we can remove observers and they don't get notified + // about changes anymore. + cps.removeObserver("test.observer.1", specificObserver); + cps.removeObserver(null, genericObserver); + cps.setPref(uri, "test.observer.1", "test value"); + cps.removePref(uri, "test.observer.1", "test value"); + do_check_eq(specificObserver.numTimesSetCalled, 1); + do_check_eq(genericObserver.numTimesSetCalled, 4); + do_check_eq(specificObserver.numTimesRemovedCalled, 1); + do_check_eq(genericObserver.numTimesRemovedCalled, 3); + + + // Get/Remove Prefs By Name + + { + var anObserver = { + interfaces: [Ci.nsIContentPrefObserver, Ci.nsISupports], + + QueryInterface: function ContentPrefTest_QueryInterface(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + }, + + onContentPrefSet: function anObserver_onContentPrefSet(group, name, value) { + }, + + expectedDomains: [], + numTimesRemovedCalled: 0, + onContentPrefRemoved: function anObserver_onContentPrefRemoved(group, name) { + ++this.numTimesRemovedCalled; + + // remove the domain from the list of expected domains + var index = this.expectedDomains.indexOf(group); + do_check_true(index >= 0); + this.expectedDomains.splice(index, 1); + } + }; + + let uri1 = ContentPrefTest.getURI("http://www.domain1.com/"); + let uri2 = ContentPrefTest.getURI("http://foo.domain1.com/"); + let uri3 = ContentPrefTest.getURI("http://domain1.com/"); + let uri4 = ContentPrefTest.getURI("http://www.domain2.com/"); + + cps.setPref(uri1, "test.byname.1", 1); + cps.setPref(uri1, "test.byname.2", 2); + cps.setPref(uri2, "test.byname.1", 4); + cps.setPref(uri3, "test.byname.3", 8); + cps.setPref(uri4, "test.byname.1", 16); + cps.setPref(null, "test.byname.1", 32); + cps.setPref(null, "test.byname.2", false); + + function enumerateAndCheck(testName, expectedSum, expectedDomains) { + var prefsByName = cps.getPrefsByName(testName); + var enumerator = prefsByName.enumerator; + var sum = 0; + while (enumerator.hasMoreElements()) { + var property = enumerator.getNext().QueryInterface(Components.interfaces.nsIProperty); + sum += parseInt(property.value); + + // remove the domain from the list of expected domains + var index = expectedDomains.indexOf(property.name); + do_check_true(index >= 0); + expectedDomains.splice(index, 1); + } + do_check_eq(sum, expectedSum); + // check all domains have been removed from the array + do_check_eq(expectedDomains.length, 0); + } + + enumerateAndCheck("test.byname.1", 53, + ["foo.domain1.com", null, "www.domain1.com", "www.domain2.com"]); + enumerateAndCheck("test.byname.2", 2, ["www.domain1.com", null]); + enumerateAndCheck("test.byname.3", 8, ["domain1.com"]); + + cps.addObserver("test.byname.1", anObserver); + anObserver.expectedDomains = ["foo.domain1.com", null, "www.domain1.com", "www.domain2.com"]; + + cps.removePrefsByName("test.byname.1"); + do_check_false(cps.hasPref(uri1, "test.byname.1")); + do_check_false(cps.hasPref(uri2, "test.byname.1")); + do_check_false(cps.hasPref(uri3, "test.byname.1")); + do_check_false(cps.hasPref(uri4, "test.byname.1")); + do_check_false(cps.hasPref(null, "test.byname.1")); + do_check_true(cps.hasPref(uri1, "test.byname.2")); + do_check_true(cps.hasPref(uri3, "test.byname.3")); + + do_check_eq(anObserver.numTimesRemovedCalled, 4); + do_check_eq(anObserver.expectedDomains.length, 0); + + cps.removeObserver("test.byname.1", anObserver); + + // Clean up after ourselves + cps.removePref(uri1, "test.byname.2"); + cps.removePref(uri3, "test.byname.3"); + cps.removePref(null, "test.byname.2"); + } + + + // Clear Private Data Pref Removal + + { + let uri1 = ContentPrefTest.getURI("http://www.domain1.com/"); + let uri2 = ContentPrefTest.getURI("http://www.domain2.com/"); + let uri3 = ContentPrefTest.getURI("http://www.domain3.com/"); + + let dbConnection = cps.DBConnection; + + let prefCount = dbConnection.createStatement("SELECT COUNT(*) AS count FROM prefs"); + + let groupCount = dbConnection.createStatement("SELECT COUNT(*) AS count FROM groups"); + + // Add some prefs for multiple domains. + cps.setPref(uri1, "test.removeAllGroups", 1); + cps.setPref(uri2, "test.removeAllGroups", 2); + cps.setPref(uri3, "test.removeAllGroups", 3); + + // Add a global pref. + cps.setPref(null, "test.removeAllGroups", 1); + + // Make sure there are some prefs and groups in the database. + prefCount.executeStep(); + do_check_true(prefCount.row.count > 0); + prefCount.reset(); + groupCount.executeStep(); + do_check_true(groupCount.row.count > 0); + groupCount.reset(); + + // Remove all prefs and groups from the database using the same routine + // the Clear Private Data dialog uses. + cps.removeGroupedPrefs(); + + // Make sure there are no longer any groups in the database and the only pref + // is the global one. + prefCount.executeStep(); + do_check_true(prefCount.row.count == 1); + prefCount.reset(); + groupCount.executeStep(); + do_check_true(groupCount.row.count == 0); + groupCount.reset(); + let globalPref = dbConnection.createStatement("SELECT groupID FROM prefs"); + globalPref.executeStep(); + do_check_true(globalPref.row.groupID == null); + globalPref.reset(); + } +} diff --git a/toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js b/toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js new file mode 100644 index 000000000..38a2faddc --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js @@ -0,0 +1,244 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var cps = new ContentPrefInstance(null); + +function run_test() { + testCacheWorks("test1.example.com", "test-pref1"); + testHasCachedPrefFunction("test2.example.com", "test-pref2"); + testSetCaches("test3.example.com", "test-pref3"); + testGetCaches("test4.example.com", "test-pref4"); + testRemovePrefs("test5.example.com", "test-pref5"); + testTypeConversions("test6.example.com", "test-pref6"); + testNonExistingPrefCachesAsUndefined("test7.example.com", "test-pref7"); + testCacheEviction("test8.example.com", "test-pref8"); +} + +function testCacheWorks(uri, prefName) { + const CACHED_VALUE = 3; + const NEW_VALUE = 5; + + cps.setPref(uri, prefName, CACHED_VALUE); + do_check_eq(cps.getPref(uri, prefName), CACHED_VALUE); + + // Now change the value directly through the DB and check + // that the cached value is different + + let groupId = selectValue("SELECT id FROM groups WHERE name = :param1", "id", uri); + let settingId = selectValue("SELECT id FROM settings WHERE name = :param1", "id", prefName); + let prefId = selectValue("SELECT id FROM prefs WHERE groupID = :param1 AND settingID = :param2", + "id", groupId, settingId); + + let stmt = cps.DBConnection.createStatement("UPDATE prefs SET value = :value WHERE id = :id"); + stmt.params.value = NEW_VALUE; + stmt.params.id = prefId; + stmt.execute(); + + let dbValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId); + let cacheValue = cps.getPref(uri, prefName); + + do_check_eq(dbValue, NEW_VALUE); + do_check_eq(cacheValue, CACHED_VALUE); + do_check_neq(cacheValue, dbValue); + + do_test_pending(); + cps.getPref(uri, prefName, function (value) { + do_check_eq(dbValue, NEW_VALUE); + do_check_eq(value, CACHED_VALUE); + do_check_neq(value, dbValue); + do_test_finished(); + }); +} + +function testHasCachedPrefFunction(uri, prefName) { + const STARTING_VALUE = 3; + const NEW_VALUE = 5; + + do_check_false(isCached(uri, prefName)); + + cps.setPref(uri, prefName, STARTING_VALUE); + + let groupId = selectValue("SELECT id FROM groups WHERE name = :param1", "id", uri); + let settingId = selectValue("SELECT id FROM settings WHERE name = :param1", "id", prefName); + let prefId = selectValue("SELECT id FROM prefs WHERE groupID = :param1 AND settingID = :param2", + "id", groupId, settingId); + + do_check_neq(prefId, undefined); + + let originalValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId); + do_check_eq(originalValue, STARTING_VALUE); + + let stmt = cps.DBConnection.createStatement("UPDATE prefs SET value = :value WHERE id = :id"); + stmt.params.value = NEW_VALUE; + stmt.params.id = prefId; + stmt.execute(); + + let newValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId); + do_check_eq(newValue, NEW_VALUE); + + let cachedValue = cps.getPref(uri, prefName); + do_check_eq(cachedValue, STARTING_VALUE); + do_check_true(isCached(uri, prefName)); +} + +function testSetCaches(uri, prefName) { + cps.setPref(uri, prefName, 0); + do_check_true(isCached(uri, prefName)); +} + +function testRemovePrefs(uri, prefName) { + + /* removePref */ + cps.setPref("www1." + uri, prefName, 1); + + do_check_eq(cps.getPref("www1." + uri, prefName), 1); + + cps.removePref("www1." + uri, prefName); + + do_check_false(isCached("www1." + uri, prefName)); + do_check_false(cps.hasPref("www1." + uri, prefName)); + do_check_neq(cps.getPref("www1." + uri, prefName), 1); + + /* removeGroupedPrefs */ + cps.setPref("www2." + uri, prefName, 2); + cps.setPref("www3." + uri, prefName, 3); + + do_check_eq(cps.getPref("www2." + uri, prefName), 2); + do_check_eq(cps.getPref("www3." + uri, prefName), 3); + + cps.removeGroupedPrefs(); + + do_check_false(isCached("www2." + uri, prefName)); + do_check_false(isCached("www3." + uri, prefName)); + do_check_false(cps.hasPref("www2." + uri, prefName)); + do_check_false(cps.hasPref("www3." + uri, prefName)); + do_check_neq(cps.getPref("www2." + uri, prefName), 2); + do_check_neq(cps.getPref("www3." + uri, prefName), 3); + + /* removePrefsByName */ + cps.setPref("www4." + uri, prefName, 4); + cps.setPref("www5." + uri, prefName, 5); + + do_check_eq(cps.getPref("www4." + uri, prefName), 4); + do_check_eq(cps.getPref("www5." + uri, prefName), 5); + + cps.removePrefsByName(prefName); + + do_check_false(isCached("www4." + uri, prefName)); + do_check_false(isCached("www5." + uri, prefName)); + do_check_false(cps.hasPref("www4." + uri, prefName)); + do_check_false(cps.hasPref("www5." + uri, prefName)); + do_check_neq(cps.getPref("www4." + uri, prefName), 4); + do_check_neq(cps.getPref("www5." + uri, prefName), 5); +} + +function testGetCaches(uri, prefName) { + const VALUE = 4; + + let insertGroup = cps.DBConnection.createStatement("INSERT INTO groups (name) VALUES (:name)"); + insertGroup.params.name = uri; + insertGroup.execute(); + let groupId = cps.DBConnection.lastInsertRowID; + + let insertSetting = cps.DBConnection.createStatement("INSERT INTO settings (name) VALUES (:name)"); + insertSetting.params.name = prefName; + insertSetting.execute(); + let settingId = cps.DBConnection.lastInsertRowID; + + let insertPref = cps.DBConnection.createStatement(` + INSERT INTO prefs (groupID, settingID, value) + VALUES (:groupId, :settingId, :value) + `); + insertPref.params.groupId = groupId; + insertPref.params.settingId = settingId; + insertPref.params.value = VALUE; + insertPref.execute(); + let prefId = cps.DBConnection.lastInsertRowID; + + let dbValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId); + + // First access from service should hit the DB + let svcValue = cps.getPref(uri, prefName); + + // Second time should get the value from cache + let cacheValue = cps.getPref(uri, prefName); + + do_check_eq(VALUE, dbValue); + do_check_eq(VALUE, svcValue); + do_check_eq(VALUE, cacheValue); + + do_check_true(isCached(uri, prefName)); +} + +function testTypeConversions(uri, prefName) { + let value; + + cps.setPref(uri, prefName, true); + value = cps.getPref(uri, prefName); + do_check_true(value === 1); + + cps.setPref(uri, prefName, false); + value = cps.getPref(uri, prefName); + do_check_true(value === 0); + + cps.setPref(uri, prefName, null); + value = cps.getPref(uri, prefName); + do_check_true(value === null); + + cps.setPref(uri, prefName, undefined); + value = cps.getPref(uri, prefName); + do_check_true(value === null); +} + +function testNonExistingPrefCachesAsUndefined(uri, prefName) { + + do_check_false(isCached(uri, prefName)); + + // Cache the pref + let value = cps.getPref(uri, prefName); + do_check_true(value === undefined); + + do_check_true(isCached(uri, prefName)); + + // Cached pref + value = cps.getPref(uri, prefName); + do_check_true(value === undefined); +} + +function testCacheEviction(uri, prefName) { + + cps.setPref(uri, prefName, 5); + do_check_eq(cps.getPref(uri, prefName), 5); + do_check_true(isCached(uri, prefName)); + + // try to evict value from cache by adding various other entries + const ENTRIES_TO_ADD = 200; + for (let i = 0; i < ENTRIES_TO_ADD; i++) { + let uriToAdd = "www" + i + uri; + cps.setPref(uriToAdd, prefName, 0); + } + + do_check_false(isCached(uri, prefName)); + +} + +function selectValue(stmt, columnName, param1, param2) { + stmt = cps.DBConnection.createStatement(stmt); + if (param1) + stmt.params.param1 = param1; + + if (param2) + stmt.params.param2 = param2; + + stmt.executeStep(); + let val = stmt.row[columnName]; + stmt.reset(); + stmt.finalize(); + return val; +} + +function isCached(uri, prefName) { + return cps.hasCachedPref(uri, prefName); +} diff --git a/toolkit/components/contentprefs/tests/unit/test_getPrefAsync.js b/toolkit/components/contentprefs/tests/unit/test_getPrefAsync.js new file mode 100644 index 000000000..27d239f79 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit/test_getPrefAsync.js @@ -0,0 +1,34 @@ +/* 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/. */ +var cps = new ContentPrefInstance(null); +var uri = ContentPrefTest.getURI("http://www.example.com/"); + +function run_test() { + do_test_pending(); + + cps.setPref(uri, "asynctest", "pie"); + do_check_eq(cps.getPref(uri, "asynctest"), "pie"); + + cps.getPref(uri, "asynctest", function(aValue) { + do_check_eq(aValue, "pie"); + testCallbackObj(); + }); +} + +function testCallbackObj() { + cps.getPref(uri, "asynctest", { + onResult: function(aValue) { + do_check_eq(aValue, "pie"); + cps.removePref(uri, "asynctest"); + testNoResult(); + } + }); +} + +function testNoResult() { + cps.getPref(uri, "asynctest", function(aValue) { + do_check_eq(aValue, undefined); + do_test_finished(); + }); +} diff --git a/toolkit/components/contentprefs/tests/unit/test_stringGroups.js b/toolkit/components/contentprefs/tests/unit/test_stringGroups.js new file mode 100644 index 000000000..afce3b64a --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit/test_stringGroups.js @@ -0,0 +1,128 @@ +/* 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/. */ + +function run_test() { + + var cps = new ContentPrefInstance(null); + + // Make sure disk synchronization checking is turned off by default. + var statement = cps.DBConnection.createStatement("PRAGMA synchronous"); + statement.executeStep(); + do_check_eq(0, statement.getInt32(0)); + + // These are the different types of aGroup arguments we'll test. + var anObject = {"foo":"bar"}; // a simple object + var uri = ContentPrefTest.getURI("http://www.example.com/"); // nsIURI + var stringURI = "www.example.com"; // typeof = "string" + var stringObjectURI = new String("www.example.com"); // typeof = "object" + + { + // First check that all the methods work or don't work. + function simple_test_methods(aGroup, shouldThrow) { + var prefName = "test.pref.0"; + var prefValue = Math.floor(Math.random() * 100); + + if (shouldThrow) { + do_check_thrown(function () { cps.getPref(aGroup, prefName); }); + do_check_thrown(function () { cps.setPref(aGroup, prefName, prefValue); }); + do_check_thrown(function () { cps.hasPref(aGroup, prefName); }); + do_check_thrown(function () { cps.removePref(aGroup, prefName); }); + do_check_thrown(function () { cps.getPrefs(aGroup); }); + } else { + do_check_eq(cps.setPref(aGroup, prefName, prefValue), undefined); + do_check_true(cps.hasPref(aGroup, prefName)); + do_check_eq(cps.getPref(aGroup, prefName), prefValue); + do_check_eq(cps.removePref(aGroup, prefName), undefined); + do_check_false(cps.hasPref(aGroup, prefName)); + } + } + + simple_test_methods(cps, true); // arbitrary nsISupports object, should throw too + simple_test_methods(anObject, true); + simple_test_methods(uri, false); + simple_test_methods(stringURI, false); + simple_test_methods(stringObjectURI, false); + } + + { + // Now we'll check that each argument produces the same result. + function complex_test_methods(aGroup) { + var prefName = "test.pref.1"; + var prefValue = Math.floor(Math.random() * 100); + + do_check_eq(cps.setPref(aGroup, prefName, prefValue), undefined); + + do_check_true(cps.hasPref(uri, prefName)); + do_check_true(cps.hasPref(stringURI, prefName)); + do_check_true(cps.hasPref(stringObjectURI, prefName)); + + do_check_eq(cps.getPref(uri, prefName), prefValue); + do_check_eq(cps.getPref(stringURI, prefName), prefValue); + do_check_eq(cps.getPref(stringObjectURI, prefName), prefValue); + + do_check_eq(cps.removePref(aGroup, prefName), undefined); + + do_check_false(cps.hasPref(uri, prefName)); + do_check_false(cps.hasPref(stringURI, prefName)); + do_check_false(cps.hasPref(stringObjectURI, prefName)); + } + + complex_test_methods(uri); + complex_test_methods(stringURI); + complex_test_methods(stringObjectURI); + } + + { + // test getPrefs returns the same prefs + do_check_eq(cps.setPref(stringObjectURI, "test.5", 5), undefined); + do_check_eq(cps.setPref(stringURI, "test.2", 2), undefined); + do_check_eq(cps.setPref(uri, "test.1", 1), undefined); + + enumerateAndCheck(cps.getPrefs(uri), 8, ["test.1", "test.2", "test.5"]); + enumerateAndCheck(cps.getPrefs(stringURI), 8, ["test.1", "test.2", "test.5"]); + enumerateAndCheck(cps.getPrefs(stringObjectURI), 8, ["test.1", "test.2", "test.5"]); + + do_check_eq(cps.setPref(uri, "test.4", 4), undefined); + do_check_eq(cps.setPref(stringObjectURI, "test.0", 0), undefined); + + enumerateAndCheck(cps.getPrefs(uri), 12, ["test.0", "test.1", "test.2", "test.4", "test.5"]); + enumerateAndCheck(cps.getPrefs(stringURI), 12, ["test.0", "test.1", "test.2", "test.4", "test.5"]); + enumerateAndCheck(cps.getPrefs(stringObjectURI), 12, ["test.0", "test.1", "test.2", "test.4", "test.5"]); + + do_check_eq(cps.setPref(stringURI, "test.3", 3), undefined); + + enumerateAndCheck(cps.getPrefs(uri), 15, ["test.0", "test.1", "test.2", "test.3", "test.4", "test.5"]); + enumerateAndCheck(cps.getPrefs(stringURI), 15, ["test.0", "test.1", "test.2", "test.3", "test.4", "test.5"]); + enumerateAndCheck(cps.getPrefs(stringObjectURI), 15, ["test.0", "test.1", "test.2", "test.3", "test.4", "test.5"]); + } +} + +function do_check_thrown (aCallback) { + var exThrown = false; + try { + aCallback(); + do_throw("NS_ERROR_ILLEGAL_VALUE should have been thrown here"); + } catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + exThrown = true; + } + do_check_true(exThrown); +} + +function enumerateAndCheck(prefs, expectedSum, expectedNames) { + var enumerator = prefs.enumerator; + var sum = 0; + while (enumerator.hasMoreElements()) { + var property = enumerator.getNext().QueryInterface(Components.interfaces.nsIProperty); + sum += parseInt(property.value); + + // remove the pref name from the list of expected names + var index = expectedNames.indexOf(property.name); + do_check_true(index >= 0); + expectedNames.splice(index, 1); + } + do_check_eq(sum, expectedSum); + // check all pref names have been removed from the array + do_check_eq(expectedNames.length, 0); +} diff --git a/toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.js b/toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.js new file mode 100644 index 000000000..24a86bcc0 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.js @@ -0,0 +1,52 @@ +/* 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/. */ + +var cps = new ContentPrefInstance(null); + +function run_test() { + var uri1 = ContentPrefTest.getURI("http://www.domain1.com/"); + var uri2 = ContentPrefTest.getURI("http://foo.domain1.com/"); + var uri3 = ContentPrefTest.getURI("http://domain1.com/"); + var uri4 = ContentPrefTest.getURI("http://www.domain2.com/"); + + cps.setPref(uri1, "one", 1); + cps.setPref(uri1, "two", 2); + cps.setPref(uri2, "one", 4); + cps.setPref(uri3, "three", 8); + cps.setPref(uri4, "two", 16); + + cps.removePref(uri3, "three"); // uri3 should be removed now + checkForUnusedGroups(); + checkForUnusedSettings(); + + cps.removePrefsByName("two"); // uri4 should be removed now + checkForUnusedGroups(); + checkForUnusedSettings(); + + cps.removeGroupedPrefs(); + checkForUnusedGroups(); + checkForUnusedSettings(); +} + +function checkForUnusedGroups() { + var stmt = cps.DBConnection.createStatement(` + SELECT COUNT(*) AS count FROM groups + WHERE id NOT IN (SELECT DISTINCT groupID FROM prefs) + `); + stmt.executeStep(); + do_check_eq(0, stmt.row.count); + stmt.reset(); + stmt.finalize(); +} + +function checkForUnusedSettings() { + var stmt = cps.DBConnection.createStatement(` + SELECT COUNT(*) AS count FROM settings + WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs) + `); + stmt.executeStep(); + do_check_eq(0, stmt.row.count); + stmt.reset(); + stmt.finalize(); +} diff --git a/toolkit/components/contentprefs/tests/unit/xpcshell.ini b/toolkit/components/contentprefs/tests/unit/xpcshell.ini new file mode 100644 index 000000000..cbae178b1 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit/xpcshell.ini @@ -0,0 +1,12 @@ +[DEFAULT] +head = head_contentPrefs.js +tail = tail_contentPrefs.js + +[test_bug248970.js] +[test_bug503971.js] +[test_bug679784.js] +[test_contentPrefs.js] +[test_contentPrefsCache.js] +[test_getPrefAsync.js] +[test_stringGroups.js] +[test_unusedGroupsAndSettings.js] diff --git a/toolkit/components/contentprefs/tests/unit_cps2/.eslintrc.js b/toolkit/components/contentprefs/tests/unit_cps2/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm b/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm new file mode 100644 index 000000000..ac878c28c --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm @@ -0,0 +1,69 @@ +/* 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/. */ + +var EXPORTED_SYMBOLS = [ + "AsyncRunner", +]; + +const { interfaces: Ci, classes: Cc } = Components; + +function AsyncRunner(callbacks) { + this._callbacks = callbacks; + this._iteratorQueue = []; + + // This catches errors reported to the console, e.g., via Cu.reportError. + Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService). + registerListener(this); +} + +AsyncRunner.prototype = { + + appendIterator: function AR_appendIterator(iter) { + this._iteratorQueue.push(iter); + }, + + next: function AR_next(arg) { + if (!this._iteratorQueue.length) { + this.destroy(); + this._callbacks.done(); + return; + } + + try { + var { done, value } = this._iteratorQueue[0].next(arg); + if (done) { + this._iteratorQueue.shift(); + this.next(); + return; + } + } + catch (err) { + this._callbacks.error(err); + } + + // val is truthy => call next + // val is an iterator => prepend it to the queue and start on it + if (value) { + if (typeof(value) != "boolean") + this._iteratorQueue.unshift(value); + this.next(); + } + }, + + destroy: function AR_destroy() { + Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService). + unregisterListener(this); + this.destroy = function AR_alreadyDestroyed() {}; + }, + + observe: function AR_consoleServiceListener(msg) { + if (msg instanceof Ci.nsIScriptError && + !(msg.flags & Ci.nsIScriptError.warningFlag)) + { + this._callbacks.consoleError(msg); + } + }, +}; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/head.js b/toolkit/components/contentprefs/tests/unit_cps2/head.js new file mode 100644 index 000000000..b86abe208 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/head.js @@ -0,0 +1,401 @@ +/* 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/. */ + +var { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); + +var cps; +var asyncRunner; +var next; + +(function init() { + // There has to be a profile directory before the CPS service is gotten. + do_get_profile(); +})(); + +function runAsyncTests(tests, dontResetBefore = false) { + do_test_pending(); + + cps = Cc["@mozilla.org/content-pref/service;1"]. + getService(Ci.nsIContentPrefService2); + + let s = {}; + Cu.import("resource://test/AsyncRunner.jsm", s); + asyncRunner = new s.AsyncRunner({ + done: do_test_finished, + error: function (err) { + // xpcshell test functions like equal throw NS_ERROR_ABORT on + // failure. Ignore those and catch only uncaught exceptions. + if (err !== Cr.NS_ERROR_ABORT) { + if (err.stack) { + err = err + "\n\nTraceback (most recent call first):\n" + err.stack + + "\nUseless do_throw stack:"; + } + do_throw(err); + } + }, + consoleError: function (scriptErr) { + // Previously, this code checked for console errors related to the test, + // and treated them as failures. This was problematic, because our current + // very-broken exception reporting machinery in XPCWrappedJSClass reports + // errors to the console even if there's actually JS on the stack above + // that will catch them. And a lot of the tests here intentionally trigger + // error conditions on the JS-implemented XPCOM component (see erroneous() + // in test_getSubdomains.js, for example). In the old world, we got lucky, + // and the errors were never reported to the console due to happenstantial + // JSContext reasons that aren't really worth going into. + // + // So. We make sure to dump this stuff so that it shows up in the logs, but + // don't turn them into duplicate failures of the exception that was already + // propagated to the caller. + dump("AsyncRunner.jsm observed console error: " + scriptErr + "\n"); + } + }); + + next = asyncRunner.next.bind(asyncRunner); + + do_register_cleanup(function () { + asyncRunner.destroy(); + asyncRunner = null; + }); + + tests.forEach(function (test) { + function* gen() { + do_print("Running " + test.name); + yield test(); + yield reset(); + } + asyncRunner.appendIterator(gen()); + }); + + // reset() ends up calling asyncRunner.next(), starting the tests. + if (dontResetBefore) { + next(); + } else { + reset(); + } +} + +function makeCallback(callbacks, success = null) { + callbacks = callbacks || {}; + if (!callbacks.handleError) { + callbacks.handleError = function (error) { + do_throw("handleError call was not expected, error: " + error); + }; + } + if (!callbacks.handleResult) { + callbacks.handleResult = function() { + do_throw("handleResult call was not expected"); + }; + } + if (!callbacks.handleCompletion) + callbacks.handleCompletion = function (reason) { + equal(reason, Ci.nsIContentPrefCallback2.COMPLETE_OK); + if (success) { + success(); + } else { + next(); + } + }; + return callbacks; +} + +function do_check_throws(fn) { + let threw = false; + try { + fn(); + } + catch (err) { + threw = true; + } + ok(threw); +} + +function sendMessage(msg, callback) { + let obj = callback || {}; + let ref = Cu.getWeakReference(obj); + cps.QueryInterface(Ci.nsIObserver).observe(ref, "test:" + msg, null); + return "value" in obj ? obj.value : undefined; +} + +function reset() { + sendMessage("reset", next); +} + +function setWithDate(group, name, val, timestamp, context) { + function updateDate() { + let db = sendMessage("db"); + let stmt = db.createAsyncStatement(` + UPDATE prefs SET timestamp = :timestamp + WHERE + settingID = (SELECT id FROM settings WHERE name = :name) + AND groupID = (SELECT id FROM groups WHERE name = :group) + `); + stmt.params.timestamp = timestamp / 1000; + stmt.params.name = name; + stmt.params.group = group; + + stmt.executeAsync({ + handleCompletion: function (reason) { + next(); + }, + handleError: function (err) { + do_throw(err); + } + }); + stmt.finalize(); + } + + cps.set(group, name, val, context, makeCallback(null, updateDate)); +} + +function getDate(group, name, context) { + let db = sendMessage("db"); + let stmt = db.createAsyncStatement(` + SELECT timestamp FROM prefs + WHERE + settingID = (SELECT id FROM settings WHERE name = :name) + AND groupID = (SELECT id FROM groups WHERE name = :group) + `); + stmt.params.name = name; + stmt.params.group = group; + + let res; + stmt.executeAsync({ + handleResult: function (results) { + let row = results.getNextRow(); + res = row.getResultByName("timestamp"); + }, + handleCompletion: function (reason) { + next(res * 1000); + }, + handleError: function (err) { + do_throw(err); + } + }); + stmt.finalize(); +} + +function set(group, name, val, context) { + cps.set(group, name, val, context, makeCallback()); +} + +function setGlobal(name, val, context) { + cps.setGlobal(name, val, context, makeCallback()); +} + +function prefOK(actual, expected, strict) { + ok(actual instanceof Ci.nsIContentPref); + equal(actual.domain, expected.domain); + equal(actual.name, expected.name); + if (strict) + strictEqual(actual.value, expected.value); + else + equal(actual.value, expected.value); +} + +function* getOK(args, expectedVal, expectedGroup, strict) { + if (args.length == 2) + args.push(undefined); + let expectedPrefs = expectedVal === undefined ? [] : + [{ domain: expectedGroup || args[0], + name: args[1], + value: expectedVal }]; + yield getOKEx("getByDomainAndName", args, expectedPrefs, strict); +} + +function* getSubdomainsOK(args, expectedGroupValPairs) { + if (args.length == 2) + args.push(undefined); + let expectedPrefs = expectedGroupValPairs.map(function ([group, val]) { + return { domain: group, name: args[1], value: val }; + }); + yield getOKEx("getBySubdomainAndName", args, expectedPrefs); +} + +function* getGlobalOK(args, expectedVal) { + if (args.length == 1) + args.push(undefined); + let expectedPrefs = expectedVal === undefined ? [] : + [{ domain: null, name: args[0], value: expectedVal }]; + yield getOKEx("getGlobal", args, expectedPrefs); +} + +function* getOKEx(methodName, args, expectedPrefs, strict, context) { + let actualPrefs = []; + args.push(makeCallback({ + handleResult: pref => actualPrefs.push(pref) + })); + yield cps[methodName].apply(cps, args); + arraysOfArraysOK([actualPrefs], [expectedPrefs], function (actual, expected) { + prefOK(actual, expected, strict); + }); +} + +function getCachedOK(args, expectedIsCached, expectedVal, expectedGroup, + strict) { + if (args.length == 2) + args.push(undefined); + let expectedPref = !expectedIsCached ? null : { + domain: expectedGroup || args[0], + name: args[1], + value: expectedVal + }; + getCachedOKEx("getCachedByDomainAndName", args, expectedPref, strict); +} + +function getCachedSubdomainsOK(args, expectedGroupValPairs) { + if (args.length == 2) + args.push(undefined); + let len = {}; + args.push(len); + let actualPrefs = cps.getCachedBySubdomainAndName.apply(cps, args); + actualPrefs = actualPrefs.sort(function (a, b) { + return a.domain.localeCompare(b.domain); + }); + equal(actualPrefs.length, len.value); + let expectedPrefs = expectedGroupValPairs.map(function ([group, val]) { + return { domain: group, name: args[1], value: val }; + }); + arraysOfArraysOK([actualPrefs], [expectedPrefs], prefOK); +} + +function getCachedGlobalOK(args, expectedIsCached, expectedVal) { + if (args.length == 1) + args.push(undefined); + let expectedPref = !expectedIsCached ? null : { + domain: null, + name: args[0], + value: expectedVal + }; + getCachedOKEx("getCachedGlobal", args, expectedPref); +} + +function getCachedOKEx(methodName, args, expectedPref, strict) { + let actualPref = cps[methodName].apply(cps, args); + if (expectedPref) + prefOK(actualPref, expectedPref, strict); + else + strictEqual(actualPref, null); +} + +function arraysOK(actual, expected, cmp) { + if (actual.length != expected.length) { + do_throw("Length is not equal: " + JSON.stringify(actual) + "==" + JSON.stringify(expected)); + } else { + actual.forEach(function (actualElt, j) { + let expectedElt = expected[j]; + cmp(actualElt, expectedElt); + }); + } +} + +function arraysOfArraysOK(actual, expected, cmp) { + cmp = cmp || equal; + arraysOK(actual, expected, function (act, exp) { + arraysOK(act, exp, cmp) + }); +} + +function dbOK(expectedRows) { + let db = sendMessage("db"); + let stmt = db.createAsyncStatement(` + SELECT groups.name AS grp, settings.name AS name, prefs.value AS value + FROM prefs + LEFT JOIN groups ON groups.id = prefs.groupID + LEFT JOIN settings ON settings.id = prefs.settingID + UNION + + /* + These second two SELECTs get the rows of the groups and settings tables + that aren't referenced by the prefs table. Neither should return any + rows if the component is working properly. + */ + SELECT groups.name AS grp, NULL AS name, NULL AS value + FROM groups + WHERE id NOT IN ( + SELECT DISTINCT groupID + FROM prefs + WHERE groupID NOTNULL + ) + UNION + SELECT NULL AS grp, settings.name AS name, NULL AS value + FROM settings + WHERE id NOT IN ( + SELECT DISTINCT settingID + FROM prefs + WHERE settingID NOTNULL + ) + + ORDER BY value ASC, grp ASC, name ASC + `); + + let actualRows = []; + let cols = ["grp", "name", "value"]; + + db.executeAsync([stmt], 1, { + handleCompletion: function (reason) { + arraysOfArraysOK(actualRows, expectedRows); + next(); + }, + handleResult: function (results) { + let row = null; + while (row = results.getNextRow()) { + actualRows.push(cols.map(c => row.getResultByName(c))); + } + }, + handleError: function (err) { + do_throw(err); + } + }); + stmt.finalize(); +} + +function on(event, names, dontRemove) { + let args = { + reset: function () { + for (let prop in this) { + if (Array.isArray(this[prop])) + this[prop].splice(0, this[prop].length); + } + }, + }; + + let observers = {}; + + names.forEach(function (name) { + let obs = {}; + ["onContentPrefSet", "onContentPrefRemoved"].forEach(function (meth) { + obs[meth] = () => do_throw(meth + " should not be called"); + }); + obs["onContentPref" + event] = function () { + args[name].push(Array.slice(arguments)); + }; + observers[name] = obs; + args[name] = []; + args[name].observer = obs; + cps.addObserverForName(name, obs); + }); + + do_execute_soon(function () { + if (!dontRemove) + names.forEach(n => cps.removeObserverForName(n, observers[n])); + next(args); + }); +} + +function schemaVersionIs(expectedVersion) { + let db = sendMessage("db"); + equal(db.schemaVersion, expectedVersion); +} + +function wait() { + do_execute_soon(next); +} + +function observerArgsOK(actualArgs, expectedArgs) { + notEqual(actualArgs, undefined); + arraysOfArraysOK(actualArgs, expectedArgs); +} diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js b/toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js new file mode 100644 index 000000000..2ec3d6878 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js @@ -0,0 +1,20 @@ +/* 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/. */ + +function run_test() { + let tests = { + "http://example.com": "example.com", + "http://example.com/": "example.com", + "http://example.com/foo/bar/baz": "example.com", + "http://subdomain.example.com/foo/bar/baz": "subdomain.example.com", + "http://qix.quux.example.com/foo/bar/baz": "qix.quux.example.com", + "file:///home/foo/bar": "file:///home/foo/bar", + "not a url": "not a url", + }; + let cps = Cc["@mozilla.org/content-pref/service;1"]. + getService(Ci.nsIContentPrefService2); + for (let url in tests) { + do_check_eq(cps.extractDomain(url), tests[url]); + } +} diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js b/toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js new file mode 100644 index 000000000..33a965b7f --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js @@ -0,0 +1,95 @@ +/* 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/. */ + +function run_test() { + runAsyncTests(tests); +} + +var tests = [ + + function* nonexistent() { + getCachedOK(["a.com", "foo"], false, undefined); + getCachedGlobalOK(["foo"], false, undefined); + yield true; + }, + + function* isomorphicDomains() { + yield set("a.com", "foo", 1); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["http://a.com/huh", "foo"], true, 1, "a.com"); + }, + + function* names() { + yield set("a.com", "foo", 1); + getCachedOK(["a.com", "foo"], true, 1); + + yield set("a.com", "bar", 2); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["a.com", "bar"], true, 2); + + yield setGlobal("foo", 3); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["a.com", "bar"], true, 2); + getCachedGlobalOK(["foo"], true, 3); + + yield setGlobal("bar", 4); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["a.com", "bar"], true, 2); + getCachedGlobalOK(["foo"], true, 3); + getCachedGlobalOK(["bar"], true, 4); + }, + + function* subdomains() { + yield set("a.com", "foo", 1); + yield set("b.a.com", "foo", 2); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["b.a.com", "foo"], true, 2); + }, + + function* privateBrowsing() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield set("b.com", "foo", 5); + + let context = { usePrivateBrowsing: true }; + yield set("a.com", "foo", 6, context); + yield setGlobal("foo", 7, context); + getCachedOK(["a.com", "foo", context], true, 6); + getCachedOK(["a.com", "bar", context], true, 2); + getCachedGlobalOK(["foo", context], true, 7); + getCachedGlobalOK(["bar", context], true, 4); + getCachedOK(["b.com", "foo", context], true, 5); + + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["a.com", "bar"], true, 2); + getCachedGlobalOK(["foo"], true, 3); + getCachedGlobalOK(["bar"], true, 4); + getCachedOK(["b.com", "foo"], true, 5); + }, + + function* erroneous() { + do_check_throws(() => cps.getCachedByDomainAndName(null, "foo", null)); + do_check_throws(() => cps.getCachedByDomainAndName("", "foo", null)); + do_check_throws(() => cps.getCachedByDomainAndName("a.com", "", null)); + do_check_throws(() => cps.getCachedByDomainAndName("a.com", null, null)); + do_check_throws(() => cps.getCachedGlobal("", null)); + do_check_throws(() => cps.getCachedGlobal(null, null)); + yield true; + }, + + function* casts() { + // SQLite casts booleans to integers. This makes sure the values stored in + // the cache are the same as the casted values in the database. + + yield set("a.com", "foo", false); + yield getOK(["a.com", "foo"], 0, "a.com", true); + getCachedOK(["a.com", "foo"], true, 0, "a.com", true); + + yield set("a.com", "bar", true); + yield getOK(["a.com", "bar"], 1, "a.com", true); + getCachedOK(["a.com", "bar"], true, 1, "a.com", true); + }, +]; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js b/toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js new file mode 100644 index 000000000..9f2599708 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js @@ -0,0 +1,186 @@ +/* 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/. */ + +function run_test() { + runAsyncTests(tests); +} + +var tests = [ + + function* nonexistent() { + getCachedSubdomainsOK(["a.com", "foo"], []); + yield true; + }, + + function* isomorphicDomains() { + yield set("a.com", "foo", 1); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + getCachedSubdomainsOK(["http://a.com/huh", "foo"], [["a.com", 1]]); + }, + + function* names() { + yield set("a.com", "foo", 1); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + + yield set("a.com", "bar", 2); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + + yield setGlobal("foo", 3); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + getCachedGlobalOK(["foo"], true, 3); + + yield setGlobal("bar", 4); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + getCachedGlobalOK(["foo"], true, 3); + getCachedGlobalOK(["bar"], true, 4); + }, + + function* subdomains() { + yield set("a.com", "foo", 1); + yield set("b.a.com", "foo", 2); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1], ["b.a.com", 2]]); + getCachedSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]); + }, + + function* populateViaGet() { + yield cps.getByDomainAndName("a.com", "foo", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + + yield cps.getGlobal("foo", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedGlobalOK(["foo"], true, undefined); + }, + + function* populateViaGetSubdomains() { + yield cps.getBySubdomainAndName("a.com", "foo", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + }, + + function* populateViaRemove() { + yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + + yield cps.removeBySubdomainAndName("b.com", "foo", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]); + + yield cps.removeGlobal("foo", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]); + getCachedGlobalOK(["foo"], true, undefined); + + yield set("a.com", "foo", 1); + yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]); + getCachedGlobalOK(["foo"], true, undefined); + + yield set("a.com", "foo", 2); + yield set("b.a.com", "foo", 3); + yield cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], + [["a.com", undefined], ["b.a.com", undefined]]); + getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]); + getCachedGlobalOK(["foo"], true, undefined); + getCachedSubdomainsOK(["b.a.com", "foo"], [["b.a.com", undefined]]); + + yield setGlobal("foo", 4); + yield cps.removeGlobal("foo", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], + [["a.com", undefined], ["b.a.com", undefined]]); + getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]); + getCachedGlobalOK(["foo"], true, undefined); + getCachedSubdomainsOK(["b.a.com", "foo"], [["b.a.com", undefined]]); + }, + + function* populateViaRemoveByDomain() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield set("b.a.com", "foo", 3); + yield set("b.a.com", "bar", 4); + yield cps.removeByDomain("a.com", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], + [["a.com", undefined], ["b.a.com", 3]]); + getCachedSubdomainsOK(["a.com", "bar"], + [["a.com", undefined], ["b.a.com", 4]]); + + yield set("a.com", "foo", 5); + yield set("a.com", "bar", 6); + yield cps.removeBySubdomain("a.com", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], + [["a.com", undefined], ["b.a.com", undefined]]); + getCachedSubdomainsOK(["a.com", "bar"], + [["a.com", undefined], ["b.a.com", undefined]]); + + yield setGlobal("foo", 7); + yield setGlobal("bar", 8); + yield cps.removeAllGlobals(null, makeCallback()); + getCachedGlobalOK(["foo"], true, undefined); + getCachedGlobalOK(["bar"], true, undefined); + }, + + function* populateViaRemoveAllDomains() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield set("b.com", "foo", 3); + yield set("b.com", "bar", 4); + yield cps.removeAllDomains(null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedSubdomainsOK(["a.com", "bar"], [["a.com", undefined]]); + getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]); + getCachedSubdomainsOK(["b.com", "bar"], [["b.com", undefined]]); + }, + + function* populateViaRemoveByName() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield cps.removeByName("foo", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + getCachedGlobalOK(["foo"], true, undefined); + getCachedGlobalOK(["bar"], true, 4); + + yield cps.removeByName("bar", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedSubdomainsOK(["a.com", "bar"], [["a.com", undefined]]); + getCachedGlobalOK(["foo"], true, undefined); + getCachedGlobalOK(["bar"], true, undefined); + }, + + function* privateBrowsing() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield set("b.com", "foo", 5); + + let context = { usePrivateBrowsing: true }; + yield set("a.com", "foo", 6, context); + yield setGlobal("foo", 7, context); + getCachedSubdomainsOK(["a.com", "foo", context], [["a.com", 6]]); + getCachedSubdomainsOK(["a.com", "bar", context], [["a.com", 2]]); + getCachedGlobalOK(["foo", context], true, 7); + getCachedGlobalOK(["bar", context], true, 4); + getCachedSubdomainsOK(["b.com", "foo", context], [["b.com", 5]]); + + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + getCachedGlobalOK(["foo"], true, 3); + getCachedGlobalOK(["bar"], true, 4); + getCachedSubdomainsOK(["b.com", "foo"], [["b.com", 5]]); + }, + + function* erroneous() { + do_check_throws(() => cps.getCachedBySubdomainAndName(null, "foo", null)); + do_check_throws(() => cps.getCachedBySubdomainAndName("", "foo", null)); + do_check_throws(() => cps.getCachedBySubdomainAndName("a.com", "", null)); + do_check_throws(() => cps.getCachedBySubdomainAndName("a.com", null, null)); + yield true; + }, +]; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js b/toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js new file mode 100644 index 000000000..d08d6fe69 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js @@ -0,0 +1,68 @@ +/* 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/. */ + +function run_test() { + runAsyncTests(tests); +} + +var tests = [ + + function* get_nonexistent() { + yield getSubdomainsOK(["a.com", "foo"], []); + }, + + function* isomorphicDomains() { + yield set("a.com", "foo", 1); + yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + yield getSubdomainsOK(["http://a.com/huh", "foo"], [["a.com", 1]]); + }, + + function* names() { + yield set("a.com", "foo", 1); + yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + + yield set("a.com", "bar", 2); + yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + yield getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + + yield setGlobal("foo", 3); + yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + yield getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + }, + + function* subdomains() { + yield set("a.com", "foo", 1); + yield set("b.a.com", "foo", 2); + yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1], ["b.a.com", 2]]); + yield getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]); + }, + + function* privateBrowsing() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield set("b.com", "foo", 5); + + let context = { usePrivateBrowsing: true }; + yield set("a.com", "foo", 6, context); + yield setGlobal("foo", 7, context); + yield getSubdomainsOK(["a.com", "foo", context], [["a.com", 6]]); + yield getSubdomainsOK(["a.com", "bar", context], [["a.com", 2]]); + yield getSubdomainsOK(["b.com", "foo", context], [["b.com", 5]]); + + yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + yield getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + yield getSubdomainsOK(["b.com", "foo"], [["b.com", 5]]); + }, + + function* erroneous() { + do_check_throws(() => cps.getBySubdomainAndName(null, "foo", null, {})); + do_check_throws(() => cps.getBySubdomainAndName("", "foo", null, {})); + do_check_throws(() => cps.getBySubdomainAndName("a.com", "", null, {})); + do_check_throws(() => cps.getBySubdomainAndName("a.com", null, null, {})); + do_check_throws(() => cps.getBySubdomainAndName("a.com", "foo", null, null)); + yield true; + }, +]; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js new file mode 100644 index 000000000..85d23e355 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js @@ -0,0 +1,82 @@ +/* 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/. */ + +// Dump of version we migrate from +var schema_version3 = ` +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; + CREATE TABLE groups (id INTEGER PRIMARY KEY, name TEXT NOT NULL); + INSERT INTO "groups" VALUES(1,'foo.com'); + INSERT INTO "groups" VALUES(2,'bar.com'); + + CREATE TABLE settings (id INTEGER PRIMARY KEY, name TEXT NOT NULL); + INSERT INTO "settings" VALUES(1,'zoom-setting'); + INSERT INTO "settings" VALUES(2,'dir-setting'); + + CREATE TABLE prefs (id INTEGER PRIMARY KEY, groupID INTEGER REFERENCES groups(id), settingID INTEGER NOT NULL REFERENCES settings(id), value BLOB); + INSERT INTO "prefs" VALUES(1,1,1,0.5); + INSERT INTO "prefs" VALUES(2,1,2,'/download/dir'); + INSERT INTO "prefs" VALUES(3,2,1,0.3); + INSERT INTO "prefs" VALUES(4,NULL,1,0.1); + + CREATE INDEX groups_idx ON groups(name); + CREATE INDEX settings_idx ON settings(name); + CREATE INDEX prefs_idx ON prefs(groupID, settingID); +COMMIT;`; + +function prepareVersion3Schema(callback) { + var dirService = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + + var dbFile = dirService.get("ProfD", Ci.nsIFile); + dbFile.append("content-prefs.sqlite"); + + var dbService = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + ok(!dbFile.exists(), "Db should not exist yet."); + + var dbConnection = dbService.openDatabase(dbFile); + equal(dbConnection.schemaVersion, 0); + + dbConnection.executeSimpleSQL(schema_version3); + dbConnection.schemaVersion = 3; + + dbConnection.close(); +} + +function run_test() { + prepareVersion3Schema(); + runAsyncTests(tests, true); +} + + +// WARNING: Database will reset after every test. This limitation comes from +// the fact that we ContentPrefService constructor is run only once per test file +// and so migration will be run only once. +var tests = [ + function* testMigration() { + // Test migrated db content. + schemaVersionIs(4); + let dbExpectedState = [ + [null, "zoom-setting", 0.1], + ["bar.com", "zoom-setting", 0.3], + ["foo.com", "zoom-setting", 0.5], + ["foo.com", "dir-setting", "/download/dir"], + ]; + yield dbOK(dbExpectedState); + + // Migrated fields should have timestamp set to 0. + yield cps.removeAllDomainsSince(1000, null, makeCallback()); + yield dbOK(dbExpectedState); + + yield cps.removeAllDomainsSince(0, null, makeCallback()); + yield dbOK([[null, "zoom-setting", 0.1]]); + + // Test that dates are present after migration (column is added). + const timestamp = 1234; + yield setWithDate("a.com", "pref-name", "val", timestamp); + let actualTimestamp = yield getDate("a.com", "pref-name"); + equal(actualTimestamp, timestamp); + } +]; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_observers.js b/toolkit/components/contentprefs/tests/unit_cps2/test_observers.js new file mode 100644 index 000000000..c48918cd9 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_observers.js @@ -0,0 +1,178 @@ +/* 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/. */ + +let global = this; + +function run_test() { + var allTests = []; + for (var i = 0; i < tests.length; i++) { + // Generate two wrappers of each test function that invoke the original test with an + // appropriate privacy context. + var pub = eval('var f = function* ' + tests[i].name + '() { yield tests[' + i + ']({ usePrivateBrowsing: false }); }; f'); + var priv = eval('var f = function* ' + tests[i].name + '_private() { yield tests[' + i + ']({ usePrivateBrowsing: true }); }; f'); + allTests.push(pub); + allTests.push(priv); + } + allTests = allTests.concat(specialTests); + runAsyncTests(allTests); +} + +var tests = [ + + function* observerForName_set(context) { + yield set("a.com", "foo", 1, context); + let args = yield on("Set", ["foo", null, "bar"]); + observerArgsOK(args.foo, [["a.com", "foo", 1, context.usePrivateBrowsing]]); + observerArgsOK(args.null, [["a.com", "foo", 1, context.usePrivateBrowsing]]); + observerArgsOK(args.bar, []); + + yield setGlobal("foo", 2, context); + args = yield on("Set", ["foo", null, "bar"]); + observerArgsOK(args.foo, [[null, "foo", 2, context.usePrivateBrowsing]]); + observerArgsOK(args.null, [[null, "foo", 2, context.usePrivateBrowsing]]); + observerArgsOK(args.bar, []); + }, + + function* observerForName_remove(context) { + yield set("a.com", "foo", 1, context); + yield setGlobal("foo", 2, context); + + yield cps.removeByDomainAndName("a.com", "bogus", context, makeCallback()); + let args = yield on("Removed", ["foo", null, "bar"]); + observerArgsOK(args.foo, []); + observerArgsOK(args.null, []); + observerArgsOK(args.bar, []); + + yield cps.removeByDomainAndName("a.com", "foo", context, makeCallback()); + args = yield on("Removed", ["foo", null, "bar"]); + observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.null, [["a.com", "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.bar, []); + + yield cps.removeGlobal("foo", context, makeCallback()); + args = yield on("Removed", ["foo", null, "bar"]); + observerArgsOK(args.foo, [[null, "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.null, [[null, "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.bar, []); + }, + + function* observerForName_removeByDomain(context) { + yield set("a.com", "foo", 1, context); + yield set("b.a.com", "bar", 2, context); + yield setGlobal("foo", 3, context); + + yield cps.removeByDomain("bogus", context, makeCallback()); + let args = yield on("Removed", ["foo", null, "bar"]); + observerArgsOK(args.foo, []); + observerArgsOK(args.null, []); + observerArgsOK(args.bar, []); + + yield cps.removeBySubdomain("a.com", context, makeCallback()); + args = yield on("Removed", ["foo", null, "bar"]); + observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.null, [["a.com", "foo", context.usePrivateBrowsing], ["b.a.com", "bar", context.usePrivateBrowsing]]); + observerArgsOK(args.bar, [["b.a.com", "bar", context.usePrivateBrowsing]]); + + yield cps.removeAllGlobals(context, makeCallback()); + args = yield on("Removed", ["foo", null, "bar"]); + observerArgsOK(args.foo, [[null, "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.null, [[null, "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.bar, []); + }, + + function* observerForName_removeAllDomains(context) { + yield set("a.com", "foo", 1, context); + yield setGlobal("foo", 2, context); + yield set("b.com", "bar", 3, context); + + yield cps.removeAllDomains(context, makeCallback()); + let args = yield on("Removed", ["foo", null, "bar"]); + observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.null, [["a.com", "foo", context.usePrivateBrowsing], ["b.com", "bar", context.usePrivateBrowsing]]); + observerArgsOK(args.bar, [["b.com", "bar", context.usePrivateBrowsing]]); + }, + + function* observerForName_removeByName(context) { + yield set("a.com", "foo", 1, context); + yield set("a.com", "bar", 2, context); + yield setGlobal("foo", 3, context); + + yield cps.removeByName("bogus", context, makeCallback()); + let args = yield on("Removed", ["foo", null, "bar"]); + observerArgsOK(args.foo, []); + observerArgsOK(args.null, []); + observerArgsOK(args.bar, []); + + yield cps.removeByName("foo", context, makeCallback()); + args = yield on("Removed", ["foo", null, "bar"]); + observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing], [null, "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.null, [["a.com", "foo", context.usePrivateBrowsing], [null, "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.bar, []); + }, + + function* removeObserverForName(context) { + let args = yield on("Set", ["foo", null, "bar"], true); + + cps.removeObserverForName("foo", args.foo.observer); + yield set("a.com", "foo", 1, context); + yield wait(); + observerArgsOK(args.foo, []); + observerArgsOK(args.null, [["a.com", "foo", 1, context.usePrivateBrowsing]]); + observerArgsOK(args.bar, []); + args.reset(); + + cps.removeObserverForName(null, args.null.observer); + yield set("a.com", "foo", 2, context); + yield wait(); + observerArgsOK(args.foo, []); + observerArgsOK(args.null, []); + observerArgsOK(args.bar, []); + args.reset(); + }, +]; + +// These tests are for functionality that doesn't behave the same way in private and public +// contexts, so the expected results cannot be automatically generated like the previous tests. +var specialTests = [ + function* observerForName_removeAllDomainsSince() { + yield setWithDate("a.com", "foo", 1, 100, null); + yield setWithDate("b.com", "foo", 2, 200, null); + yield setWithDate("c.com", "foo", 3, 300, null); + + yield setWithDate("a.com", "bar", 1, 0, null); + yield setWithDate("b.com", "bar", 2, 100, null); + yield setWithDate("c.com", "bar", 3, 200, null); + yield setGlobal("foo", 2, null); + + yield cps.removeAllDomainsSince(200, null, makeCallback()); + + let args = yield on("Removed", ["foo", "bar", null]); + + observerArgsOK(args.foo, [["b.com", "foo", false], ["c.com", "foo", false]]); + observerArgsOK(args.bar, [["c.com", "bar", false]]); + observerArgsOK(args.null, [["b.com", "foo", false], ["c.com", "bar", false], ["c.com", "foo", false]]); + }, + + function* observerForName_removeAllDomainsSince_private() { + let context = {usePrivateBrowsing: true}; + yield setWithDate("a.com", "foo", 1, 100, context); + yield setWithDate("b.com", "foo", 2, 200, context); + yield setWithDate("c.com", "foo", 3, 300, context); + + yield setWithDate("a.com", "bar", 1, 0, context); + yield setWithDate("b.com", "bar", 2, 100, context); + yield setWithDate("c.com", "bar", 3, 200, context); + yield setGlobal("foo", 2, context); + + yield cps.removeAllDomainsSince(200, context, makeCallback()); + + let args = yield on("Removed", ["foo", "bar", null]); + + observerArgsOK(args.foo, [["a.com", "foo", true], ["b.com", "foo", true], ["c.com", "foo", true]]); + observerArgsOK(args.bar, [["a.com", "bar", true], ["b.com", "bar", true], ["c.com", "bar", true]]); + observerArgsOK(args.null, [["a.com", "foo", true], ["a.com", "bar", true], + ["b.com", "foo", true], ["b.com", "bar", true], + ["c.com", "foo", true], ["c.com", "bar", true]]); + }, +]; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js b/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js new file mode 100644 index 000000000..9853293fc --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js @@ -0,0 +1,222 @@ +/* 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/. */ + +function run_test() { + runAsyncTests(tests); +} + +var tests = [ + + function* nonexistent() { + yield set("a.com", "foo", 1); + yield setGlobal("foo", 2); + + yield cps.removeByDomainAndName("a.com", "bogus", null, makeCallback()); + yield dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + yield getOK(["a.com", "foo"], 1); + yield getGlobalOK(["foo"], 2); + + yield cps.removeBySubdomainAndName("a.com", "bogus", null, makeCallback()); + yield dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + yield getOK(["a.com", "foo"], 1); + yield getGlobalOK(["foo"], 2); + + yield cps.removeGlobal("bogus", null, makeCallback()); + yield dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + yield getOK(["a.com", "foo"], 1); + yield getGlobalOK(["foo"], 2); + + yield cps.removeByDomainAndName("bogus", "bogus", null, makeCallback()); + yield dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + yield getOK(["a.com", "foo"], 1); + yield getGlobalOK(["foo"], 2); + }, + + function* isomorphicDomains() { + yield set("a.com", "foo", 1); + yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback()); + yield dbOK([]); + yield getOK(["a.com", "foo"], undefined); + + yield set("a.com", "foo", 2); + yield cps.removeByDomainAndName("http://a.com/huh", "foo", null, + makeCallback()); + yield dbOK([]); + yield getOK(["a.com", "foo"], undefined); + }, + + function* names() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + + yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback()); + yield dbOK([ + ["a.com", "bar", 2], + [null, "foo", 3], + [null, "bar", 4], + ]); + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], 2); + yield getGlobalOK(["foo"], 3); + yield getGlobalOK(["bar"], 4); + + yield cps.removeGlobal("foo", null, makeCallback()); + yield dbOK([ + ["a.com", "bar", 2], + [null, "bar", 4], + ]); + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], 2); + yield getGlobalOK(["foo"], undefined); + yield getGlobalOK(["bar"], 4); + + yield cps.removeByDomainAndName("a.com", "bar", null, makeCallback()); + yield dbOK([ + [null, "bar", 4], + ]); + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], undefined); + yield getGlobalOK(["foo"], undefined); + yield getGlobalOK(["bar"], 4); + + yield cps.removeGlobal("bar", null, makeCallback()); + yield dbOK([ + ]); + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], undefined); + yield getGlobalOK(["foo"], undefined); + yield getGlobalOK(["bar"], undefined); + }, + + function* subdomains() { + yield set("a.com", "foo", 1); + yield set("b.a.com", "foo", 2); + yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback()); + yield dbOK([ + ["b.a.com", "foo", 2], + ]); + yield getSubdomainsOK(["a.com", "foo"], [["b.a.com", 2]]); + yield getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]); + + yield set("a.com", "foo", 3); + yield cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback()); + yield dbOK([ + ]); + yield getSubdomainsOK(["a.com", "foo"], []); + yield getSubdomainsOK(["b.a.com", "foo"], []); + + yield set("a.com", "foo", 4); + yield set("b.a.com", "foo", 5); + yield cps.removeByDomainAndName("b.a.com", "foo", null, makeCallback()); + yield dbOK([ + ["a.com", "foo", 4], + ]); + yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]); + yield getSubdomainsOK(["b.a.com", "foo"], []); + + yield set("b.a.com", "foo", 6); + yield cps.removeBySubdomainAndName("b.a.com", "foo", null, makeCallback()); + yield dbOK([ + ["a.com", "foo", 4], + ]); + yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]); + yield getSubdomainsOK(["b.a.com", "foo"], []); + }, + + function* privateBrowsing() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield setGlobal("qux", 5); + yield set("b.com", "foo", 6); + yield set("b.com", "bar", 7); + + let context = { usePrivateBrowsing: true }; + yield set("a.com", "foo", 8, context); + yield setGlobal("foo", 9, context); + yield cps.removeByDomainAndName("a.com", "foo", context, makeCallback()); + yield cps.removeGlobal("foo", context, makeCallback()); + yield cps.removeGlobal("qux", context, makeCallback()); + yield cps.removeByDomainAndName("b.com", "foo", context, makeCallback()); + yield dbOK([ + ["a.com", "bar", 2], + [null, "bar", 4], + ["b.com", "bar", 7], + ]); + yield getOK(["a.com", "foo", context], undefined); + yield getOK(["a.com", "bar", context], 2); + yield getGlobalOK(["foo", context], undefined); + yield getGlobalOK(["bar", context], 4); + yield getGlobalOK(["qux", context], undefined); + yield getOK(["b.com", "foo", context], undefined); + yield getOK(["b.com", "bar", context], 7); + + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], 2); + yield getGlobalOK(["foo"], undefined); + yield getGlobalOK(["bar"], 4); + yield getGlobalOK(["qux"], undefined); + yield getOK(["b.com", "foo"], undefined); + yield getOK(["b.com", "bar"], 7); + }, + + function* erroneous() { + do_check_throws(() => cps.removeByDomainAndName(null, "foo", null)); + do_check_throws(() => cps.removeByDomainAndName("", "foo", null)); + do_check_throws(() => cps.removeByDomainAndName("a.com", "foo", null, + "bogus")); + do_check_throws(() => cps.removeBySubdomainAndName(null, "foo", + null)); + do_check_throws(() => cps.removeBySubdomainAndName("", "foo", null)); + do_check_throws(() => cps.removeBySubdomainAndName("a.com", "foo", + null, "bogus")); + do_check_throws(() => cps.removeGlobal("", null)); + do_check_throws(() => cps.removeGlobal(null, null)); + do_check_throws(() => cps.removeGlobal("foo", null, "bogus")); + yield true; + }, + + function* removeByDomainAndName_invalidateCache() { + yield set("a.com", "foo", 1); + getCachedOK(["a.com", "foo"], true, 1); + cps.removeByDomainAndName("a.com", "foo", null, makeCallback()); + getCachedOK(["a.com", "foo"], false); + yield; + }, + + function* removeBySubdomainAndName_invalidateCache() { + yield set("a.com", "foo", 1); + yield set("b.a.com", "foo", 2); + getCachedSubdomainsOK(["a.com", "foo"], [ + ["a.com", 1], + ["b.a.com", 2], + ]); + cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], []); + yield; + }, + + function* removeGlobal_invalidateCache() { + yield setGlobal("foo", 1); + getCachedGlobalOK(["foo"], true, 1); + cps.removeGlobal("foo", null, makeCallback()); + getCachedGlobalOK(["foo"], false); + yield; + }, +]; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js new file mode 100644 index 000000000..63e1b0552 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js @@ -0,0 +1,87 @@ +/* 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/. */ + +function run_test() { + runAsyncTests(tests); +} + +var tests = [ + + function* nonexistent() { + yield setGlobal("foo", 1); + yield cps.removeAllDomains(null, makeCallback()); + yield dbOK([ + [null, "foo", 1], + ]); + yield getGlobalOK(["foo"], 1); + }, + + function* domains() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield set("b.com", "foo", 5); + yield set("b.com", "bar", 6); + + yield cps.removeAllDomains(null, makeCallback()); + yield dbOK([ + [null, "foo", 3], + [null, "bar", 4], + ]); + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], undefined); + yield getGlobalOK(["foo"], 3); + yield getGlobalOK(["bar"], 4); + yield getOK(["b.com", "foo"], undefined); + yield getOK(["b.com", "bar"], undefined); + }, + + function* privateBrowsing() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield set("b.com", "foo", 5); + + let context = { usePrivateBrowsing: true }; + yield set("a.com", "foo", 6, context); + yield setGlobal("foo", 7, context); + yield cps.removeAllDomains(context, makeCallback()); + yield dbOK([ + [null, "foo", 3], + [null, "bar", 4], + ]); + yield getOK(["a.com", "foo", context], undefined); + yield getOK(["a.com", "bar", context], undefined); + yield getGlobalOK(["foo", context], 7); + yield getGlobalOK(["bar", context], 4); + yield getOK(["b.com", "foo", context], undefined); + + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], undefined); + yield getGlobalOK(["foo"], 3); + yield getGlobalOK(["bar"], 4); + yield getOK(["b.com", "foo"], undefined); + }, + + function* erroneous() { + do_check_throws(() => cps.removeAllDomains(null, "bogus")); + yield true; + }, + + function* invalidateCache() { + yield set("a.com", "foo", 1); + yield set("b.com", "bar", 2); + yield setGlobal("baz", 3); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["b.com", "bar"], true, 2); + getCachedGlobalOK(["baz"], true, 3); + cps.removeAllDomains(null, makeCallback()); + getCachedOK(["a.com", "foo"], false); + getCachedOK(["b.com", "bar"], false); + getCachedGlobalOK(["baz"], true, 3); + yield; + }, +]; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js new file mode 100644 index 000000000..fa0bf31c3 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js @@ -0,0 +1,111 @@ +/* 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/. */ + +function run_test() { + runAsyncTests(tests); +} + +var tests = [ + + function* nonexistent() { + yield setGlobal("foo", 1); + yield cps.removeAllDomainsSince(0, null, makeCallback()); + yield getGlobalOK(["foo"], 1); + }, + + function* domainsAll() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield set("b.com", "foo", 5); + yield set("b.com", "bar", 6); + + yield cps.removeAllDomainsSince(0, null, makeCallback()); + yield dbOK([ + [null, "foo", 3], + [null, "bar", 4], + ]); + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], undefined); + yield getGlobalOK(["foo"], 3); + yield getGlobalOK(["bar"], 4); + yield getOK(["b.com", "foo"], undefined); + yield getOK(["b.com", "bar"], undefined); + }, + + function* domainsWithDate() { + yield setWithDate("a.com", "foobar", 0, 0); + yield setWithDate("a.com", "foo", 1, 1000); + yield setWithDate("a.com", "bar", 2, 4000); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield setWithDate("b.com", "foo", 5, 2000); + yield setWithDate("b.com", "bar", 6, 3000); + yield setWithDate("b.com", "foobar", 7, 1000); + + yield cps.removeAllDomainsSince(2000, null, makeCallback()); + yield dbOK([ + ["a.com", "foobar", 0], + ["a.com", "foo", 1], + [null, "foo", 3], + [null, "bar", 4], + ["b.com", "foobar", 7], + ]); + }, + + function* privateBrowsing() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield set("b.com", "foo", 5); + + let context = { usePrivateBrowsing: true }; + yield set("a.com", "foo", 6, context); + yield setGlobal("foo", 7, context); + yield cps.removeAllDomainsSince(0, context, makeCallback()); + yield dbOK([ + [null, "foo", 3], + [null, "bar", 4], + ]); + yield getOK(["a.com", "foo", context], undefined); + yield getOK(["a.com", "bar", context], undefined); + yield getGlobalOK(["foo", context], 7); + yield getGlobalOK(["bar", context], 4); + yield getOK(["b.com", "foo", context], undefined); + + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], undefined); + yield getGlobalOK(["foo"], 3); + yield getGlobalOK(["bar"], 4); + yield getOK(["b.com", "foo"], undefined); + }, + + function* erroneous() { + do_check_throws(() => cps.removeAllDomainsSince(null, "bogus")); + yield true; + }, + + function* invalidateCache() { + yield setWithDate("a.com", "foobar", 0, 0); + yield setWithDate("a.com", "foo", 1, 1000); + yield setWithDate("a.com", "bar", 2, 4000); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield setWithDate("b.com", "foo", 5, 2000); + yield setWithDate("b.com", "bar", 6, 3000); + yield setWithDate("b.com", "foobar", 7, 1000); + cps.removeAllDomainsSince(0, null, makeCallback()); + getCachedOK(["a.com", "foobar"], false); + getCachedOK(["a.com", "foo"], false); + getCachedOK(["a.com", "bar"], false); + getCachedGlobalOK(["foo"], true, 3); + getCachedGlobalOK(["bar"], true, 4); + getCachedOK(["b.com", "foo"], false); + getCachedOK(["b.com", "bar"], false); + getCachedOK(["b.com", "foobar"], false); + yield true; + }, +]; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js new file mode 100644 index 000000000..1cf6bd8f2 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js @@ -0,0 +1,199 @@ +/* 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/. */ + +function run_test() { + runAsyncTests(tests); +} + +var tests = [ + + function* nonexistent() { + yield set("a.com", "foo", 1); + yield setGlobal("foo", 2); + + yield cps.removeByDomain("bogus", null, makeCallback()); + yield dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + yield getOK(["a.com", "foo"], 1); + yield getGlobalOK(["foo"], 2); + + yield cps.removeBySubdomain("bogus", null, makeCallback()); + yield dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + yield getOK(["a.com", "foo"], 1); + yield getGlobalOK(["foo"], 2); + }, + + function* isomorphicDomains() { + yield set("a.com", "foo", 1); + yield cps.removeByDomain("a.com", null, makeCallback()); + yield dbOK([]); + yield getOK(["a.com", "foo"], undefined); + + yield set("a.com", "foo", 2); + yield cps.removeByDomain("http://a.com/huh", null, makeCallback()); + yield dbOK([]); + yield getOK(["a.com", "foo"], undefined); + }, + + function* domains() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield set("b.com", "foo", 5); + yield set("b.com", "bar", 6); + + yield cps.removeByDomain("a.com", null, makeCallback()); + yield dbOK([ + [null, "foo", 3], + [null, "bar", 4], + ["b.com", "foo", 5], + ["b.com", "bar", 6], + ]); + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], undefined); + yield getGlobalOK(["foo"], 3); + yield getGlobalOK(["bar"], 4); + yield getOK(["b.com", "foo"], 5); + yield getOK(["b.com", "bar"], 6); + + yield cps.removeAllGlobals(null, makeCallback()); + yield dbOK([ + ["b.com", "foo", 5], + ["b.com", "bar", 6], + ]); + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], undefined); + yield getGlobalOK(["foo"], undefined); + yield getGlobalOK(["bar"], undefined); + yield getOK(["b.com", "foo"], 5); + yield getOK(["b.com", "bar"], 6); + + yield cps.removeByDomain("b.com", null, makeCallback()); + yield dbOK([ + ]); + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], undefined); + yield getGlobalOK(["foo"], undefined); + yield getGlobalOK(["bar"], undefined); + yield getOK(["b.com", "foo"], undefined); + yield getOK(["b.com", "bar"], undefined); + }, + + function* subdomains() { + yield set("a.com", "foo", 1); + yield set("b.a.com", "foo", 2); + yield cps.removeByDomain("a.com", null, makeCallback()); + yield dbOK([ + ["b.a.com", "foo", 2], + ]); + yield getSubdomainsOK(["a.com", "foo"], [["b.a.com", 2]]); + yield getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]); + + yield set("a.com", "foo", 3); + yield cps.removeBySubdomain("a.com", null, makeCallback()); + yield dbOK([ + ]); + yield getSubdomainsOK(["a.com", "foo"], []); + yield getSubdomainsOK(["b.a.com", "foo"], []); + + yield set("a.com", "foo", 4); + yield set("b.a.com", "foo", 5); + yield cps.removeByDomain("b.a.com", null, makeCallback()); + yield dbOK([ + ["a.com", "foo", 4], + ]); + yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]); + yield getSubdomainsOK(["b.a.com", "foo"], []); + + yield set("b.a.com", "foo", 6); + yield cps.removeBySubdomain("b.a.com", null, makeCallback()); + yield dbOK([ + ["a.com", "foo", 4], + ]); + yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]); + yield getSubdomainsOK(["b.a.com", "foo"], []); + }, + + function* privateBrowsing() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield set("b.com", "foo", 5); + + let context = { usePrivateBrowsing: true }; + yield set("a.com", "foo", 6, context); + yield set("b.com", "foo", 7, context); + yield setGlobal("foo", 8, context); + yield cps.removeByDomain("a.com", context, makeCallback()); + yield getOK(["b.com", "foo", context], 7); + yield getGlobalOK(["foo", context], 8); + yield cps.removeAllGlobals(context, makeCallback()); + yield dbOK([ + ["b.com", "foo", 5], + ]); + yield getOK(["a.com", "foo", context], undefined); + yield getOK(["a.com", "bar", context], undefined); + yield getGlobalOK(["foo", context], undefined); + yield getGlobalOK(["bar", context], undefined); + yield getOK(["b.com", "foo", context], 5); + + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], undefined); + yield getGlobalOK(["foo"], undefined); + yield getGlobalOK(["bar"], undefined); + yield getOK(["b.com", "foo"], 5); + }, + + function* erroneous() { + do_check_throws(() => cps.removeByDomain(null, null)); + do_check_throws(() => cps.removeByDomain("", null)); + do_check_throws(() => cps.removeByDomain("a.com", null, "bogus")); + do_check_throws(() => cps.removeBySubdomain(null, null)); + do_check_throws(() => cps.removeBySubdomain("", null)); + do_check_throws(() => cps.removeBySubdomain("a.com", null, "bogus")); + do_check_throws(() => cps.removeAllGlobals(null, "bogus")); + yield true; + }, + + function* removeByDomain_invalidateCache() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["a.com", "bar"], true, 2); + cps.removeByDomain("a.com", null, makeCallback()); + getCachedOK(["a.com", "foo"], false); + getCachedOK(["a.com", "bar"], false); + yield; + }, + + function* removeBySubdomain_invalidateCache() { + yield set("a.com", "foo", 1); + yield set("b.a.com", "foo", 2); + getCachedSubdomainsOK(["a.com", "foo"], [ + ["a.com", 1], + ["b.a.com", 2], + ]); + cps.removeBySubdomain("a.com", null, makeCallback()); + getCachedSubdomainsOK(["a.com", "foo"], []); + yield; + }, + + function* removeAllGlobals_invalidateCache() { + yield setGlobal("foo", 1); + yield setGlobal("bar", 2); + getCachedGlobalOK(["foo"], true, 1); + getCachedGlobalOK(["bar"], true, 2); + cps.removeAllGlobals(null, makeCallback()); + getCachedGlobalOK(["foo"], false); + getCachedGlobalOK(["bar"], false); + yield; + }, +]; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js new file mode 100644 index 000000000..fa04656e2 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js @@ -0,0 +1,96 @@ +/* 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/. */ + +function run_test() { + runAsyncTests(tests); +} + +var tests = [ + + function* nonexistent() { + yield set("a.com", "foo", 1); + yield setGlobal("foo", 2); + + yield cps.removeByName("bogus", null, makeCallback()); + yield dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + yield getOK(["a.com", "foo"], 1); + yield getGlobalOK(["foo"], 2); + }, + + function* names() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield set("b.com", "foo", 5); + yield set("b.com", "bar", 6); + + yield cps.removeByName("foo", null, makeCallback()); + yield dbOK([ + ["a.com", "bar", 2], + [null, "bar", 4], + ["b.com", "bar", 6], + ]); + yield getOK(["a.com", "foo"], undefined); + yield getOK(["a.com", "bar"], 2); + yield getGlobalOK(["foo"], undefined); + yield getGlobalOK(["bar"], 4); + yield getOK(["b.com", "foo"], undefined); + yield getOK(["b.com", "bar"], 6); + }, + + function* privateBrowsing() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield set("b.com", "foo", 5); + yield set("b.com", "bar", 6); + + let context = { usePrivateBrowsing: true }; + yield set("a.com", "foo", 7, context); + yield setGlobal("foo", 8, context); + yield set("b.com", "bar", 9, context); + yield cps.removeByName("bar", context, makeCallback()); + yield dbOK([ + ["a.com", "foo", 1], + [null, "foo", 3], + ["b.com", "foo", 5], + ]); + yield getOK(["a.com", "foo", context], 7); + yield getOK(["a.com", "bar", context], undefined); + yield getGlobalOK(["foo", context], 8); + yield getGlobalOK(["bar", context], undefined); + yield getOK(["b.com", "foo", context], 5); + yield getOK(["b.com", "bar", context], undefined); + + yield getOK(["a.com", "foo"], 1); + yield getOK(["a.com", "bar"], undefined); + yield getGlobalOK(["foo"], 3); + yield getGlobalOK(["bar"], undefined); + yield getOK(["b.com", "foo"], 5); + yield getOK(["b.com", "bar"], undefined); + }, + + function* erroneous() { + do_check_throws(() => cps.removeByName("", null)); + do_check_throws(() => cps.removeByName(null, null)); + do_check_throws(() => cps.removeByName("foo", null, "bogus")); + yield true; + }, + + function* invalidateCache() { + yield set("a.com", "foo", 1); + yield set("b.com", "foo", 2); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["b.com", "foo"], true, 2); + cps.removeByName("foo", null, makeCallback()); + getCachedOK(["a.com", "foo"], false); + getCachedOK(["b.com", "foo"], false); + yield; + }, +]; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_service.js b/toolkit/components/contentprefs/tests/unit_cps2/test_service.js new file mode 100644 index 000000000..75292063e --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_service.js @@ -0,0 +1,12 @@ +/* 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/. */ + +function run_test() { + let serv = Cc["@mozilla.org/content-pref/service;1"]. + getService(Ci.nsIContentPrefService2); + do_check_eq(serv.QueryInterface(Ci.nsIContentPrefService2), serv); + do_check_eq(serv.QueryInterface(Ci.nsISupports), serv); + let val = serv.QueryInterface(Ci.nsIContentPrefService); + do_check_true(val instanceof Ci.nsIContentPrefService); +} diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js b/toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js new file mode 100644 index 000000000..b10a05bbc --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js @@ -0,0 +1,206 @@ +/* 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/. */ + +function run_test() { + runAsyncTests(tests); +} + +var tests = [ + + function* get_nonexistent() { + yield getOK(["a.com", "foo"], undefined); + yield getGlobalOK(["foo"], undefined); + }, + + function* isomorphicDomains() { + yield set("a.com", "foo", 1); + yield dbOK([ + ["a.com", "foo", 1], + ]); + yield getOK(["a.com", "foo"], 1); + yield getOK(["http://a.com/huh", "foo"], 1, "a.com"); + + yield set("http://a.com/huh", "foo", 2); + yield dbOK([ + ["a.com", "foo", 2], + ]); + yield getOK(["a.com", "foo"], 2); + yield getOK(["http://a.com/yeah", "foo"], 2, "a.com"); + }, + + function* names() { + yield set("a.com", "foo", 1); + yield dbOK([ + ["a.com", "foo", 1], + ]); + yield getOK(["a.com", "foo"], 1); + + yield set("a.com", "bar", 2); + yield dbOK([ + ["a.com", "foo", 1], + ["a.com", "bar", 2], + ]); + yield getOK(["a.com", "foo"], 1); + yield getOK(["a.com", "bar"], 2); + + yield setGlobal("foo", 3); + yield dbOK([ + ["a.com", "foo", 1], + ["a.com", "bar", 2], + [null, "foo", 3], + ]); + yield getOK(["a.com", "foo"], 1); + yield getOK(["a.com", "bar"], 2); + yield getGlobalOK(["foo"], 3); + + yield setGlobal("bar", 4); + yield dbOK([ + ["a.com", "foo", 1], + ["a.com", "bar", 2], + [null, "foo", 3], + [null, "bar", 4], + ]); + yield getOK(["a.com", "foo"], 1); + yield getOK(["a.com", "bar"], 2); + yield getGlobalOK(["foo"], 3); + yield getGlobalOK(["bar"], 4); + }, + + function* subdomains() { + yield set("a.com", "foo", 1); + yield set("b.a.com", "foo", 2); + yield dbOK([ + ["a.com", "foo", 1], + ["b.a.com", "foo", 2], + ]); + yield getOK(["a.com", "foo"], 1); + yield getOK(["b.a.com", "foo"], 2); + }, + + function* privateBrowsing() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield setGlobal("foo", 3); + yield setGlobal("bar", 4); + yield set("b.com", "foo", 5); + + let context = { usePrivateBrowsing: true }; + yield set("a.com", "foo", 6, context); + yield setGlobal("foo", 7, context); + yield dbOK([ + ["a.com", "foo", 1], + ["a.com", "bar", 2], + [null, "foo", 3], + [null, "bar", 4], + ["b.com", "foo", 5], + ]); + yield getOK(["a.com", "foo", context], 6, "a.com"); + yield getOK(["a.com", "bar", context], 2); + yield getGlobalOK(["foo", context], 7); + yield getGlobalOK(["bar", context], 4); + yield getOK(["b.com", "foo", context], 5); + + yield getOK(["a.com", "foo"], 1); + yield getOK(["a.com", "bar"], 2); + yield getGlobalOK(["foo"], 3); + yield getGlobalOK(["bar"], 4); + yield getOK(["b.com", "foo"], 5); + }, + + function* set_erroneous() { + do_check_throws(() => cps.set(null, "foo", 1, null)); + do_check_throws(() => cps.set("", "foo", 1, null)); + do_check_throws(() => cps.set("a.com", "", 1, null)); + do_check_throws(() => cps.set("a.com", null, 1, null)); + do_check_throws(() => cps.set("a.com", "foo", undefined, null)); + do_check_throws(() => cps.set("a.com", "foo", 1, null, "bogus")); + do_check_throws(() => cps.setGlobal("", 1, null)); + do_check_throws(() => cps.setGlobal(null, 1, null)); + do_check_throws(() => cps.setGlobal("foo", undefined, null)); + do_check_throws(() => cps.setGlobal("foo", 1, null, "bogus")); + yield true; + }, + + function* get_erroneous() { + do_check_throws(() => cps.getByDomainAndName(null, "foo", null, {})); + do_check_throws(() => cps.getByDomainAndName("", "foo", null, {})); + do_check_throws(() => cps.getByDomainAndName("a.com", "", null, {})); + do_check_throws(() => cps.getByDomainAndName("a.com", null, null, {})); + do_check_throws(() => cps.getByDomainAndName("a.com", "foo", null, null)); + do_check_throws(() => cps.getGlobal("", null, {})); + do_check_throws(() => cps.getGlobal(null, null, {})); + do_check_throws(() => cps.getGlobal("foo", null, null)); + yield true; + }, + + function* set_invalidateCache() { + // (1) Set a pref and wait for it to finish. + yield set("a.com", "foo", 1); + + // (2) It should be cached. + getCachedOK(["a.com", "foo"], true, 1); + + // (3) Set the pref to a new value but don't wait for it to finish. + cps.set("a.com", "foo", 2, null, { + handleCompletion: function () { + // (6) The pref should be cached after setting it. + getCachedOK(["a.com", "foo"], true, 2); + }, + }); + + // (4) Group "a.com" and name "foo" should no longer be cached. + getCachedOK(["a.com", "foo"], false); + + // (5) Call getByDomainAndName. + var fetchedPref; + cps.getByDomainAndName("a.com", "foo", null, { + handleResult: function (pref) { + fetchedPref = pref; + }, + handleCompletion: function () { + // (7) Finally, this callback should be called after set's above. + do_check_true(!!fetchedPref); + do_check_eq(fetchedPref.value, 2); + next(); + }, + }); + + yield; + }, + + function* get_nameOnly() { + yield set("a.com", "foo", 1); + yield set("a.com", "bar", 2); + yield set("b.com", "foo", 3); + yield setGlobal("foo", 4); + + yield getOKEx("getByName", ["foo", undefined], [ + {"domain": "a.com", "name": "foo", "value": 1}, + {"domain": "b.com", "name": "foo", "value": 3}, + {"domain": null, "name": "foo", "value": 4} + ]); + + let context = { usePrivateBrowsing: true }; + yield set("b.com", "foo", 5, context); + + yield getOKEx("getByName", ["foo", context], [ + {"domain": "a.com", "name": "foo", "value": 1}, + {"domain": null, "name": "foo", "value": 4}, + {"domain": "b.com", "name": "foo", "value": 5} + ]); + }, + + function* setSetsCurrentDate() { + // Because Date.now() is not guaranteed to be monotonically increasing + // we just do here rough sanity check with one minute tolerance. + const MINUTE = 60 * 1000; + let now = Date.now(); + let start = now - MINUTE; + let end = now + MINUTE; + yield set("a.com", "foo", 1); + let timestamp = yield getDate("a.com", "foo"); + ok(start <= timestamp, "Timestamp is not too early (" + start + "<=" + timestamp + ")."); + ok(timestamp <= end, "Timestamp is not too late (" + timestamp + "<=" + end + ")."); + }, +]; diff --git a/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini b/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini new file mode 100644 index 000000000..bdbcaf8fd --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini @@ -0,0 +1,19 @@ +[DEFAULT] +head = head.js +tail = +skip-if = toolkit == 'android' +support-files = AsyncRunner.jsm + +[test_service.js] +[test_setGet.js] +[test_getSubdomains.js] +[test_remove.js] +[test_removeByDomain.js] +[test_removeAllDomains.js] +[test_removeByName.js] +[test_getCached.js] +[test_getCachedSubdomains.js] +[test_observers.js] +[test_extractDomain.js] +[test_migrationToSchema4.js] +[test_removeAllDomainsSince.js] |