diff --git a/toolkit/components/search/nsSearchService.js b/toolkit/components/search/nsSearchService.js index b65bb9a9d865..feeef1f279a7 100644 --- a/toolkit/components/search/nsSearchService.js +++ b/toolkit/components/search/nsSearchService.js @@ -2818,6 +2818,75 @@ Engine.prototype = { return this.__id = this._file.path; }, + // This indicates where we found the .xml file to load the engine, + // and attempts to hide user-identifiable data (such as username). + get _anonymizedLoadPath() { + /* Examples of expected output: + * jar:[app]/omni.ja!browser/engine.xml + * 'browser' here is the name of the chrome package, not a folder. + * [profile]/searchplugins/engine.xml + * [distribution]/searchplugins/common/engine.xml + * [other]/engine.xml + */ + + let leafName = this._getLeafName(); + if (!leafName) + return "null"; + + let prefix = "", suffix = ""; + let file = this._file; + if (!file) { + let uri = this._uri; + if (uri.schemeIs("chrome")) { + let packageName = uri.hostPort; + uri = gChromeReg.convertChromeURL(uri); + if (uri instanceof Ci.nsINestedURI) { + prefix = "jar:"; + suffix = "!" + packageName + "/" + leafName; + uri = uri.innermostURI; + } + uri.QueryInterface(Ci.nsIFileURL) + file = uri.file; + } else { + return "[" + uri.scheme + "]/" + leafName; + } + } + + let id; + let enginePath = file.path; + + const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD"; + const NS_APP_USER_PROFILE_50_DIR = "ProfD"; + const XRE_APP_DISTRIBUTION_DIR = "XREAppDist"; + + const knownDirs = { + app: NS_XPCOM_CURRENT_PROCESS_DIR, + profile: NS_APP_USER_PROFILE_50_DIR, + distribution: XRE_APP_DISTRIBUTION_DIR + }; + + for (let key in knownDirs) { + let path; + try { + path = getDir(knownDirs[key]).path; + } catch(e) { + // Getting XRE_APP_DISTRIBUTION_DIR throws during unit tests. + continue; + } + if (enginePath.startsWith(path)) { + id = "[" + key + "]" + enginePath.slice(path.length).replace(/\\/g, "/"); + break; + } + } + + // If the folder doesn't have a known ancestor, don't record its path to + // avoid leaking user identifiable data. + if (!id) + id = "[other]/" + file.leafName; + + return prefix + id + suffix; + }, + get _installLocation() { if (this.__installLocation === null) { if (!this._file) { @@ -3200,6 +3269,10 @@ function SearchService() { SearchService.prototype = { classID: Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}"), + get wrappedJSObject() { + return this; + }, + // The current status of initialization. Note that it does not determine if // initialization is complete, only if an error has been encountered so far. _initRV: Cr.NS_OK, @@ -4597,6 +4670,71 @@ SearchService.prototype = { notifyAction(this._currentEngine, SEARCH_ENGINE_CURRENT); }, + getDefaultEngineInfo() { + let result = {}; + + let engine; + try { + engine = this.defaultEngine; + } catch(e) { + // The defaultEngine getter will throw if there's no engine at all, + // which shouldn't happen unless an add-on or a test deleted all of them. + // Our preferences UI doesn't let users do that. + Cu.reportError("getDefaultEngineInfo: No default engine"); + } + + if (!engine) { + result.name = "NONE"; + } else { + if (engine.name) + result.name = engine.name; + + result.loadPath = engine._anonymizedLoadPath; + + // For privacy, we only collect the submission URL for engines + // from the application or distribution folder... + let sendSubmissionURL = + /^(?:jar:|\[app\]|\[distribution\])/.test(result.loadPath); + + // ... or engines sorted by default near the top of the list. + if (!sendSubmissionURL) { + let extras = + Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra."); + + for (let prefName of extras) { + try { + if (result.name == Services.prefs.getCharPref(prefName)) { + sendSubmissionURL = true; + break; + } + } catch(e) {} + } + + let prefNameBase = getGeoSpecificPrefName(BROWSER_SEARCH_PREF + "order"); + let i = 0; + while (!sendSubmissionURL) { + let prefName = prefNameBase + "." + (++i); + let engineName = getLocalizedPref(prefName); + if (!engineName) + break; + if (result.name == engineName) { + sendSubmissionURL = true; + break; + } + } + } + + if (sendSubmissionURL) { + let uri = engine._getURLOfType("text/html") + .getSubmission("", engine, "searchbar").uri; + uri.userPass = ""; // Avoid reporting a username or password. + result.submissionURL = uri.spec; + } + } + + return result; + }, + /** * This map is built lazily after the available search engines change. It * allows quick parsing of an URL representing a search submission into the diff --git a/toolkit/components/telemetry/TelemetryEnvironment.jsm b/toolkit/components/telemetry/TelemetryEnvironment.jsm index 4a1630526084..207d1529c72a 100644 --- a/toolkit/components/telemetry/TelemetryEnvironment.jsm +++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm @@ -880,6 +880,8 @@ EnvironmentCache.prototype = { this._currentEnvironment.settings = this._currentEnvironment.settings || {}; // Update the search engine entry in the current environment. this._currentEnvironment.settings.defaultSearchEngine = this._getDefaultSearchEngine(); + this._currentEnvironment.settings.defaultSearchEngineData = + Services.search.wrappedJSObject.getDefaultEngineInfo(); }, /** diff --git a/toolkit/components/telemetry/docs/environment.rst b/toolkit/components/telemetry/docs/environment.rst index 416fb029ba29..8ce36cae5775 100644 --- a/toolkit/components/telemetry/docs/environment.rst +++ b/toolkit/components/telemetry/docs/environment.rst @@ -35,6 +35,11 @@ Structure:: blocklistEnabled: , // true on failure isDefaultBrowser: , // null on failure, not available on Android defaultSearchEngine: , // e.g. "yahoo" + defaultSearchEngineData: {, // data about the current default engine + name: , // engine name, e.g. "Yahoo"; or "NONE" if no default + loadPath: , // where the engine line is located; missing if no default + submissionURL: // missing if no default or for user-installed engines + }, e10sEnabled: , // false on failure telemetryEnabled: , // false on failure locale: , // e.g. "it", null on failure @@ -197,6 +202,8 @@ Settings defaultSearchEngine ~~~~~~~~~~~~~~~~~~~ +Note: Deprecated, use defaultSearchEngineData instead. + Contains the string identifier or name of the default search engine provider. This will not be present in environment data collected before the Search Service initialization. The special value ``NONE`` could occur if there is no default search engine. @@ -204,3 +211,24 @@ The special value ``NONE`` could occur if there is no default search engine. The special value ``UNDEFINED`` could occur if a default search engine exists but its identifier could not be determined. This field's contents are ``Services.search.defaultEngine.identifier`` (if defined) or ``"other-"`` + ``Services.search.defaultEngine.name`` if not. In other words, search engines without an ``.identifier`` are prefixed with ``other-``. + +defaultSearchEngineData +~~~~~~~~~~~~~~~~~~~~~~~ +Contains data identifying the engine currently set as the default. + +The object contains: + +- a ``name`` property with the name of the engine, or ``NONE`` if no + engine is currently set as the default. + +- a ``loadPath`` property: an anonymized path of the engine xml file, e.g. + jar:[app]/omni.ja!browser/engine.xml + (where 'browser' is the name of the chrome package, not a folder) + [profile]/searchplugins/engine.xml + [distribution]/searchplugins/common/engine.xml + [other]/engine.xml + +- a ``submissionURL`` property with the HTTP url we would use to search. + For privacy, we don't record this for user-installed engines. + +``loadPath`` and ``submissionURL`` are not present if ``name`` is ``NONE``. diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js index d7d3fb780b46..7c024591fdc2 100644 --- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js @@ -284,6 +284,7 @@ function checkSettingsSection(data) { // Check "defaultSearchEngine" separately, as it can either be undefined or string. if ("defaultSearchEngine" in data.settings) { checkString(data.settings.defaultSearchEngine); + Assert.equal(typeof data.settings.defaultSearchEngineData, "object"); } } @@ -994,6 +995,7 @@ add_task(function* test_defaultSearchEngine() { let data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.ok(!("defaultSearchEngine" in data.settings)); + Assert.ok(!("defaultSearchEngineData" in data.settings)); // Load the engines definitions from a custom JAR file: that's needed so that // the search provider reports an engine identifier. @@ -1011,6 +1013,12 @@ add_task(function* test_defaultSearchEngine() { data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.equal(data.settings.defaultSearchEngine, "telemetrySearchIdentifier"); + let expectedSearchEngineData = { + name: "telemetrySearchIdentifier", + loadPath: "jar:[other]/searchTest.jar!testsearchplugin/telemetrySearchIdentifier.xml", + submissionURL: "http://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB?search=&sourceid=Mozilla-search" + }; + Assert.deepEqual(data.settings.defaultSearchEngineData, expectedSearchEngineData); // Remove all the search engines. for (let engine of Services.search.getEngines()) { @@ -1025,6 +1033,7 @@ add_task(function* test_defaultSearchEngine() { data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.equal(data.settings.defaultSearchEngine, "NONE"); + Assert.deepEqual(data.settings.defaultSearchEngineData, {name:"NONE"}); // Add a new search engine (this will have no engine identifier). const SEARCH_ENGINE_ID = "telemetry_default"; @@ -1044,6 +1053,12 @@ add_task(function* test_defaultSearchEngine() { const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID; Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE); + + const EXPECTED_SEARCH_ENGINE_DATA = { + name: "telemetry_default", + loadPath: "[profile]/searchplugins/telemetrydefault.xml" + }; + Assert.deepEqual(data.settings.defaultSearchEngineData, EXPECTED_SEARCH_ENGINE_DATA); TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault"); // Define and reset the test preference.