diff options
Diffstat (limited to 'toolkit/components/contentprefs')
25 files changed, 4605 insertions, 0 deletions
diff --git a/toolkit/components/contentprefs/ContentPrefService2.sys.mjs b/toolkit/components/contentprefs/ContentPrefService2.sys.mjs new file mode 100644 index 0000000000..360689b4e8 --- /dev/null +++ b/toolkit/components/contentprefs/ContentPrefService2.sys.mjs @@ -0,0 +1,1427 @@ +/* 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/. */ + +import { + ContentPref, + cbHandleCompletion, + cbHandleError, + cbHandleResult, +} from "resource://gre/modules/ContentPrefUtils.sys.mjs"; + +import { ContentPrefStore } from "resource://gre/modules/ContentPrefStore.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", +}); + +const CACHE_MAX_GROUP_ENTRIES = 100; + +const GROUP_CLAUSE = ` + SELECT id + FROM groups + WHERE name = :group OR + (:includeSubdomains AND name LIKE :pattern ESCAPE '/') +`; + +export function ContentPrefService2() { + if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) { + return ChromeUtils.importESModule( + "resource://gre/modules/ContentPrefServiceChild.sys.mjs" + ).ContentPrefServiceChild; + } + + Services.obs.addObserver(this, "last-pb-context-exited"); + + // Observe shutdown so we can shut down the database connection. + Services.obs.addObserver(this, "profile-before-change"); +} + +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(); + +function executeStatementsInTransaction(conn, stmts) { + return conn.executeTransaction(async () => { + let rows = []; + for (let { sql, params, cachable } of stmts) { + let execute = cachable ? conn.executeCached : conn.execute; + let stmtRows = await execute.call(conn, sql, params); + rows = rows.concat(stmtRows); + } + return rows; + }); +} + +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 new Error("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; +} + +ContentPrefService2.prototype = { + // XPCOM Plumbing + + classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"), + + // Destruction + + _destroy() { + Services.obs.removeObserver(this, "profile-before-change"); + Services.obs.removeObserver(this, "last-pb-context-exited"); + + // 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; + }, + + // in-memory cache and private-browsing stores + + _cache: cache, + _pbStore: privModeStorage, + + _connPromise: null, + + get conn() { + if (this._connPromise) { + return this._connPromise; + } + + return (this._connPromise = (async () => { + let conn; + try { + conn = await this._getConnection(); + } catch (e) { + this.log("Failed to establish database connection: " + e); + throw e; + } + return conn; + })()); + }, + + // nsIContentPrefService + + 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: 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: (reason, ok, gotRow) => { + if (ok) { + for (let [pbGroup, pbName, pbVal] of pbPrefs) { + cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal)); + } + } + cbHandleCompletion(callback, reason); + }, + 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: 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: (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: 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, false); + stmt.params.group = group; + stmt.params.includeSubdomains = includeSubdomains || false; + stmt.params.pattern = + "%." + (group == null ? null : group.replace(/\/|%|_/g, "/$&")); + 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 + ) { + checkGroupArg(group); + return this._getCached(group, name, true, context); + }, + + 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._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: (reason, ok) => { + if (ok) { + this._cache.setWithCast(group, name, value); + } + cbHandleCompletion(callback, reason); + if (ok) { + this._notifyPrefSet( + group, + name, + value, + context && context.usePrivateBrowsing + ); + } + }, + 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: row => { + let grp = row.getResultByName("grp"); + prefs.set(grp, name, undefined); + this._cache.set(grp, name, undefined); + }, + 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._notifyPrefRemoved(sgroup, name, isPrivate); + } + } + }, + onError: nsresult => { + cbHandleError(callback, nsresult); + }, + }); + }, + + // Deletes settings and groups that are no longer used. + _settingsAndGroupsCleanupStmts() { + // 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: row => { + let grp = row.getResultByName("grp"); + let name = row.getResultByName("name"); + prefs.set(grp, name, undefined); + this._cache.set(grp, name, undefined); + }, + 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._notifyPrefRemoved(sgroup, sname, isPrivate); + } + } + }, + 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: row => { + let grp = row.getResultByName("grp"); + let name = row.getResultByName("name"); + prefs.set(grp, name, undefined); + this._cache.set(grp, name, undefined); + }, + 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._notifyPrefRemoved(sgroup, sname, isPrivate); + } + } + }, + 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: row => { + let grp = row.getResultByName("grp"); + prefs.set(grp, name, undefined); + this._cache.set(grp, name, undefined); + }, + 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._notifyPrefRemoved(sgroup, name, isPrivate); + } + } + }, + onError: nsresult => { + cbHandleError(callback, nsresult); + }, + }); + }, + + /** + * 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, cachable = true) { + return { + sql, + cachable, + params: {}, + }; + }, + + /** + * 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: async function CPS2__execStmts(stmts, callbacks) { + let conn = await this.conn; + let rows; + let ok = true; + try { + rows = await executeStatementsInTransaction(conn, stmts); + } catch (e) { + ok = false; + if (callbacks.onError) { + try { + callbacks.onError(e); + } catch (e) { + console.error(e); + } + } else { + console.error(e); + } + } + + if (rows && callbacks.onRow) { + for (let row of rows) { + try { + callbacks.onRow(row); + } catch (e) { + console.error(e); + } + } + } + + try { + callbacks.onDone( + ok + ? Ci.nsIContentPrefCallback2.COMPLETE_OK + : Ci.nsIContentPrefCallback2.COMPLETE_ERROR, + ok, + rows && !!rows.length + ); + } catch (e) { + console.error(e); + } + }, + + /** + * 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. + * The return value is truncated at GROUP_NAME_MAX_LENGTH. + */ + _parseGroup: function CPS2__parseGroup(groupStr) { + if (!groupStr) { + return null; + } + try { + var groupURI = Services.io.newURI(groupStr); + groupStr = HostnameGrouper_group(groupURI); + } catch (err) {} + return groupStr.substring( + 0, + Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH - 1 + ); + }, + + _schedule: function CPS2__schedule(fn) { + Services.tm.dispatchToMainThread(fn.bind(this)); + }, + + // A hash of arrays of observers, indexed by setting name. + _observers: new Map(), + + // An array of generic observers, which observe all settings. + _genericObservers: new Set(), + + addObserverForName(aName, aObserver) { + let observers; + if (aName) { + observers = this._observers.get(aName); + if (!observers) { + observers = new Set(); + this._observers.set(aName, observers); + } + } else { + observers = this._genericObservers; + } + + observers.add(aObserver); + }, + + removeObserverForName(aName, aObserver) { + let observers; + if (aName) { + observers = this._observers.get(aName); + if (!observers) { + return; + } + } else { + observers = this._genericObservers; + } + + observers.delete(aObserver); + }, + + /** + * 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(aName) { + let genericObserverList = Array.from(this._genericObservers); + if (aName) { + let observersForName = this._observers.get(aName); + if (observersForName) { + return Array.from(observersForName).concat(genericObserverList); + } + } + return genericObserverList; + }, + + /** + * 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) { + console.error(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) { + console.error(ex); + } + } + }, + + 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 "profile-before-change": + this._destroy(); + break; + case "last-pb-context-exited": + this._pbStore.removeAll(); + break; + 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.conn; + break; + } + }, + + /** + * Removes all state from the service. Used by tests. + * + * @param callback A function that will be called when done. + */ + async _reset(callback) { + this._pbStore.removeAll(); + this._cache.removeAll(); + + this._observers = new Map(); + this._genericObservers = new Set(); + + let tables = ["prefs", "groups", "settings"]; + let stmts = tables.map(t => this._stmt(`DELETE FROM ${t}`)); + this._execStmts(stmts, { + onDone: () => { + callback(); + }, + }); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIContentPrefService2", + "nsIObserver", + ]), + + // Database Creation & Access + + _dbVersion: 5, + + _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"], + }, + }, + }, + + _debugLog: false, + + log: function CPS2_log(aMessage) { + if (this._debugLog) { + Services.console.logStringMessage("ContentPrefService2: " + aMessage); + } + }, + + async _getConnection(aAttemptNum = 0) { + if ( + Services.startup.isInOrBeyondShutdownPhase( + Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWN + ) + ) { + throw new Error("Can't open content prefs, we're in shutdown."); + } + let path = PathUtils.join(PathUtils.profileDir, "content-prefs.sqlite"); + let conn; + let resetAndRetry = async e => { + if (e.result != Cr.NS_ERROR_FILE_CORRUPTED) { + throw e; + } + + if (aAttemptNum >= this.MAX_ATTEMPTS) { + if (conn) { + await conn.close(); + } + this.log("Establishing connection failed too many times. Giving up."); + throw e; + } + + try { + await this._failover(conn, path); + } catch (e) { + console.error(e); + throw e; + } + return this._getConnection(++aAttemptNum); + }; + try { + conn = await lazy.Sqlite.openConnection({ + path, + incrementalVacuum: true, + vacuumOnIdle: true, + }); + try { + lazy.Sqlite.shutdown.addBlocker( + "Closing ContentPrefService2 connection.", + () => conn.close() + ); + } catch (ex) { + // Uh oh, we failed to add a shutdown blocker. Close the connection + // anyway, but make sure that doesn't throw. + try { + await conn?.close(); + } catch (ex) { + console.error(ex); + } + return null; + } + } catch (e) { + console.error(e); + return resetAndRetry(e); + } + + try { + await this._dbMaybeInit(conn); + } catch (e) { + console.error(e); + return resetAndRetry(e); + } + + // 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. + await conn.execute("PRAGMA synchronous = OFF"); + + return conn; + }, + + async _failover(aConn, aPath) { + this.log("Cleaning up DB file - close & remove & backup."); + if (aConn) { + await aConn.close(); + } + let uniquePath = await IOUtils.createUniqueFile( + PathUtils.parent(aPath), + PathUtils.filename(aPath) + ".corrupt", + 0o600 + ); + await IOUtils.copy(aPath, uniquePath); + await IOUtils.remove(aPath); + this.log("Completed DB cleanup."); + }, + + _dbMaybeInit: async function CPS2__dbMaybeInit(aConn) { + let version = parseInt(await aConn.getSchemaVersion(), 10); + this.log("Schema version: " + version); + + if (version == 0) { + await this._dbCreateSchema(aConn); + } else if (version != this._dbVersion) { + await this._dbMigrate(aConn, version, this._dbVersion); + } + }, + + _createTable: async function CPS2__createTable(aConn, aName) { + let tSQL = this._dbSchema.tables[aName]; + this.log("Creating table " + aName + " with " + tSQL); + await aConn.execute(`CREATE TABLE ${aName} (${tSQL})`); + }, + + _createIndex: async function CPS2__createTable(aConn, aName) { + let index = this._dbSchema.indices[aName]; + let statement = + "CREATE INDEX IF NOT EXISTS " + + aName + + " ON " + + index.table + + "(" + + index.columns.join(", ") + + ")"; + await aConn.execute(statement); + }, + + _dbCreateSchema: async function CPS2__dbCreateSchema(aConn) { + await aConn.executeTransaction(async () => { + this.log("Creating DB -- tables"); + for (let name in this._dbSchema.tables) { + await this._createTable(aConn, name); + } + + this.log("Creating DB -- indices"); + for (let name in this._dbSchema.indices) { + await this._createIndex(aConn, name); + } + + await aConn.setSchemaVersion(this._dbVersion); + }); + }, + + _dbMigrate: async function CPS2__dbMigrate(aConn, 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. + */ + await aConn.executeTransaction(async () => { + for (let i = aOldVersion; i < aNewVersion; i++) { + let migrationName = "_dbMigrate" + i + "To" + (i + 1); + if (typeof this[migrationName] != "function") { + throw new Error( + "no migrator function from version " + + aOldVersion + + " to version " + + aNewVersion + ); + } + await this[migrationName](aConn); + } + await aConn.setSchemaVersion(aNewVersion); + }); + }, + + _dbMigrate1To2: async function CPS2___dbMigrate1To2(aConn) { + await aConn.execute("ALTER TABLE groups RENAME TO groupsOld"); + await this._createTable(aConn, "groups"); + await aConn.execute(` + INSERT INTO groups (id, name) + SELECT id, name FROM groupsOld + `); + + await aConn.execute("DROP TABLE groupers"); + await aConn.execute("DROP TABLE groupsOld"); + }, + + _dbMigrate2To3: async function CPS2__dbMigrate2To3(aConn) { + for (let name in this._dbSchema.indices) { + await this._createIndex(aConn, name); + } + }, + + _dbMigrate3To4: async function CPS2__dbMigrate3To4(aConn) { + // Add timestamp column if it does not exist yet. This operation is idempotent. + try { + await aConn.execute("SELECT timestamp FROM prefs"); + } catch (e) { + await aConn.execute( + "ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0" + ); + } + + // To modify prefs_idx drop it and create again. + await aConn.execute("DROP INDEX IF EXISTS prefs_idx"); + for (let name in this._dbSchema.indices) { + await this._createIndex(aConn, name); + } + }, + + async _dbMigrate4To5(conn) { + // This is a data migration for browser.download.lastDir. While it may not + // affect all consumers, it's simpler and safer to do it here than elsewhere. + await conn.execute(` + DELETE FROM prefs + WHERE id IN ( + SELECT p.id FROM prefs p + JOIN groups g ON g.id = p.groupID + JOIN settings s ON s.id = p.settingID + WHERE s.name = 'browser.download.lastDir' + AND ( + (g.name BETWEEN 'data:' AND 'data:' || X'FFFF') OR + (g.name BETWEEN 'file:' AND 'file:' || X'FFFF') + ) + ) + `); + await conn.execute(` + DELETE FROM groups WHERE NOT EXISTS ( + SELECT 1 FROM prefs WHERE groupId = groups.id + ) + `); + // Trim group names longer than MAX_GROUP_LENGTH. + await conn.execute( + ` + UPDATE groups + SET name = substr(name, 0, :maxlen) + WHERE LENGTH(name) > :maxlen + `, + { + maxlen: Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH, + } + ); + }, +}; + +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); +} + +// XPCOM Plumbing diff --git a/toolkit/components/contentprefs/ContentPrefServiceChild.sys.mjs b/toolkit/components/contentprefs/ContentPrefServiceChild.sys.mjs new file mode 100644 index 0000000000..38d0d3c160 --- /dev/null +++ b/toolkit/components/contentprefs/ContentPrefServiceChild.sys.mjs @@ -0,0 +1,185 @@ +/* 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/. */ + +import { + ContentPref, + _methodsCallableFromChild, + cbHandleCompletion, + cbHandleError, + cbHandleResult, + safeCallback, +} from "resource://gre/modules/ContentPrefUtils.sys.mjs"; + +// 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(contentPref) { + cbHandleResult( + this._callback, + new ContentPref(contentPref.domain, contentPref.name, contentPref.value) + ); + }, + + handleError(result) { + cbHandleError(this._callback, result); + }, + + handleCompletion(reason) { + cbHandleCompletion(this._callback, reason); + }, +}; + +export class ContentPrefsChild extends JSProcessActorChild { + constructor() { + super(); + + // Map from pref name -> set of observers + this._observers = new Map(); + + // Map from random ID string -> CallbackCaller, per request + this._requests = new Map(); + } + + _getRandomId() { + return Services.uuid.generateUUID().toString(); + } + + receiveMessage(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(call, args, callback) { + let requestId = this._getRandomId(); + let data = { call, args, requestId }; + + this._requests.set(requestId, new CallbackCaller(callback)); + this.sendAsyncMessage("ContentPrefs:FunctionCall", data); + } + + addObserverForName(name, observer) { + let set = this._observers.get(name); + if (!set) { + set = new Set(); + + // This is the first observer for this name. Start listening for changes + // to it. + this.sendAsyncMessage("ContentPrefs:AddObserverForName", { + name, + }); + this._observers.set(name, set); + } + + set.add(observer); + } + + removeObserverForName(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.sendAsyncMessage("ContentPrefs:RemoveObserverForName", { + name, + }); + + this._observers.delete(name); + } + } +} + +export var ContentPrefServiceChild = { + QueryInterface: ChromeUtils.generateQI(["nsIContentPrefService2"]), + + addObserverForName: (name, observer) => { + ChromeUtils.domProcessChild + .getActor("ContentPrefs") + .addObserverForName(name, observer); + }, + removeObserverForName: (name, observer) => { + ChromeUtils.domProcessChild + .getActor("ContentPrefs") + .removeObserverForName(name, observer); + }, + + getCachedByDomainAndName: NYI, + getCachedBySubdomainAndName: NYI, + getCachedGlobal: NYI, + 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]; + } + + let actor = ChromeUtils.domProcessChild.getActor("ContentPrefs"); + actor.callFunction(method, args, callback); +} + +for (let [method, signature] of _methodsCallableFromChild) { + ContentPrefServiceChild[method] = forwardMethodToParent.bind( + ContentPrefServiceChild, + method, + signature + ); +} diff --git a/toolkit/components/contentprefs/ContentPrefServiceParent.sys.mjs b/toolkit/components/contentprefs/ContentPrefServiceParent.sys.mjs new file mode 100644 index 0000000000..66b4a02741 --- /dev/null +++ b/toolkit/components/contentprefs/ContentPrefServiceParent.sys.mjs @@ -0,0 +1,160 @@ +/* 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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + _methodsCallableFromChild: "resource://gre/modules/ContentPrefUtils.sys.mjs", +}); + +let loadContext = Cu.createLoadContext(); +let privateLoadContext = Cu.createPrivateLoadContext(); + +function contextArg(context) { + return context && context.usePrivateBrowsing + ? privateLoadContext + : loadContext; +} + +export class ContentPrefsParent extends JSProcessActorParent { + constructor() { + super(); + + // 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. + this._prefsToObserve = new Set(); + this._observer = null; + } + + didDestroy() { + if (this._observer) { + for (let i of this._prefsToObserve) { + lazy.cps2.removeObserverForName(i, this._observer); + } + this._observer = null; + } + } + + receiveMessage(msg) { + switch (msg.name) { + case "ContentPrefs:AddObserverForName": { + // The child process is responsible for not adding multiple parent + // observers for the same name. + let actor = this; + if (!this._observer) { + this._observer = { + onContentPrefSet(group, name, value, isPrivate) { + actor.onContentPrefSet(group, name, value, isPrivate); + }, + onContentPrefRemoved(group, name, isPrivate) { + actor.onContentPrefRemoved(group, name, isPrivate); + }, + }; + } + + let prefName = msg.data.name; + this._prefsToObserve.add(prefName); + lazy.cps2.addObserverForName(prefName, this._observer); + break; + } + + case "ContentPrefs:RemoveObserverForName": { + let prefName = msg.data.name; + this._prefsToObserve.delete(prefName); + if (this._prefsToObserve.size == 0) { + lazy.cps2.removeObserverForName(prefName, this._observer); + this._observer = null; + } + break; + } + + case "ContentPrefs:FunctionCall": + let data = msg.data; + let signature; + + if ( + !lazy._methodsCallableFromChild.some(([method, args]) => { + if (method == data.call) { + signature = args; + return true; + } + return false; + }) + ) { + throw new Error(`Can't call ${data.call} from child!`); + } + + let actor = this; + let args = data.args; + + return new Promise(resolve => { + let listener = { + handleResult(pref) { + actor.sendAsyncMessage("ContentPrefs:HandleResult", { + requestId: data.requestId, + contentPref: { + domain: pref.domain, + name: pref.name, + value: pref.value, + }, + }); + }, + + handleError(error) { + actor.sendAsyncMessage("ContentPrefs:HandleError", { + requestId: data.requestId, + error, + }); + }, + handleCompletion(reason) { + actor.sendAsyncMessage("ContentPrefs:HandleCompletion", { + requestId: data.requestId, + reason, + }); + }, + }; + // Push our special listener. + args.push(listener); + + // Process context argument for forwarding + let contextIndex = signature.indexOf("context"); + if (contextIndex > -1) { + args[contextIndex] = contextArg(args[contextIndex]); + } + + // And call the function. + lazy.cps2[data.call](...args); + }); + } + + return undefined; + } + + onContentPrefSet(group, name, value, isPrivate) { + this.sendAsyncMessage("ContentPrefs:NotifyObservers", { + name, + callback: "onContentPrefSet", + args: [group, name, value, isPrivate], + }); + } + + onContentPrefRemoved(group, name, isPrivate) { + this.sendAsyncMessage("ContentPrefs:NotifyObservers", { + name, + callback: "onContentPrefRemoved", + args: [group, name, isPrivate], + }); + } +} + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "cps2", + "@mozilla.org/content-pref/service;1", + "nsIContentPrefService2" +); diff --git a/toolkit/components/contentprefs/ContentPrefStore.sys.mjs b/toolkit/components/contentprefs/ContentPrefStore.sys.mjs new file mode 100644 index 0000000000..67ba7de317 --- /dev/null +++ b/toolkit/components/contentprefs/ContentPrefStore.sys.mjs @@ -0,0 +1,122 @@ +/* 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/. */ + +export 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.sys.mjs b/toolkit/components/contentprefs/ContentPrefUtils.sys.mjs new file mode 100644 index 0000000000..0a17913d99 --- /dev/null +++ b/toolkit/components/contentprefs/ContentPrefUtils.sys.mjs @@ -0,0 +1,54 @@ +/* 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/. */ + +export function ContentPref(domain, name, value) { + this.domain = domain; + this.name = name; + this.value = value; +} + +ContentPref.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIContentPref"]), +}; + +export function cbHandleResult(callback, pref) { + safeCallback(callback, "handleResult", [pref]); +} + +export function cbHandleCompletion(callback, reason) { + safeCallback(callback, "handleCompletion", [reason]); +} + +export function cbHandleError(callback, nsresult) { + safeCallback(callback, "handleError", [nsresult]); +} + +export function safeCallback(callbackObj, methodName, args) { + if (!callbackObj || typeof callbackObj[methodName] != "function") { + return; + } + try { + callbackObj[methodName].apply(callbackObj, args); + } catch (err) { + console.error(err); + } +} + +export 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/components.conf b/toolkit/components/contentprefs/components.conf new file mode 100644 index 0000000000..dac48777b5 --- /dev/null +++ b/toolkit/components/contentprefs/components.conf @@ -0,0 +1,14 @@ +# -*- 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/. + +Classes = [ + { + 'cid': '{e3f772f3-023f-4b32-b074-36cf0fd5d414}', + 'contract_ids': ['@mozilla.org/content-pref/service;1'], + 'esModule': 'resource://gre/modules/ContentPrefService2.sys.mjs', + 'constructor': 'ContentPrefService2', + }, +] diff --git a/toolkit/components/contentprefs/moz.build b/toolkit/components/contentprefs/moz.build new file mode 100644 index 0000000000..6d9ff8d805 --- /dev/null +++ b/toolkit/components/contentprefs/moz.build @@ -0,0 +1,26 @@ +# -*- 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_cps2/xpcshell.ini", +] + +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"] + +EXTRA_JS_MODULES += [ + "ContentPrefService2.sys.mjs", + "ContentPrefServiceChild.sys.mjs", + "ContentPrefServiceParent.sys.mjs", + "ContentPrefStore.sys.mjs", + "ContentPrefUtils.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Preferences") diff --git a/toolkit/components/contentprefs/tests/browser/browser.ini b/toolkit/components/contentprefs/tests/browser/browser.ini new file mode 100644 index 0000000000..9722f5df24 --- /dev/null +++ b/toolkit/components/contentprefs/tests/browser/browser.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[browser_remoteContentPrefs.js] diff --git a/toolkit/components/contentprefs/tests/browser/browser_remoteContentPrefs.js b/toolkit/components/contentprefs/tests/browser/browser_remoteContentPrefs.js new file mode 100644 index 0000000000..00c845e488 --- /dev/null +++ b/toolkit/components/contentprefs/tests/browser/browser_remoteContentPrefs.js @@ -0,0 +1,242 @@ +"use strict"; + +const childFrameURL = Services.io.newURI( + "data:text/html,<!DOCTYPE HTML><html><body></body></html>" +); + +async function runTestsForFrame(browser, isPrivate) { + let loadContext = Cu.createLoadContext(); + let privateLoadContext = Cu.createPrivateLoadContext(); + + await SpecialPowers.spawn(browser, [], async () => { + var cps = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + Assert.ok(cps !== null, "got the content pref service"); + + await new Promise(resolve => { + cps.setGlobal("testing", 42, null, { + handleCompletion(reason) { + Assert.equal(reason, 0, "set a pref?"); + resolve(); + }, + }); + }); + + let numResults = 0; + await new Promise(resolve => { + cps.getGlobal("testing", null, { + handleResult(pref) { + numResults++; + Assert.equal(pref.name, "testing", "pref has the right name"); + Assert.equal(pref.value, 42, "pref has the right value"); + }, + + handleCompletion(reason) { + Assert.equal(reason, 0, "get a pref?"); + Assert.equal(numResults, 1, "got the right number of prefs"); + resolve(); + }, + }); + }); + }); + + await SpecialPowers.spawn(browser, [isPrivate], async isFramePrivate => { + var cps = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + + if (!content._result) { + content._result = []; + } + + let observer; + cps.addObserverForName( + "testName", + (observer = { + onContentPrefSet(group, name, value, isPrivate) { + Assert.equal(group, null, "group should be null"); + Assert.equal(name, "testName", "should only see testName"); + Assert.equal(value, 42, "value should be correct"); + Assert.equal(isPrivate, isFramePrivate, "privacy should match"); + + content._result.push("set"); + }, + + onContentPrefRemoved(group, name, isPrivate) { + Assert.equal(group, null, "group should be null"); + Assert.equal(name, "testName", "name should match"); + Assert.equal(isPrivate, isFramePrivate, "privacy should match"); + + content._result.push("removed"); + }, + }) + ); + content._observer = observer; + }); + + for (let expectedResponse of ["set", "removed"]) { + var cps = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + + if (expectedResponse == "set") { + cps.setGlobal( + "testName", + 42, + isPrivate ? privateLoadContext : loadContext + ); + } else { + cps.removeGlobal( + "testName", + isPrivate ? privateLoadContext : loadContext + ); + } + + let response = await SpecialPowers.spawn( + browser, + [expectedResponse], + async expected => { + var cps = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + await ContentTaskUtils.waitForCondition( + () => content._result.length == 1, + "expecting one notification" + ); + + if (expected == "removed") { + cps.removeObserverForName("testName", content._observer); + } + + Assert.equal( + content._result.length, + 1, + "correct number of notifications" + ); + let result = content._result[0]; + content._result = []; + return result; + } + ); + + is(response, expectedResponse, "got correct observer notification"); + } + + await SpecialPowers.spawn(browser, [], async () => { + var cps = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + + await new Promise(resolve => { + cps.setGlobal("testName", 42, null, { + handleCompletion(reason) { + Assert.equal(reason, 0, "set a pref"); + cps.set("http://mochi.test", "testpref", "str", null, { + /* eslint no-shadow: 0 */ + handleCompletion(reason) { + Assert.equal(reason, 0, "set a pref"); + resolve(); + }, + }); + }, + }); + }); + + await new Promise(resolve => { + cps.removeByDomain("http://mochi.test", null, { + handleCompletion(reason) { + Assert.equal(reason, 0, "remove succeeded"); + cps.getByDomainAndName("http://mochi.test", "testpref", null, { + handleResult() { + throw new Error("got removed pref in test3"); + }, + handleCompletion() { + resolve(); + }, + handleError(rv) { + throw new Error(`got a pref error ${rv}`); + }, + }); + }, + }); + }); + }); + + await SpecialPowers.spawn(browser, [], async () => { + var cps = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + + let event = await new Promise((resolve, reject) => { + let prefObserver = { + onContentPrefSet(group, name, value, isPrivate) { + resolve({ group, name, value, isPrivate }); + }, + onContentPrefRemoved(group, name, isPrivate) { + reject("got unexpected notification"); + }, + }; + + cps.addObserverForName("test", prefObserver); + + let privateLoadContext = Cu.createPrivateLoadContext(); + cps.set("http://mochi.test", "test", 42, privateLoadContext); + + cps.addObserverForName("test", prefObserver); + }); + + Assert.equal(event.name, "test", "got the right event"); + Assert.equal(event.isPrivate, true, "the event was for an isPrivate pref"); + }); + + await SpecialPowers.spawn(browser, [], async () => { + var cps = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + + let results = []; + await new Promise(resolve => { + cps.getByDomainAndName("http://mochi.test", "test", null, { + handleResult(pref) { + info("received handleResult"); + results.push(pref); + }, + handleCompletion(reason) { + resolve(); + }, + handleError(rv) { + ok(false, `failed to get pref ${rv}`); + throw new Error("got unexpected error"); + }, + }); + }); + + Assert.equal(results.length, 0, "should not have seen the pb pref"); + }); +} + +async function runTest(isPrivate) { + /* globals Services */ + info("testing with isPrivate=" + isPrivate); + + let newWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: isPrivate, + }); + + const system = Services.scriptSecurityManager.getSystemPrincipal(); + + let browser = newWindow.gBrowser.selectedBrowser; + let loadedPromise = BrowserTestUtils.browserLoaded(browser); + browser.loadURI(childFrameURL, { triggeringPrincipal: system }); + await loadedPromise; + + await runTestsForFrame(browser, isPrivate); + + newWindow.close(); +} + +add_task(async function () { + await runTest(false); + await runTest(true); +}); diff --git a/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.sys.mjs b/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.sys.mjs new file mode 100644 index 0000000000..3fab7c981f --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.sys.mjs @@ -0,0 +1,59 @@ +/* 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/. */ + +export function AsyncRunner(callbacks) { + this._callbacks = callbacks; + this._iteratorQueue = []; + + // This catches errors reported to the console, e.g., via Cu.reportError. + Services.console.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() { + Services.console.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 0000000000..32b6167e19 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/head.js @@ -0,0 +1,360 @@ +/* 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 loadContext = Cu.createLoadContext(); +let privateLoadContext = Cu.createPrivateLoadContext(); + +const CURRENT_DB_VERSION = 5; + +// There has to be a profile directory before the CPS service is gotten. +do_get_profile(); +let cps = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 +); + +function makeCallback(resolve, 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 { + resolve(); + } + }; + } + return callbacks; +} + +async function do_check_throws(fn) { + let threw = false; + try { + await 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() { + return new Promise(resolve => { + sendMessage("reset", resolve); + }); +} + +function setWithDate(group, name, val, timestamp, context) { + return new Promise(resolve => { + async function updateDate() { + let conn = await sendMessage("db"); + await conn.execute( + ` + UPDATE prefs SET timestamp = :timestamp + WHERE + settingID = (SELECT id FROM settings WHERE name = :name) + AND groupID = (SELECT id FROM groups WHERE name = :group) + `, + { name, group, timestamp: timestamp / 1000 } + ); + + resolve(); + } + + cps.set(group, name, val, context, makeCallback(null, null, updateDate)); + }); +} + +async function getDate(group, name, context) { + let conn = await sendMessage("db"); + let [result] = await conn.execute( + ` + SELECT timestamp FROM prefs + WHERE + settingID = (SELECT id FROM settings WHERE name = :name) + AND groupID = (SELECT id FROM groups WHERE name = :group) + `, + { name, group } + ); + + return result.getResultByName("timestamp") * 1000; +} + +function set(group, name, val, context) { + return new Promise(resolve => { + cps.set(group, name, val, context, makeCallback(resolve)); + }); +} + +function setGlobal(name, val, context) { + return new Promise(resolve => { + cps.setGlobal(name, val, context, makeCallback(resolve)); + }); +} + +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); + } +} + +async 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, + }, + ]; + await getOKEx("getByDomainAndName", args, expectedPrefs, strict); +} + +async 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 }; + }); + await getOKEx("getBySubdomainAndName", args, expectedPrefs); +} + +async function getGlobalOK(args, expectedVal) { + if (args.length == 1) { + args.push(undefined); + } + let expectedPrefs = + expectedVal === undefined + ? [] + : [{ domain: null, name: args[0], value: expectedVal }]; + await getOKEx("getGlobal", args, expectedPrefs); +} + +async function getOKEx(methodName, args, expectedPrefs, strict, context) { + let actualPrefs = []; + await new Promise(resolve => { + args.push( + makeCallback(resolve, { + handleResult: pref => actualPrefs.push(pref), + }) + ); + 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 actualPrefs = cps.getCachedBySubdomainAndName.apply(cps, args); + actualPrefs = actualPrefs.sort(function (a, b) { + return a.domain.localeCompare(b.domain); + }); + 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); + }); +} + +async function dbOK(expectedRows) { + let conn = await sendMessage("db"); + let stmt = ` + 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 cols = ["grp", "name", "value"]; + + let actualRows = (await conn.execute(stmt)).map(row => + cols.map(c => row.getResultByName(c)) + ); + arraysOfArraysOK(actualRows, expectedRows); +} + +function on(event, names, dontRemove) { + return onEx(event, names, dontRemove).promise; +} + +function onEx(event, names, dontRemove) { + let args = { + reset() { + for (let prop in this) { + if (Array.isArray(this[prop])) { + this[prop].splice(0, this[prop].length); + } + } + }, + }; + + let observers = {}; + let deferred = null; + let triggered = false; + + names.forEach(function (name) { + let obs = {}; + ["onContentPrefSet", "onContentPrefRemoved"].forEach(function (meth) { + obs[meth] = () => do_throw(meth + " should not be called for " + name); + }); + obs["onContentPref" + event] = function () { + args[name].push(Array.from(arguments)); + + if (!triggered) { + triggered = true; + executeSoon(function () { + if (!dontRemove) { + names.forEach(n => cps.removeObserverForName(n, observers[n])); + } + deferred.resolve(args); + }); + } + }; + observers[name] = obs; + args[name] = []; + args[name].observer = obs; + cps.addObserverForName(name, obs); + }); + + return { + observers, + promise: new Promise(resolve => { + deferred = { resolve }; + }), + }; +} + +async function schemaVersionIs(expectedVersion) { + let db = await sendMessage("db"); + equal(await db.getSchemaVersion(), expectedVersion); +} + +function wait() { + return new Promise(resolve => executeSoon(resolve)); +} + +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 0000000000..013e8fc327 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js @@ -0,0 +1,21 @@ +/* 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) { + Assert.equal(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 0000000000..d8a70a545a --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js @@ -0,0 +1,97 @@ +/* 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/. */ + +add_task(async function resetBeforeTests() { + await reset(); +}); + +add_task(async function nonexistent() { + getCachedOK(["a.com", "foo"], false, undefined); + getCachedGlobalOK(["foo"], false, undefined); + await reset(); +}); + +add_task(async function isomorphicDomains() { + await set("a.com", "foo", 1); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["http://a.com/huh", "foo"], true, 1, "a.com"); + await reset(); +}); + +add_task(async function names() { + await set("a.com", "foo", 1); + getCachedOK(["a.com", "foo"], true, 1); + + await set("a.com", "bar", 2); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["a.com", "bar"], true, 2); + + await setGlobal("foo", 3); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["a.com", "bar"], true, 2); + getCachedGlobalOK(["foo"], true, 3); + + await setGlobal("bar", 4); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["a.com", "bar"], true, 2); + getCachedGlobalOK(["foo"], true, 3); + getCachedGlobalOK(["bar"], true, 4); + await reset(); +}); + +add_task(async function subdomains() { + await set("a.com", "foo", 1); + await set("b.a.com", "foo", 2); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["b.a.com", "foo"], true, 2); + await reset(); +}); + +add_task(async function privateBrowsing() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await set("b.com", "foo", 5); + + let context = privateLoadContext; + await set("a.com", "foo", 6, context); + await 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); + await reset(); +}); + +add_task(async 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)); + await reset(); +}); + +add_task(async 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. + + await set("a.com", "foo", false); + await getOK(["a.com", "foo"], 0, "a.com", true); + getCachedOK(["a.com", "foo"], true, 0, "a.com", true); + + await set("a.com", "bar", true); + await getOK(["a.com", "bar"], 1, "a.com", true); + getCachedOK(["a.com", "bar"], true, 1, "a.com", true); + await reset(); +}); 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 0000000000..ed15e6fb0e --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js @@ -0,0 +1,259 @@ +/* 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/. */ + +add_task(async function resetBeforeTests() { + await reset(); +}); + +add_task(async function nonexistent() { + getCachedSubdomainsOK(["a.com", "foo"], []); + await reset(); +}); + +add_task(async function isomorphicDomains() { + await set("a.com", "foo", 1); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + getCachedSubdomainsOK(["http://a.com/huh", "foo"], [["a.com", 1]]); + await reset(); +}); + +add_task(async function names() { + await set("a.com", "foo", 1); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + + await set("a.com", "bar", 2); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + + await setGlobal("foo", 3); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + getCachedGlobalOK(["foo"], true, 3); + + await 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); + await reset(); +}); + +add_task(async function subdomains() { + await set("a.com", "foo", 1); + await 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]]); + await reset(); +}); + +add_task(async function populateViaGet() { + await new Promise(resolve => + cps.getByDomainAndName("a.com", "foo", null, makeCallback(resolve)) + ); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + + await new Promise(resolve => + cps.getGlobal("foo", null, makeCallback(resolve)) + ); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedGlobalOK(["foo"], true, undefined); + await reset(); +}); + +add_task(async function populateViaGetSubdomains() { + await new Promise(resolve => + cps.getBySubdomainAndName("a.com", "foo", null, makeCallback(resolve)) + ); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + await reset(); +}); + +add_task(async function populateViaRemove() { + await new Promise(resolve => + cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve)) + ); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + + await new Promise(resolve => + cps.removeBySubdomainAndName("b.com", "foo", null, makeCallback(resolve)) + ); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]); + + await new Promise(resolve => + cps.removeGlobal("foo", null, makeCallback(resolve)) + ); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]); + getCachedGlobalOK(["foo"], true, undefined); + + await set("a.com", "foo", 1); + await new Promise(resolve => + cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve)) + ); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]); + getCachedGlobalOK(["foo"], true, undefined); + + await set("a.com", "foo", 2); + await set("b.a.com", "foo", 3); + await new Promise(resolve => + cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback(resolve)) + ); + 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]]); + + await setGlobal("foo", 4); + await new Promise(resolve => + cps.removeGlobal("foo", null, makeCallback(resolve)) + ); + 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]]); + await reset(); +}); + +add_task(async function populateViaRemoveByDomain() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await set("b.a.com", "foo", 3); + await set("b.a.com", "bar", 4); + await new Promise(resolve => + cps.removeByDomain("a.com", null, makeCallback(resolve)) + ); + getCachedSubdomainsOK( + ["a.com", "foo"], + [ + ["a.com", undefined], + ["b.a.com", 3], + ] + ); + getCachedSubdomainsOK( + ["a.com", "bar"], + [ + ["a.com", undefined], + ["b.a.com", 4], + ] + ); + + await set("a.com", "foo", 5); + await set("a.com", "bar", 6); + await new Promise(resolve => + cps.removeBySubdomain("a.com", null, makeCallback(resolve)) + ); + getCachedSubdomainsOK( + ["a.com", "foo"], + [ + ["a.com", undefined], + ["b.a.com", undefined], + ] + ); + getCachedSubdomainsOK( + ["a.com", "bar"], + [ + ["a.com", undefined], + ["b.a.com", undefined], + ] + ); + + await setGlobal("foo", 7); + await setGlobal("bar", 8); + await new Promise(resolve => + cps.removeAllGlobals(null, makeCallback(resolve)) + ); + getCachedGlobalOK(["foo"], true, undefined); + getCachedGlobalOK(["bar"], true, undefined); + await reset(); +}); + +add_task(async function populateViaRemoveAllDomains() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await set("b.com", "foo", 3); + await set("b.com", "bar", 4); + await new Promise(resolve => + cps.removeAllDomains(null, makeCallback(resolve)) + ); + 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]]); + await reset(); +}); + +add_task(async function populateViaRemoveByName() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await new Promise(resolve => + cps.removeByName("foo", null, makeCallback(resolve)) + ); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + getCachedGlobalOK(["foo"], true, undefined); + getCachedGlobalOK(["bar"], true, 4); + + await new Promise(resolve => + cps.removeByName("bar", null, makeCallback(resolve)) + ); + getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]); + getCachedSubdomainsOK(["a.com", "bar"], [["a.com", undefined]]); + getCachedGlobalOK(["foo"], true, undefined); + getCachedGlobalOK(["bar"], true, undefined); + await reset(); +}); + +add_task(async function privateBrowsing() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await set("b.com", "foo", 5); + + let context = privateLoadContext; + await set("a.com", "foo", 6, context); + await 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]]); + await reset(); +}); + +add_task(async 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)); + await reset(); +}); 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 0000000000..7015174d53 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js @@ -0,0 +1,76 @@ +/* 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/. */ + +add_task(async function resetBeforeTests() { + await reset(); +}); + +add_task(async function get_nonexistent() { + await getSubdomainsOK(["a.com", "foo"], []); + await reset(); +}); + +add_task(async function isomorphicDomains() { + await set("a.com", "foo", 1); + await getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + await getSubdomainsOK(["http://a.com/huh", "foo"], [["a.com", 1]]); + await reset(); +}); + +add_task(async function names() { + await set("a.com", "foo", 1); + await getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + + await set("a.com", "bar", 2); + await getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + await getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + + await setGlobal("foo", 3); + await getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + await getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + await reset(); +}); + +add_task(async function subdomains() { + await set("a.com", "foo", 1); + await set("b.a.com", "foo", 2); + await getSubdomainsOK( + ["a.com", "foo"], + [ + ["a.com", 1], + ["b.a.com", 2], + ] + ); + await getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]); + await reset(); +}); + +add_task(async function privateBrowsing() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await set("b.com", "foo", 5); + + let context = privateLoadContext; + await set("a.com", "foo", 6, context); + await setGlobal("foo", 7, context); + await getSubdomainsOK(["a.com", "foo", context], [["a.com", 6]]); + await getSubdomainsOK(["a.com", "bar", context], [["a.com", 2]]); + await getSubdomainsOK(["b.com", "foo", context], [["b.com", 5]]); + + await getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]); + await getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]); + await getSubdomainsOK(["b.com", "foo"], [["b.com", 5]]); + await reset(); +}); + +add_task(async 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)); + await reset(); +}); 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 0000000000..916fc55498 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js @@ -0,0 +1,78 @@ +/* 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 dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + dbFile.append("content-prefs.sqlite"); + + ok(!dbFile.exists(), "Db should not exist yet."); + + var dbConnection = Services.storage.openDatabase(dbFile); + equal(dbConnection.schemaVersion, 0); + + dbConnection.executeSimpleSQL(schema_version3); + dbConnection.schemaVersion = 3; + + dbConnection.close(); +} + +add_task(async function resetBeforeTests() { + prepareVersion3Schema(); +}); + +// 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. +add_task(async function testMigration() { + // Test migrated db content. + await schemaVersionIs(CURRENT_DB_VERSION); + 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"], + ]; + await dbOK(dbExpectedState); + + // Migrated fields should have timestamp set to 0. + await new Promise(resolve => + cps.removeAllDomainsSince(1000, null, makeCallback(resolve)) + ); + await dbOK(dbExpectedState); + + await new Promise(resolve => + cps.removeAllDomainsSince(0, null, makeCallback(resolve)) + ); + await dbOK([[null, "zoom-setting", 0.1]]); + + // Test that dates are present after migration (column is added). + const timestamp = 1234; + await setWithDate("a.com", "pref-name", "val", timestamp); + let actualTimestamp = await getDate("a.com", "pref-name"); + equal(actualTimestamp, timestamp); + await reset(); +}); diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema5.js b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema5.js new file mode 100644 index 0000000000..a904542899 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema5.js @@ -0,0 +1,66 @@ +/* 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/. */ + +ChromeUtils.defineESModuleGetters(this, { + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", +}); + +const MAX_LENGTH = Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH; +const LONG_DATA_URL = `data:,${new Array(MAX_LENGTH).fill("x").join("")}`; + +// Dump of version we migrate from +const schema_queries = [ + "PRAGMA foreign_keys=OFF", + "CREATE TABLE groups (id INTEGER PRIMARY KEY, name TEXT NOT NULL)", + `INSERT INTO groups VALUES (1,'foo.com'), + (2,'bar.com'), + (3,'data:image/png;base64,1234'), + (4,'file:///d/test.file'), + (5,'${LONG_DATA_URL}')`, + "CREATE TABLE settings (id INTEGER PRIMARY KEY, name TEXT NOT NULL)", + `INSERT INTO settings VALUES (1,'zoom-setting'), + (2,'browser.download.lastDir')`, + `CREATE TABLE 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)`, + `INSERT INTO prefs VALUES (1,1,1,0.5,0), + (2,1,2,'/download/dir',0), + (3,2,1,0.3,0), + (4,NULL,1,0.1,0), + (5,3,2,'/download/dir',0), + (6,4,2,'/download/dir',0), + (7,5,1,0.7,0)`, + "CREATE INDEX groups_idx ON groups(name)", + "CREATE INDEX settings_idx ON settings(name)", + "CREATE INDEX prefs_idx ON prefs(timestamp, groupID, settingID)", +]; + +add_setup(async function () { + let conn = await Sqlite.openConnection({ + path: PathUtils.join(PathUtils.profileDir, "content-prefs.sqlite"), + }); + Assert.equal(await conn.getSchemaVersion(), 0); + await conn.executeTransaction(async () => { + for (let query of schema_queries) { + await conn.execute(query); + } + }); + await conn.setSchemaVersion(4); + await conn.close(); +}); + +add_task(async function test() { + // Test migrated db content. + await schemaVersionIs(CURRENT_DB_VERSION); + let dbExpectedState = [ + [null, "zoom-setting", 0.1], + ["bar.com", "zoom-setting", 0.3], + ["foo.com", "zoom-setting", 0.5], + [LONG_DATA_URL.substring(0, MAX_LENGTH - 1), "zoom-setting", 0.7], + ["foo.com", "browser.download.lastDir", "/download/dir"], + ]; + await dbOK(dbExpectedState); +}); 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 0000000000..cc31cd740a --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_observers.js @@ -0,0 +1,291 @@ +/* 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/. */ + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task(async function resetBeforeTests() { + await reset(); +}); + +function raceWithIdleDispatch(promise) { + return Promise.race([ + promise, + new Promise(resolve => { + Services.tm.idleDispatchToMainThread(() => resolve(null)); + }), + ]); +} + +var tests = [ + async function observerForName_set(context) { + let argsPromise; + argsPromise = on("Set", ["foo", null, "bar"]); + await set("a.com", "foo", 1, context); + let args = await argsPromise; + observerArgsOK(args.foo, [["a.com", "foo", 1, context.usePrivateBrowsing]]); + observerArgsOK(args.null, [ + ["a.com", "foo", 1, context.usePrivateBrowsing], + ]); + observerArgsOK(args.bar, []); + + argsPromise = on("Set", ["foo", null, "bar"]); + await setGlobal("foo", 2, context); + args = await argsPromise; + observerArgsOK(args.foo, [[null, "foo", 2, context.usePrivateBrowsing]]); + observerArgsOK(args.null, [[null, "foo", 2, context.usePrivateBrowsing]]); + observerArgsOK(args.bar, []); + await reset(); + }, + + async function observerForName_remove(context) { + let argsPromise; + await set("a.com", "foo", 1, context); + await setGlobal("foo", 2, context); + + argsPromise = on("Removed", ["foo", null, "bar"]); + await new Promise(resolve => + cps.removeByDomainAndName( + "a.com", + "bogus", + context, + makeCallback(resolve) + ) + ); + let args = await raceWithIdleDispatch(argsPromise); + strictEqual(args, null); + + argsPromise = on("Removed", ["foo", null, "bar"]); + await new Promise(resolve => + cps.removeByDomainAndName("a.com", "foo", context, makeCallback(resolve)) + ); + args = await argsPromise; + observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.null, [["a.com", "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.bar, []); + + argsPromise = on("Removed", ["foo", null, "bar"]); + await new Promise(resolve => + cps.removeGlobal("foo", context, makeCallback(resolve)) + ); + args = await argsPromise; + observerArgsOK(args.foo, [[null, "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.null, [[null, "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.bar, []); + await reset(); + }, + + async function observerForName_removeByDomain(context) { + let argsPromise; + await set("a.com", "foo", 1, context); + await set("b.a.com", "bar", 2, context); + await setGlobal("foo", 3, context); + + argsPromise = on("Removed", ["foo", null, "bar"]); + await new Promise(resolve => + cps.removeByDomain("bogus", context, makeCallback(resolve)) + ); + let args = await raceWithIdleDispatch(argsPromise); + strictEqual(args, null); + + argsPromise = on("Removed", ["foo", null, "bar"]); + await new Promise(resolve => + cps.removeBySubdomain("a.com", context, makeCallback(resolve)) + ); + args = await argsPromise; + 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]]); + + argsPromise = on("Removed", ["foo", null, "bar"]); + await new Promise(resolve => + cps.removeAllGlobals(context, makeCallback(resolve)) + ); + args = await argsPromise; + observerArgsOK(args.foo, [[null, "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.null, [[null, "foo", context.usePrivateBrowsing]]); + observerArgsOK(args.bar, []); + await reset(); + }, + + async function observerForName_removeAllDomains(context) { + let argsPromise; + await set("a.com", "foo", 1, context); + await setGlobal("foo", 2, context); + await set("b.com", "bar", 3, context); + + argsPromise = on("Removed", ["foo", null, "bar"]); + await new Promise(resolve => + cps.removeAllDomains(context, makeCallback(resolve)) + ); + let args = await argsPromise; + 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]]); + await reset(); + }, + + async function observerForName_removeByName(context) { + let argsPromise; + await set("a.com", "foo", 1, context); + await set("a.com", "bar", 2, context); + await setGlobal("foo", 3, context); + + argsPromise = on("Removed", ["foo", null, "bar"]); + await new Promise(resolve => + cps.removeByName("bogus", context, makeCallback(resolve)) + ); + let args = await raceWithIdleDispatch(argsPromise); + strictEqual(args, null); + + argsPromise = on("Removed", ["foo", null, "bar"]); + await new Promise(resolve => + cps.removeByName("foo", context, makeCallback(resolve)) + ); + args = await argsPromise; + 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, []); + await reset(); + }, + + async function removeObserverForName(context) { + let { promise, observers } = onEx("Set", ["foo", null, "bar"]); + cps.removeObserverForName("foo", observers.foo); + await set("a.com", "foo", 1, context); + await wait(); + let args = await promise; + observerArgsOK(args.foo, []); + observerArgsOK(args.null, [ + ["a.com", "foo", 1, context.usePrivateBrowsing], + ]); + observerArgsOK(args.bar, []); + args.reset(); + + cps.removeObserverForName(null, args.null.observer); + await set("a.com", "foo", 2, context); + await wait(); + observerArgsOK(args.foo, []); + observerArgsOK(args.null, []); + observerArgsOK(args.bar, []); + args.reset(); + await 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 = [ + async function observerForName_removeAllDomainsSince() { + let argsPromise; + await setWithDate("a.com", "foo", 1, 100, null); + await setWithDate("b.com", "foo", 2, 200, null); + await setWithDate("c.com", "foo", 3, 300, null); + + await setWithDate("a.com", "bar", 1, 0, null); + await setWithDate("b.com", "bar", 2, 100, null); + await setWithDate("c.com", "bar", 3, 200, null); + await setGlobal("foo", 2, null); + + argsPromise = on("Removed", ["foo", "bar", null]); + await new Promise(resolve => + cps.removeAllDomainsSince(200, null, makeCallback(resolve)) + ); + + let args = await argsPromise; + + 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], + ]); + await reset(); + }, + + async function observerForName_removeAllDomainsSince_private() { + let argsPromise; + let context = privateLoadContext; + await setWithDate("a.com", "foo", 1, 100, context); + await setWithDate("b.com", "foo", 2, 200, context); + await setWithDate("c.com", "foo", 3, 300, context); + + await setWithDate("a.com", "bar", 1, 0, context); + await setWithDate("b.com", "bar", 2, 100, context); + await setWithDate("c.com", "bar", 3, 200, context); + await setGlobal("foo", 2, context); + + argsPromise = on("Removed", ["foo", "bar", null]); + await new Promise(resolve => + cps.removeAllDomainsSince(200, context, makeCallback(resolve)) + ); + + let args = await argsPromise; + + 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], + ]); + await reset(); + }, +]; + +for (let i = 0; i < tests.length; i++) { + // Generate two wrappers of each test function that invoke the original test with an + // appropriate privacy context. + /* eslint-disable no-eval */ + var pub = eval( + "var f = async function " + + tests[i].name + + "() { await tests[" + + i + + "](privateLoadContext); }; f" + ); + var priv = eval( + "var f = async function " + + tests[i].name + + "_private() { await tests[" + + i + + "](privateLoadContext); }; f" + ); + /* eslint-enable no-eval */ + add_task(pub); + add_task(priv); +} + +for (let test of specialTests) { + add_task(test); +} 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 0000000000..0c9e797903 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js @@ -0,0 +1,267 @@ +/* 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/. */ + +add_task(async function resetBeforeTests() { + await reset(); +}); + +add_task(async function nonexistent() { + await set("a.com", "foo", 1); + await setGlobal("foo", 2); + + await new Promise(resolve => + cps.removeByDomainAndName("a.com", "bogus", null, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + await getOK(["a.com", "foo"], 1); + await getGlobalOK(["foo"], 2); + + await new Promise(resolve => + cps.removeBySubdomainAndName("a.com", "bogus", null, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + await getOK(["a.com", "foo"], 1); + await getGlobalOK(["foo"], 2); + + await new Promise(resolve => + cps.removeGlobal("bogus", null, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + await getOK(["a.com", "foo"], 1); + await getGlobalOK(["foo"], 2); + + await new Promise(resolve => + cps.removeByDomainAndName("bogus", "bogus", null, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + await getOK(["a.com", "foo"], 1); + await getGlobalOK(["foo"], 2); + await reset(); +}); + +add_task(async function isomorphicDomains() { + await set("a.com", "foo", 1); + await new Promise(resolve => + cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve)) + ); + await dbOK([]); + await getOK(["a.com", "foo"], undefined); + + await set("a.com", "foo", 2); + await new Promise(resolve => + cps.removeByDomainAndName( + "http://a.com/huh", + "foo", + null, + makeCallback(resolve) + ) + ); + await dbOK([]); + await getOK(["a.com", "foo"], undefined); + await reset(); +}); + +add_task(async function names() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + + await new Promise(resolve => + cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "bar", 2], + [null, "foo", 3], + [null, "bar", 4], + ]); + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], 2); + await getGlobalOK(["foo"], 3); + await getGlobalOK(["bar"], 4); + + await new Promise(resolve => + cps.removeGlobal("foo", null, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "bar", 2], + [null, "bar", 4], + ]); + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], 2); + await getGlobalOK(["foo"], undefined); + await getGlobalOK(["bar"], 4); + + await new Promise(resolve => + cps.removeByDomainAndName("a.com", "bar", null, makeCallback(resolve)) + ); + await dbOK([[null, "bar", 4]]); + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], undefined); + await getGlobalOK(["foo"], undefined); + await getGlobalOK(["bar"], 4); + + await new Promise(resolve => + cps.removeGlobal("bar", null, makeCallback(resolve)) + ); + await dbOK([]); + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], undefined); + await getGlobalOK(["foo"], undefined); + await getGlobalOK(["bar"], undefined); + await reset(); +}); + +add_task(async function subdomains() { + await set("a.com", "foo", 1); + await set("b.a.com", "foo", 2); + await new Promise(resolve => + cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve)) + ); + await dbOK([["b.a.com", "foo", 2]]); + await getSubdomainsOK(["a.com", "foo"], [["b.a.com", 2]]); + await getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]); + + await set("a.com", "foo", 3); + await new Promise(resolve => + cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback(resolve)) + ); + await dbOK([]); + await getSubdomainsOK(["a.com", "foo"], []); + await getSubdomainsOK(["b.a.com", "foo"], []); + + await set("a.com", "foo", 4); + await set("b.a.com", "foo", 5); + await new Promise(resolve => + cps.removeByDomainAndName("b.a.com", "foo", null, makeCallback(resolve)) + ); + await dbOK([["a.com", "foo", 4]]); + await getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]); + await getSubdomainsOK(["b.a.com", "foo"], []); + + await set("b.a.com", "foo", 6); + await new Promise(resolve => + cps.removeBySubdomainAndName("b.a.com", "foo", null, makeCallback(resolve)) + ); + await dbOK([["a.com", "foo", 4]]); + await getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]); + await getSubdomainsOK(["b.a.com", "foo"], []); + await reset(); +}); + +add_task(async function privateBrowsing() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await setGlobal("qux", 5); + await set("b.com", "foo", 6); + await set("b.com", "bar", 7); + + let context = privateLoadContext; + await set("a.com", "foo", 8, context); + await setGlobal("foo", 9, context); + await new Promise(resolve => + cps.removeByDomainAndName("a.com", "foo", context, makeCallback(resolve)) + ); + await new Promise(resolve => + cps.removeGlobal("foo", context, makeCallback(resolve)) + ); + await new Promise(resolve => + cps.removeGlobal("qux", context, makeCallback(resolve)) + ); + await new Promise(resolve => + cps.removeByDomainAndName("b.com", "foo", context, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "bar", 2], + [null, "bar", 4], + ["b.com", "bar", 7], + ]); + await getOK(["a.com", "foo", context], undefined); + await getOK(["a.com", "bar", context], 2); + await getGlobalOK(["foo", context], undefined); + await getGlobalOK(["bar", context], 4); + await getGlobalOK(["qux", context], undefined); + await getOK(["b.com", "foo", context], undefined); + await getOK(["b.com", "bar", context], 7); + + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], 2); + await getGlobalOK(["foo"], undefined); + await getGlobalOK(["bar"], 4); + await getGlobalOK(["qux"], undefined); + await getOK(["b.com", "foo"], undefined); + await getOK(["b.com", "bar"], 7); + await reset(); +}); + +add_task(async 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")); + await reset(); +}); + +add_task(async function removeByDomainAndName_invalidateCache() { + await set("a.com", "foo", 1); + getCachedOK(["a.com", "foo"], true, 1); + let promiseRemoved = new Promise(resolve => { + cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve)); + }); + getCachedOK(["a.com", "foo"], false); + await promiseRemoved; + await reset(); +}); + +add_task(async function removeBySubdomainAndName_invalidateCache() { + await set("a.com", "foo", 1); + await set("b.a.com", "foo", 2); + getCachedSubdomainsOK( + ["a.com", "foo"], + [ + ["a.com", 1], + ["b.a.com", 2], + ] + ); + let promiseRemoved = new Promise(resolve => { + cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback(resolve)); + }); + getCachedSubdomainsOK(["a.com", "foo"], []); + await promiseRemoved; + await reset(); +}); + +add_task(async function removeGlobal_invalidateCache() { + await setGlobal("foo", 1); + getCachedGlobalOK(["foo"], true, 1); + let promiseRemoved = new Promise(resolve => { + cps.removeGlobal("foo", null, makeCallback(resolve)); + }); + getCachedGlobalOK(["foo"], false); + await promiseRemoved; + await reset(); +}); 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 0000000000..7615757f97 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js @@ -0,0 +1,94 @@ +/* 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/. */ + +add_task(async function resetBeforeTests() { + await reset(); +}); + +add_task(async function nonexistent() { + await setGlobal("foo", 1); + await new Promise(resolve => + cps.removeAllDomains(null, makeCallback(resolve)) + ); + await dbOK([[null, "foo", 1]]); + await getGlobalOK(["foo"], 1); + await reset(); +}); + +add_task(async function domains() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await set("b.com", "foo", 5); + await set("b.com", "bar", 6); + + await new Promise(resolve => + cps.removeAllDomains(null, makeCallback(resolve)) + ); + await dbOK([ + [null, "foo", 3], + [null, "bar", 4], + ]); + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], undefined); + await getGlobalOK(["foo"], 3); + await getGlobalOK(["bar"], 4); + await getOK(["b.com", "foo"], undefined); + await getOK(["b.com", "bar"], undefined); + await reset(); +}); + +add_task(async function privateBrowsing() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await set("b.com", "foo", 5); + + let context = privateLoadContext; + await set("a.com", "foo", 6, context); + await setGlobal("foo", 7, context); + await new Promise(resolve => + cps.removeAllDomains(context, makeCallback(resolve)) + ); + await dbOK([ + [null, "foo", 3], + [null, "bar", 4], + ]); + await getOK(["a.com", "foo", context], undefined); + await getOK(["a.com", "bar", context], undefined); + await getGlobalOK(["foo", context], 7); + await getGlobalOK(["bar", context], 4); + await getOK(["b.com", "foo", context], undefined); + + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], undefined); + await getGlobalOK(["foo"], 3); + await getGlobalOK(["bar"], 4); + await getOK(["b.com", "foo"], undefined); + await reset(); +}); + +add_task(async function erroneous() { + do_check_throws(() => cps.removeAllDomains(null, "bogus")); + await reset(); +}); + +add_task(async function invalidateCache() { + await set("a.com", "foo", 1); + await set("b.com", "bar", 2); + await setGlobal("baz", 3); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["b.com", "bar"], true, 2); + getCachedGlobalOK(["baz"], true, 3); + let promiseRemoved = new Promise(resolve => + cps.removeAllDomains(null, makeCallback(resolve)) + ); + getCachedOK(["a.com", "foo"], false); + getCachedOK(["b.com", "bar"], false); + getCachedGlobalOK(["baz"], true, 3); + await promiseRemoved; + await reset(); +}); 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 0000000000..a78d35054b --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js @@ -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/. */ + +add_task(async function resetBeforeTests() { + await reset(); +}); + +add_task(async function nonexistent() { + await setGlobal("foo", 1); + await new Promise(resolve => + cps.removeAllDomainsSince(0, null, makeCallback(resolve)) + ); + await getGlobalOK(["foo"], 1); + await reset(); +}); + +add_task(async function domainsAll() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await set("b.com", "foo", 5); + await set("b.com", "bar", 6); + + await new Promise(resolve => + cps.removeAllDomainsSince(0, null, makeCallback(resolve)) + ); + await dbOK([ + [null, "foo", 3], + [null, "bar", 4], + ]); + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], undefined); + await getGlobalOK(["foo"], 3); + await getGlobalOK(["bar"], 4); + await getOK(["b.com", "foo"], undefined); + await getOK(["b.com", "bar"], undefined); + await reset(); +}); + +add_task(async function domainsWithDate() { + await setWithDate("a.com", "foobar", 0, 0); + await setWithDate("a.com", "foo", 1, 1000); + await setWithDate("a.com", "bar", 2, 4000); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await setWithDate("b.com", "foo", 5, 2000); + await setWithDate("b.com", "bar", 6, 3000); + await setWithDate("b.com", "foobar", 7, 1000); + + await new Promise(resolve => + cps.removeAllDomainsSince(2000, null, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "foobar", 0], + ["a.com", "foo", 1], + [null, "foo", 3], + [null, "bar", 4], + ["b.com", "foobar", 7], + ]); + await reset(); +}); + +add_task(async function privateBrowsing() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await set("b.com", "foo", 5); + + let context = privateLoadContext; + await set("a.com", "foo", 6, context); + await setGlobal("foo", 7, context); + await new Promise(resolve => + cps.removeAllDomainsSince(0, context, makeCallback(resolve)) + ); + await dbOK([ + [null, "foo", 3], + [null, "bar", 4], + ]); + await getOK(["a.com", "foo", context], undefined); + await getOK(["a.com", "bar", context], undefined); + await getGlobalOK(["foo", context], 7); + await getGlobalOK(["bar", context], 4); + await getOK(["b.com", "foo", context], undefined); + + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], undefined); + await getGlobalOK(["foo"], 3); + await getGlobalOK(["bar"], 4); + await getOK(["b.com", "foo"], undefined); + await reset(); +}); + +add_task(async function erroneous() { + do_check_throws(() => cps.removeAllDomainsSince(null, "bogus")); + await reset(); +}); + +add_task(async function invalidateCache() { + await setWithDate("a.com", "foobar", 0, 0); + await setWithDate("a.com", "foo", 1, 1000); + await setWithDate("a.com", "bar", 2, 4000); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await setWithDate("b.com", "foo", 5, 2000); + await setWithDate("b.com", "bar", 6, 3000); + await setWithDate("b.com", "foobar", 7, 1000); + let clearPromise = new Promise(resolve => { + cps.removeAllDomainsSince(0, null, makeCallback(resolve)); + }); + 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); + await clearPromise; + await reset(); +}); 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 0000000000..b68d589dbf --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js @@ -0,0 +1,229 @@ +/* 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/. */ + +add_task(async function resetBeforeTests() { + await reset(); +}); + +add_task(async function nonexistent() { + await set("a.com", "foo", 1); + await setGlobal("foo", 2); + + await new Promise(resolve => + cps.removeByDomain("bogus", null, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + await getOK(["a.com", "foo"], 1); + await getGlobalOK(["foo"], 2); + + await new Promise(resolve => + cps.removeBySubdomain("bogus", null, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + await getOK(["a.com", "foo"], 1); + await getGlobalOK(["foo"], 2); + await reset(); +}); + +add_task(async function isomorphicDomains() { + await set("a.com", "foo", 1); + await new Promise(resolve => + cps.removeByDomain("a.com", null, makeCallback(resolve)) + ); + await dbOK([]); + await getOK(["a.com", "foo"], undefined); + + await set("a.com", "foo", 2); + await new Promise(resolve => + cps.removeByDomain("http://a.com/huh", null, makeCallback(resolve)) + ); + await dbOK([]); + await getOK(["a.com", "foo"], undefined); + await reset(); +}); + +add_task(async function domains() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await set("b.com", "foo", 5); + await set("b.com", "bar", 6); + + await new Promise(resolve => + cps.removeByDomain("a.com", null, makeCallback(resolve)) + ); + await dbOK([ + [null, "foo", 3], + [null, "bar", 4], + ["b.com", "foo", 5], + ["b.com", "bar", 6], + ]); + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], undefined); + await getGlobalOK(["foo"], 3); + await getGlobalOK(["bar"], 4); + await getOK(["b.com", "foo"], 5); + await getOK(["b.com", "bar"], 6); + + await new Promise(resolve => + cps.removeAllGlobals(null, makeCallback(resolve)) + ); + await dbOK([ + ["b.com", "foo", 5], + ["b.com", "bar", 6], + ]); + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], undefined); + await getGlobalOK(["foo"], undefined); + await getGlobalOK(["bar"], undefined); + await getOK(["b.com", "foo"], 5); + await getOK(["b.com", "bar"], 6); + + await new Promise(resolve => + cps.removeByDomain("b.com", null, makeCallback(resolve)) + ); + await dbOK([]); + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], undefined); + await getGlobalOK(["foo"], undefined); + await getGlobalOK(["bar"], undefined); + await getOK(["b.com", "foo"], undefined); + await getOK(["b.com", "bar"], undefined); + await reset(); +}); + +add_task(async function subdomains() { + await set("a.com", "foo", 1); + await set("b.a.com", "foo", 2); + await new Promise(resolve => + cps.removeByDomain("a.com", null, makeCallback(resolve)) + ); + await dbOK([["b.a.com", "foo", 2]]); + await getSubdomainsOK(["a.com", "foo"], [["b.a.com", 2]]); + await getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]); + + await set("a.com", "foo", 3); + await new Promise(resolve => + cps.removeBySubdomain("a.com", null, makeCallback(resolve)) + ); + await dbOK([]); + await getSubdomainsOK(["a.com", "foo"], []); + await getSubdomainsOK(["b.a.com", "foo"], []); + + await set("a.com", "foo", 4); + await set("b.a.com", "foo", 5); + await new Promise(resolve => + cps.removeByDomain("b.a.com", null, makeCallback(resolve)) + ); + await dbOK([["a.com", "foo", 4]]); + await getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]); + await getSubdomainsOK(["b.a.com", "foo"], []); + + await set("b.a.com", "foo", 6); + await new Promise(resolve => + cps.removeBySubdomain("b.a.com", null, makeCallback(resolve)) + ); + await dbOK([["a.com", "foo", 4]]); + await getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]); + await getSubdomainsOK(["b.a.com", "foo"], []); + await reset(); +}); + +add_task(async function privateBrowsing() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await set("b.com", "foo", 5); + + let context = privateLoadContext; + await set("a.com", "foo", 6, context); + await set("b.com", "foo", 7, context); + await setGlobal("foo", 8, context); + await new Promise(resolve => + cps.removeByDomain("a.com", context, makeCallback(resolve)) + ); + await getOK(["b.com", "foo", context], 7); + await getGlobalOK(["foo", context], 8); + await new Promise(resolve => + cps.removeAllGlobals(context, makeCallback(resolve)) + ); + await dbOK([["b.com", "foo", 5]]); + await getOK(["a.com", "foo", context], undefined); + await getOK(["a.com", "bar", context], undefined); + await getGlobalOK(["foo", context], undefined); + await getGlobalOK(["bar", context], undefined); + await getOK(["b.com", "foo", context], 5); + + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], undefined); + await getGlobalOK(["foo"], undefined); + await getGlobalOK(["bar"], undefined); + await getOK(["b.com", "foo"], 5); + await reset(); +}); + +add_task(async 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")); + await reset(); +}); + +add_task(async function removeByDomain_invalidateCache() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + getCachedOK(["a.com", "foo"], true, 1); + getCachedOK(["a.com", "bar"], true, 2); + let promiseRemoved = new Promise(resolve => { + cps.removeByDomain("a.com", null, makeCallback(resolve)); + }); + getCachedOK(["a.com", "foo"], false); + getCachedOK(["a.com", "bar"], false); + await promiseRemoved; + await reset(); +}); + +add_task(async function removeBySubdomain_invalidateCache() { + await set("a.com", "foo", 1); + await set("b.a.com", "foo", 2); + getCachedSubdomainsOK( + ["a.com", "foo"], + [ + ["a.com", 1], + ["b.a.com", 2], + ] + ); + let promiseRemoved = new Promise(resolve => { + cps.removeBySubdomain("a.com", null, makeCallback(resolve)); + }); + getCachedSubdomainsOK(["a.com", "foo"], []); + await promiseRemoved; + await reset(); +}); + +add_task(async function removeAllGlobals_invalidateCache() { + await setGlobal("foo", 1); + await setGlobal("bar", 2); + getCachedGlobalOK(["foo"], true, 1); + getCachedGlobalOK(["bar"], true, 2); + let promiseRemoved = new Promise(resolve => { + cps.removeAllGlobals(null, makeCallback(resolve)); + }); + getCachedGlobalOK(["foo"], false); + getCachedGlobalOK(["bar"], false); + await promiseRemoved; + await reset(); +}); 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 0000000000..b7fe310802 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js @@ -0,0 +1,102 @@ +/* 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/. */ + +add_task(async function resetBeforeTests() { + await reset(); +}); + +add_task(async function nonexistent() { + await set("a.com", "foo", 1); + await setGlobal("foo", 2); + + await new Promise(resolve => + cps.removeByName("bogus", null, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "foo", 1], + [null, "foo", 2], + ]); + await getOK(["a.com", "foo"], 1); + await getGlobalOK(["foo"], 2); + await reset(); +}); + +add_task(async function names() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await set("b.com", "foo", 5); + await set("b.com", "bar", 6); + + await new Promise(resolve => + cps.removeByName("foo", null, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "bar", 2], + [null, "bar", 4], + ["b.com", "bar", 6], + ]); + await getOK(["a.com", "foo"], undefined); + await getOK(["a.com", "bar"], 2); + await getGlobalOK(["foo"], undefined); + await getGlobalOK(["bar"], 4); + await getOK(["b.com", "foo"], undefined); + await getOK(["b.com", "bar"], 6); + await reset(); +}); + +add_task(async function privateBrowsing() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await set("b.com", "foo", 5); + await set("b.com", "bar", 6); + + let context = privateLoadContext; + await set("a.com", "foo", 7, context); + await setGlobal("foo", 8, context); + await set("b.com", "bar", 9, context); + await new Promise(resolve => + cps.removeByName("bar", context, makeCallback(resolve)) + ); + await dbOK([ + ["a.com", "foo", 1], + [null, "foo", 3], + ["b.com", "foo", 5], + ]); + await getOK(["a.com", "foo", context], 7); + await getOK(["a.com", "bar", context], undefined); + await getGlobalOK(["foo", context], 8); + await getGlobalOK(["bar", context], undefined); + await getOK(["b.com", "foo", context], 5); + await getOK(["b.com", "bar", context], undefined); + + await getOK(["a.com", "foo"], 1); + await getOK(["a.com", "bar"], undefined); + await getGlobalOK(["foo"], 3); + await getGlobalOK(["bar"], undefined); + await getOK(["b.com", "foo"], 5); + await getOK(["b.com", "bar"], undefined); + await reset(); +}); + +add_task(async function erroneous() { + do_check_throws(() => cps.removeByName("", null)); + do_check_throws(() => cps.removeByName(null, null)); + do_check_throws(() => cps.removeByName("foo", null, "bogus")); + await reset(); +}); + +add_task(async function invalidateCache() { + await set("a.com", "foo", 1); + await 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); + await reset(); +}); 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 0000000000..5b8ecb2bc0 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js @@ -0,0 +1,232 @@ +/* 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/. */ + +add_task(async function resetBeforeTests() { + await reset(); +}); + +add_task(async function get_nonexistent() { + await getOK(["a.com", "foo"], undefined); + await getGlobalOK(["foo"], undefined); + await reset(); +}); + +add_task(async function isomorphicDomains() { + await set("a.com", "foo", 1); + await dbOK([["a.com", "foo", 1]]); + await getOK(["a.com", "foo"], 1); + await getOK(["http://a.com/huh", "foo"], 1, "a.com"); + + await set("http://a.com/huh", "foo", 2); + await dbOK([["a.com", "foo", 2]]); + await getOK(["a.com", "foo"], 2); + await getOK(["http://a.com/yeah", "foo"], 2, "a.com"); + await reset(); +}); + +add_task(async function names() { + await set("a.com", "foo", 1); + await dbOK([["a.com", "foo", 1]]); + await getOK(["a.com", "foo"], 1); + + await set("a.com", "bar", 2); + await dbOK([ + ["a.com", "foo", 1], + ["a.com", "bar", 2], + ]); + await getOK(["a.com", "foo"], 1); + await getOK(["a.com", "bar"], 2); + + await setGlobal("foo", 3); + await dbOK([ + ["a.com", "foo", 1], + ["a.com", "bar", 2], + [null, "foo", 3], + ]); + await getOK(["a.com", "foo"], 1); + await getOK(["a.com", "bar"], 2); + await getGlobalOK(["foo"], 3); + + await setGlobal("bar", 4); + await dbOK([ + ["a.com", "foo", 1], + ["a.com", "bar", 2], + [null, "foo", 3], + [null, "bar", 4], + ]); + await getOK(["a.com", "foo"], 1); + await getOK(["a.com", "bar"], 2); + await getGlobalOK(["foo"], 3); + await getGlobalOK(["bar"], 4); + await reset(); +}); + +add_task(async function subdomains() { + await set("a.com", "foo", 1); + await set("b.a.com", "foo", 2); + await dbOK([ + ["a.com", "foo", 1], + ["b.a.com", "foo", 2], + ]); + await getOK(["a.com", "foo"], 1); + await getOK(["b.a.com", "foo"], 2); + await reset(); +}); + +add_task(async function privateBrowsing() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await set("b.com", "foo", 5); + + let context = privateLoadContext; + await set("a.com", "foo", 6, context); + await setGlobal("foo", 7, context); + await dbOK([ + ["a.com", "foo", 1], + ["a.com", "bar", 2], + [null, "foo", 3], + [null, "bar", 4], + ["b.com", "foo", 5], + ]); + await getOK(["a.com", "foo", context], 6, "a.com"); + await getOK(["a.com", "bar", context], 2); + await getGlobalOK(["foo", context], 7); + await getGlobalOK(["bar", context], 4); + await getOK(["b.com", "foo", context], 5); + + await getOK(["a.com", "foo"], 1); + await getOK(["a.com", "bar"], 2); + await getGlobalOK(["foo"], 3); + await getGlobalOK(["bar"], 4); + await getOK(["b.com", "foo"], 5); + await reset(); +}); + +add_task(async 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")); + await reset(); +}); + +add_task(async 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)); + await reset(); +}); + +add_task(async function set_invalidateCache() { + // (1) Set a pref and wait for it to finish. + await 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() { + // (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. + let fetchedPref; + let getPromise = new Promise(resolve => + cps.getByDomainAndName("a.com", "foo", null, { + handleResult(pref) { + fetchedPref = pref; + }, + handleCompletion() { + // (7) Finally, this callback should be called after set's above. + Assert.ok(!!fetchedPref); + Assert.equal(fetchedPref.value, 2); + resolve(); + }, + }) + ); + + await getPromise; + await reset(); +}); + +add_task(async function get_nameOnly() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await set("b.com", "foo", 3); + await setGlobal("foo", 4); + + await 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 = privateLoadContext; + await set("b.com", "foo", 5, context); + + await getOKEx( + "getByName", + ["foo", context], + [ + { domain: "a.com", name: "foo", value: 1 }, + { domain: null, name: "foo", value: 4 }, + { domain: "b.com", name: "foo", value: 5 }, + ] + ); + await reset(); +}); + +add_task(async 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; + await set("a.com", "foo", 1); + let timestamp = await getDate("a.com", "foo"); + ok( + start <= timestamp, + "Timestamp is not too early (" + start + "<=" + timestamp + ")." + ); + ok( + timestamp <= end, + "Timestamp is not too late (" + timestamp + "<=" + end + ")." + ); + await reset(); +}); + +add_task(async function maxLength() { + const MAX_LENGTH = Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH; + const LONG_DATA_URL = `data:,${new Array(MAX_LENGTH).fill("x").join("")}`; + await set(LONG_DATA_URL, "foo", 1); + await getOK( + [LONG_DATA_URL, "foo"], + 1, + LONG_DATA_URL.substring(0, MAX_LENGTH - 1) + ); +}); 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 0000000000..41e18191e5 --- /dev/null +++ b/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini @@ -0,0 +1,18 @@ +[DEFAULT] +head = head.js +skip-if = toolkit == 'android' +support-files = AsyncRunner.sys.mjs + +[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_migrationToSchema5.js] +[test_removeAllDomainsSince.js] |