Backed out 6 changesets (bug 1525076) for ES Lint failure. CLOSED TREE

Backed out changeset 912fcc3cb274 (bug 1525076)
Backed out changeset 690d730341c6 (bug 1525076)
Backed out changeset 5dd08176812c (bug 1525076)
Backed out changeset 963c8d33d779 (bug 1525076)
Backed out changeset f7d26b270884 (bug 1525076)
Backed out changeset 95fd52531439 (bug 1525076)
This commit is contained in:
Razvan Maries 2020-10-09 20:17:23 +03:00
Родитель afbeaa9f10
Коммит 9c16898a12
20 изменённых файлов: 273 добавлений и 993 удалений

Просмотреть файл

@ -19,23 +19,6 @@ ChromeUtils.defineModuleGetter(
"Services",
"resource://gre/modules/Services.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"MacAttribution",
"resource:///modules/MacAttribution.jsm"
);
XPCOMUtils.defineLazyGetter(this, "log", () => {
let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {})
.ConsoleAPI;
let consoleOptions = {
// tip: set maxLogLevel to "debug" and use log.debug() to create detailed
// messages during development. See LOG_LEVELS in Console.jsm for details.
maxLogLevel: "error",
maxLogLevelPref: "browser.attribution.loglevel",
prefix: "AttributionCode",
};
return new ConsoleAPI(consoleOptions);
});
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
const ATTR_CODE_MAX_LENGTH = 1010;
@ -54,72 +37,19 @@ const ATTR_CODE_KEYS = [
let gCachedAttrData = null;
/**
* Returns an nsIFile for the file containing the attribution data.
*/
function getAttributionFile() {
let file = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
// appinfo does not exist in xpcshell, so we need defaults.
file.append(Services.appinfo.vendor || "mozilla");
file.append(AppConstants.MOZ_APP_NAME);
file.append("postSigningData");
return file;
}
var AttributionCode = {
/**
* Returns a platform-specific nsIFile for the file containing the attribution
* data, or null if the current platform does not support (caching)
* attribution data.
*/
get attributionFile() {
if (AppConstants.platform == "win") {
let file = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
// appinfo does not exist in xpcshell, so we need defaults.
file.append(Services.appinfo.vendor || "mozilla");
file.append(AppConstants.MOZ_APP_NAME);
if (!file.exists()) {
file.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
}
file.append("postSigningData");
return file;
} else if (AppConstants.platform == "macosx") {
// There's no `UpdRootD` in xpcshell tests. Some existing tests override
// it, which is onerous and difficult to share across tests. When
// testing, if it's not defined, fallback to the xpcshell temp directory.
let file;
try {
file = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
} catch (ex) {
let env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
// It's most common to test for the profile dir, even though we actually
// are using the temp dir.
if (
ex instanceof Ci.nsIException &&
ex.result == Cr.NS_ERROR_FAILURE &&
env.exists("XPCSHELL_TEST_PROFILE_DIR")
) {
let path = env.get("XPCSHELL_TEST_TEMP_DIR");
file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
file.initWithPath(path);
} else {
throw ex;
}
}
if (!file.exists()) {
file.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
}
file.append("macAttributionData");
return file;
}
return null;
},
/**
* Write the given attribution code to the attribution file.
* @param {String} code to write.
*/
async writeAttributionFile(code) {
const file = AttributionCode.attributionFile;
await OS.File.makeDir(file.parent.path, {
ignoreExisting: true,
from: file.parent.parent.path,
});
await OS.File.writeAtomic(file.path, code);
},
/**
* Returns an array of allowed attribution code keys.
*/
@ -146,9 +76,6 @@ var AttributionCode = {
parsed[key] = value;
}
} else {
log.debug(
`parseAttributionCode: "${code}" => isValid = false: "${key}", "${value}"`
);
isValid = false;
break;
}
@ -165,63 +92,6 @@ var AttributionCode = {
return {};
},
/**
* Returns an object containing a key-value pair for each piece of attribution
* data included in the passed-in URL containing a query string encoding an
* attribution code.
*
* We have less control of the attribution codes on macOS so we accept more
* URLs than we accept attribution codes on Windows.
*
* If the URL is empty, returns an empty object.
*
* If the URL doesn't parse, throws.
*/
parseAttributionCodeFromUrl(url) {
if (!url) {
return {};
}
let parsed = {};
let params = new URL(url).searchParams;
for (let key of ATTR_CODE_KEYS) {
// We support the key prefixed with utm_ or not, but intentionally
// choose non-utm params over utm params.
for (let paramKey of [`utm_${key}`, `funnel_${key}`, key]) {
if (params.has(paramKey)) {
// We expect URI-encoded components in our attribution codes.
let value = encodeURIComponent(params.get(paramKey));
if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
parsed[key] = value;
}
}
}
}
return parsed;
},
/**
* Returns a string serializing the given attribution data.
*
* It is expected that the given values are already URL-encoded.
*/
serializeAttributionData(data) {
// Iterating in this way makes the order deterministic.
let s = "";
for (let key of ATTR_CODE_KEYS) {
if (key in data) {
let value = data[key];
if (s) {
s += ATTR_CODE_FIELD_SEPARATOR; // URL-encoded &
}
s += `${key}${ATTR_CODE_KEY_VALUE_SEPARATOR}${value}`; // URL-encoded =
}
}
return s;
},
/**
* Reads the attribution code, either from disk or a cached version.
* Returns a promise that fulfills with an object containing the parsed
@ -234,125 +104,59 @@ var AttributionCode = {
*/
async getAttrDataAsync() {
if (gCachedAttrData != null) {
log.debug(
`getAttrDataAsync: attribution is cached: ${JSON.stringify(
gCachedAttrData
)}`
);
return gCachedAttrData;
}
gCachedAttrData = {};
let attributionFile = this.attributionFile;
if (!attributionFile) {
// This platform doesn't support attribution.
log.debug(`getAttrDataAsync: no attribution (attributionFile is null)`);
return gCachedAttrData;
}
if (
AppConstants.platform == "macosx" &&
!(await OS.File.exists(attributionFile.path))
) {
log.debug(
`getAttrDataAsync: macOS && !exists("${attributionFile.path}")`
);
// On macOS, we fish the attribution data from the system quarantine DB.
if (AppConstants.platform == "win") {
let bytes;
try {
let referrer = await MacAttribution.getReferrerUrl();
log.debug(
`getAttrDataAsync: macOS attribution getReferrerUrl: "${referrer}"`
);
gCachedAttrData = this.parseAttributionCodeFromUrl(referrer);
bytes = await OS.File.read(getAttributionFile().path);
} catch (ex) {
// Avoid partial attribution data.
gCachedAttrData = {};
// No attributions. Just `warn` 'cuz this isn't necessarily an error.
log.warn("Caught exception fetching macOS attribution codes!", ex);
if (
ex instanceof Ci.nsIException &&
ex.result == Cr.NS_ERROR_UNEXPECTED
) {
// Bad quarantine data.
Services.telemetry
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
.add("quarantine_error");
}
}
log.debug(`macOS attribution data is ${JSON.stringify(gCachedAttrData)}`);
// We only want to try to fetch the referrer from the quarantine
// database once on macOS.
try {
let s = this.serializeAttributionData(gCachedAttrData);
log.debug(`macOS attribution data serializes as "${s}"`);
let bytes = new TextEncoder().encode(s);
await OS.File.writeAtomic(attributionFile.path, bytes);
} catch (ex) {
log.debug(`Caught exception writing "${attributionFile.path}"`, ex);
Services.telemetry
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
.add("write_error");
return gCachedAttrData;
}
log.debug(
`Returning after successfully writing "${attributionFile.path}"`
);
return gCachedAttrData;
}
log.debug(`getAttrDataAsync: !macOS || !exists("${attributionFile.path}")`);
let bytes;
try {
bytes = await OS.File.read(attributionFile.path);
} catch (ex) {
if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
log.debug(
`getAttrDataAsync: !exists("${
attributionFile.path
}"), returning ${JSON.stringify(gCachedAttrData)}`
);
return gCachedAttrData;
}
Services.telemetry
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
.add("read_error");
}
if (bytes) {
try {
let decoder = new TextDecoder();
let code = decoder.decode(bytes);
log.debug(
`getAttrDataAsync: ${attributionFile.path} deserializes to ${code}`
);
if (AppConstants.platform == "macosx" && !code) {
// On macOS, an empty attribution code is fine. (On Windows, that
// means the stub/full installer has been incorrectly attributed,
// which is an error.)
if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
return gCachedAttrData;
}
gCachedAttrData = this.parseAttributionCode(code);
log.debug(
`getAttrDataAsync: ${code} parses to ${JSON.stringify(
gCachedAttrData
)}`
);
} catch (ex) {
// TextDecoder can throw an error
Services.telemetry
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
.add("decode_error");
.add("read_error");
}
if (bytes) {
try {
let decoder = new TextDecoder();
let code = decoder.decode(bytes);
gCachedAttrData = this.parseAttributionCode(code);
} catch (ex) {
// TextDecoder can throw an error
Services.telemetry
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
.add("decode_error");
}
}
} else if (AppConstants.platform == "macosx") {
try {
let appPath = Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent
.path;
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
let referrer = attributionSvc.getReferrerUrl(appPath);
let params = new URL(referrer).searchParams;
for (let key of ATTR_CODE_KEYS) {
// We support the key prefixed with utm_ or not, but intentionally
// choose non-utm params over utm params.
for (let paramKey of [`utm_${key}`, `funnel_${key}`, key]) {
if (params.has(paramKey)) {
let value = params.get(paramKey);
if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
gCachedAttrData[key] = value;
}
}
}
}
} catch (ex) {
// No attributions
}
}
return gCachedAttrData;
},
@ -373,7 +177,7 @@ var AttributionCode = {
*/
async deleteFileAsync() {
try {
await OS.File.remove(this.attributionFile.path);
await OS.File.remove(getAttributionFile().path);
} catch (ex) {
// The attribution file may already have been deleted,
// or it may have never been installed at all;

Просмотреть файл

@ -1,186 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["MacAttribution"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyGetter(this, "log", () => {
let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {})
.ConsoleAPI;
let consoleOptions = {
// tip: set maxLogLevel to "debug" and use log.debug() to create detailed
// messages during development. See LOG_LEVELS in Console.jsm for details.
maxLogLevel: "error",
maxLogLevelPref: "browser.attribution.mac.loglevel",
prefix: "MacAttribution",
};
return new ConsoleAPI(consoleOptions);
});
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
ChromeUtils.defineModuleGetter(
this,
"Services",
"resource://gre/modules/Services.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"Subprocess",
"resource://gre/modules/Subprocess.jsm"
);
/**
* Get the location of the user's macOS quarantine database.
* @return {String} path.
*/
function getQuarantineDatabasePath() {
let file = Services.dirsvc.get("Home", Ci.nsIFile);
file.append("Library");
file.append("Preferences");
file.append("com.apple.LaunchServices.QuarantineEventsV2");
return file.path;
}
/**
* Query given path for quarantine extended attributes.
* @param {String} path of the file to query.
* @return {[String, String]} pair of the quarantine data GUID and remaining
* quarantine data (usually, Gatekeeper flags).
* @throws NS_ERROR_NOT_AVAILABLE if there is no quarantine GUID for the given path.
* @throws NS_ERROR_UNEXPECTED if there is a quarantine GUID, but it is malformed.
*/
async function getQuarantineAttributes(path) {
let bytes = await OS.File.macGetXAttr(path, "com.apple.quarantine");
if (!bytes) {
throw new Components.Exception(
`No macOS quarantine xattrs found for ${path}`,
Cr.NS_ERROR_NOT_AVAILABLE
);
}
let string = new TextDecoder("utf-8").decode(bytes);
let parts = string.split(";");
if (!parts.length) {
throw new Components.Exception(
`macOS quarantine data is not ; separated`,
Cr.NS_ERROR_UNEXPECTED
);
}
let guid = parts[parts.length - 1];
if (guid.length != 36) {
// Like "12345678-90AB-CDEF-1234-567890ABCDEF".
throw new Components.Exception(
`macOS quarantine data guid is not length 36: ${guid.length}`,
Cr.NS_ERROR_UNEXPECTED
);
}
return { guid, parts };
}
/**
* Invoke system SQLite binary to extract the referrer URL corresponding to
* the given GUID from the given macOS quarantine database.
* @param {String} path of the user's macOS quarantine database.
* @param {String} guid to query.
* @return {String} referrer URL.
*/
async function queryQuarantineDatabase(
guid,
path = getQuarantineDatabasePath()
) {
let query = `SELECT COUNT(*), LSQuarantineOriginURLString
FROM LSQuarantineEvent
WHERE LSQuarantineEventIdentifier = '${guid}'
ORDER BY LSQuarantineTimeStamp DESC LIMIT 1`;
let proc = await Subprocess.call({
command: "/usr/bin/sqlite3",
arguments: [path, query],
environment: {},
stderr: "stdout",
});
let stdout = await proc.stdout.readString();
let { exitCode } = await proc.wait();
if (exitCode != 0) {
throw new Components.Exception(
"Failed to run sqlite3",
Cr.NS_ERROR_UNEXPECTED
);
}
// Output is like "integer|url".
let parts = stdout.split("|", 2);
if (parts.length != 2) {
throw new Components.Exception(
"Failed to parse sqlite3 output",
Cr.NS_ERROR_UNEXPECTED
);
}
if (parts[0].trim() == "0") {
throw new Components.Exception(
`Quarantine database does not contain URL for guid ${guid}`,
Cr.NS_ERROR_UNEXPECTED
);
}
return parts[1].trim();
}
var MacAttribution = {
/**
* The file path to the `.app` directory.
*/
get applicationPath() {
// On macOS, `GreD` is like "App.app/Contents/macOS". Return "App.app".
return Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent.path;
},
/**
* Used by the Attributions system to get the download referrer.
*
* @param {String} path to get the quarantine data from.
* Usually this is a `.app` directory but can be any
* (existing) file or directory. Default: `this.applicationPath`.
* @return {String} referrer URL.
* @throws NS_ERROR_NOT_AVAILABLE if there is no quarantine GUID for the given path.
* @throws NS_ERROR_UNEXPECTED if there is a quarantine GUID, but no corresponding referrer URL is known.
*/
async getReferrerUrl(path = this.applicationPath) {
log.debug(`getReferrerUrl(${JSON.stringify(path)})`);
// First, determine the quarantine GUID assigned by macOS to the given path.
let guid;
try {
guid = (await getQuarantineAttributes(path)).guid;
} catch (ex) {
throw new Components.Exception(
`No macOS quarantine GUID found for ${path}`,
Cr.NS_ERROR_NOT_AVAILABLE
);
}
log.debug(`getReferrerUrl: guid: ${guid}`);
// Second, fish the relevant record from the quarantine database.
let url = "";
try {
url = await queryQuarantineDatabase(guid);
log.debug(`getReferrerUrl: url: ${url}`);
} catch (ex) {
// This path is known to macOS but we failed to extract a referrer -- be noisy.
throw new Components.Exception(
`No macOS quarantine referrer URL found for ${path} with GUID ${guid}`,
Cr.NS_ERROR_UNEXPECTED
);
}
return url;
},
};

Просмотреть файл

@ -31,7 +31,3 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
]
FINAL_LIBRARY = 'browsercomps'
EXTRA_JS_MODULES += [
'MacAttribution.jsm',
]

Просмотреть файл

@ -9,7 +9,15 @@
interface nsIMacAttributionService : nsISupports
{
/**
* Set the referrer URL on a given path.
* Used by the Attributions system to get the download referrer.
*
* @param aFilePath A path to the file to get the quarantine data from.
* @returns referrerUrl
*/
AString getReferrerUrl(in AUTF8String aFilePath);
/**
* Used by the tests.
*
* @param aFilePath A path to the file to set the quarantine data on.
* @param aReferrer A url to set as the referrer for the download.

Просмотреть файл

@ -16,6 +16,19 @@ using namespace mozilla;
NS_IMPL_ISUPPORTS(nsMacAttributionService, nsIMacAttributionService)
NS_IMETHODIMP
nsMacAttributionService::GetReferrerUrl(const nsACString& aFilePath,
nsAString& aReferrer) {
const nsCString& flat = PromiseFlatCString(aFilePath);
CFStringRef filePath = ::CFStringCreateWithCString(
kCFAllocatorDefault, flat.get(), kCFStringEncodingUTF8);
CocoaFileUtils::CopyQuarantineReferrerUrl(filePath, aReferrer);
::CFRelease(filePath);
return NS_OK;
}
NS_IMETHODIMP
nsMacAttributionService::SetReferrerUrl(const nsACString& aFilePath,
const nsACString& aReferrerUrl,
@ -43,9 +56,7 @@ nsMacAttributionService::SetReferrerUrl(const nsACString& aFilePath,
::CFRelease(filePath);
::CFRelease(referrer);
if (referrerURL) {
::CFRelease(referrerURL);
}
::CFRelease(referrerURL);
return NS_OK;
}

Просмотреть файл

@ -1,8 +1,4 @@
[DEFAULT]
support-files =
head.js
[browser_AttributionCode_telemetry.js]
skip-if = (os != "win" && toolkit != "cocoa") # Windows and macOS only telemetry.
[browser_AttributionCode_Mac_telemetry.js]
skip-if = toolkit != "cocoa" # macOS only telemetry.
skip-if = os != "win" # Windows only telemetry

Просмотреть файл

@ -1,249 +0,0 @@
ChromeUtils.defineModuleGetter(
this,
"TelemetryTestUtils",
"resource://testing-common/TelemetryTestUtils.jsm"
);
const { MacAttribution } = ChromeUtils.import(
"resource:///modules/MacAttribution.jsm"
);
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
async function assertCacheExistsAndIsEmpty() {
// We should have written to the cache, and be able to read back
// with no errors.
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
histogram.clear();
ok(await OS.File.exists(AttributionCode.attributionFile.path));
Assert.deepEqual(
"",
new TextDecoder().decode(
await OS.File.read(AttributionCode.attributionFile.path)
)
);
AttributionCode._clearCache();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Should be able to get cached result");
Assert.deepEqual({}, histogram.snapshot().values || {});
}
add_task(async function test_write_error() {
const sandbox = sinon.createSandbox();
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
attributionSvc.setReferrerUrl(
MacAttribution.applicationPath,
"https://example.com?content=content",
true
);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
try {
// Clear any existing telemetry
histogram.clear();
// Force the file to not exist and then cause a write error. This is delicate
// because various background tasks may invoke `OS.File.writeAtomic` while
// this test is running. Be careful to only stub the one call.
const writeAtomic = sandbox.stub(OS.File, "writeAtomic");
writeAtomic
.withArgs(
sinon.match(AttributionCode.attributionFile.path),
sinon.match.any
)
.throws(() => new Error("write_error"));
OS.File.writeAtomic.callThrough();
// Try to read the attribution code.
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
result,
{ content: "content" },
"Should be able to get a result even if the file doesn't write"
);
TelemetryTestUtils.assertHistogram(histogram, INDEX_WRITE_ERROR, 1);
} finally {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
histogram.clear();
sandbox.restore();
}
});
add_task(async function test_unusual_referrer() {
// This referrer URL looks malformed, but the malformed bits are dropped, so
// it's actually ok. This is what allows extraneous bits like `fbclid` tags
// to be ignored.
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
attributionSvc.setReferrerUrl(
MacAttribution.applicationPath,
"https://example.com?content=&=campaign",
true
);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
try {
// Clear any existing telemetry
histogram.clear();
// Try to read the attribution code
await AttributionCode.getAttrDataAsync();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Should be able to get empty result");
Assert.deepEqual({}, histogram.snapshot().values || {});
await assertCacheExistsAndIsEmpty();
} finally {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
histogram.clear();
}
});
add_task(async function test_blank_referrer() {
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
attributionSvc.setReferrerUrl(MacAttribution.applicationPath, "", true);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
try {
// Clear any existing telemetry
histogram.clear();
// Try to read the attribution code
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Should be able to get empty result");
Assert.deepEqual({}, histogram.snapshot().values || {});
await assertCacheExistsAndIsEmpty();
} finally {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
histogram.clear();
}
});
add_task(async function test_no_referrer() {
const sandbox = sinon.createSandbox();
let newApplicationPath = MacAttribution.applicationPath + ".test";
sandbox.stub(MacAttribution, "applicationPath").get(() => newApplicationPath);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
try {
// Clear any existing telemetry
histogram.clear();
// Try to read the attribution code
await AttributionCode.getAttrDataAsync();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Should be able to get empty result");
Assert.deepEqual({}, histogram.snapshot().values || {});
await assertCacheExistsAndIsEmpty();
} finally {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
histogram.clear();
sandbox.restore();
}
});
add_task(async function test_broken_referrer() {
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
attributionSvc.setReferrerUrl(
MacAttribution.applicationPath,
"https://example.com?content=content",
true
);
// This uses macOS internals to change the GUID so that it will look like the
// application has quarantine data but nothing will be pressent in the
// quarantine database. This shouldn't happen in the wild.
function generateQuarantineGUID() {
let str = Cc["@mozilla.org/uuid-generator;1"]
.getService(Ci.nsIUUIDGenerator)
.generateUUID()
.toString()
.toUpperCase();
// Strip {}.
return str.substring(1, str.length - 1);
}
// These magic constants are macOS GateKeeper flags.
let string = [
"01c1",
"5991b778",
"Safari.app",
generateQuarantineGUID(),
].join(";");
let bytes = new TextEncoder().encode(string);
await OS.File.macSetXAttr(
MacAttribution.applicationPath,
"com.apple.quarantine",
bytes
);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
try {
// Clear any existing telemetry
histogram.clear();
// Try to read the attribution code
await AttributionCode.getAttrDataAsync();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Should be able to get empty result");
TelemetryTestUtils.assertHistogram(histogram, INDEX_QUARANTINE_ERROR, 1);
histogram.clear();
await assertCacheExistsAndIsEmpty();
} finally {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
histogram.clear();
}
});

Просмотреть файл

@ -3,21 +3,37 @@ ChromeUtils.defineModuleGetter(
"TelemetryTestUtils",
"resource://testing-common/TelemetryTestUtils.jsm"
);
const { AttributionCode } = ChromeUtils.import(
"resource:///modules/AttributionCode.jsm"
);
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
add_task(async function test_parse_error() {
if (AppConstants.platform == "macosx") {
// On macOS, the underlying data is the OS-level quarantine
// database. We need to start from nothing to isolate the cache.
const { MacAttribution } = ChromeUtils.import(
"resource:///modules/MacAttribution.jsm"
);
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
attributionSvc.setReferrerUrl(MacAttribution.applicationPath, "", true);
}
async function writeAttributionFile(data) {
let appDir = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
let file = appDir.clone();
file.append(Services.appinfo.vendor || "mozilla");
file.append(AppConstants.MOZ_APP_NAME);
await OS.File.makeDir(file.path, { from: appDir.path, ignoreExisting: true });
file.append("postSigningData");
await OS.File.writeAtomic(file.path, data);
}
add_task(function setup() {
// Clear cache call is only possible in a testing environment
let env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
env.set("XPCSHELL_TEST_PROFILE_DIR", "testing");
registerCleanupFunction(() => {
env.set("XPCSHELL_TEST_PROFILE_DIR", null);
});
});
add_task(async function test_parse_error() {
registerCleanupFunction(async () => {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
@ -40,16 +56,13 @@ add_task(async function test_parse_error() {
// Write an invalid file to trigger a decode error
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
// Empty string is valid on macOS.
await AttributionCode.writeAttributionFile(
AppConstants.platform == "macosx" ? "invalid" : ""
);
await writeAttributionFile(""); // empty string is invalid
result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Should have failed to parse");
// `assertHistogram` also ensures that `read_error` index 0 is 0
// as we should not have recorded telemetry from the previous `getAttrDataAsync` call
TelemetryTestUtils.assertHistogram(histogram, INDEX_DECODE_ERROR, 1);
TelemetryTestUtils.assertHistogram(histogram, 1, 1);
// Reset
histogram.clear();
});
@ -69,16 +82,14 @@ add_task(async function test_read_error() {
// Clear any existing telemetry
histogram.clear();
// Force the file to exist but then cause a read error
const exists = sandbox.stub(OS.File, "exists");
exists.resolves(true);
const read = sandbox.stub(OS.File, "read");
read.throws(() => new Error("read_error"));
// Force a read error
const stub = sandbox.stub(OS.File, "read");
stub.throws(() => new Error("read_error"));
// Try to read the file
await AttributionCode.getAttrDataAsync();
// It should record the read error
TelemetryTestUtils.assertHistogram(histogram, INDEX_READ_ERROR, 1);
TelemetryTestUtils.assertHistogram(histogram, 0, 1);
// Clear any existing telemetry
histogram.clear();

Просмотреть файл

@ -1,27 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const { AttributionCode } = ChromeUtils.import(
"resource:///modules/AttributionCode.jsm"
);
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
// Keep in sync with `BROWSER_ATTRIBUTION_ERRORS` in Histograms.json.
const INDEX_READ_ERROR = 0;
const INDEX_DECODE_ERROR = 1;
const INDEX_WRITE_ERROR = 2;
const INDEX_QUARANTINE_ERROR = 3;
add_task(function setup() {
// AttributionCode._clearCache is only possible in a testing environment
let env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
env.set("XPCSHELL_TEST_PROFILE_DIR", "testing");
registerCleanupFunction(() => {
env.set("XPCSHELL_TEST_PROFILE_DIR", null);
});
});

Просмотреть файл

@ -1,117 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const { AttributionCode } = ChromeUtils.import(
"resource:///modules/AttributionCode.jsm"
);
let validAttrCodes = [
{
code:
"source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D(not%20set)%26content%3D(not%20set)",
parsed: {
source: "google.com",
medium: "organic",
campaign: "(not%20set)",
content: "(not%20set)",
},
},
{
code: "source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D%26content%3D",
parsed: { source: "google.com", medium: "organic" },
doesNotRoundtrip: true, // `campaign=` and `=content` are dropped.
},
{
code: "source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D(not%20set)",
parsed: {
source: "google.com",
medium: "organic",
campaign: "(not%20set)",
},
},
{
code: "source%3Dgoogle.com%26medium%3Dorganic",
parsed: { source: "google.com", medium: "organic" },
},
{ code: "source%3Dgoogle.com", parsed: { source: "google.com" } },
{ code: "medium%3Dgoogle.com", parsed: { medium: "google.com" } },
{ code: "campaign%3Dgoogle.com", parsed: { campaign: "google.com" } },
{ code: "content%3Dgoogle.com", parsed: { content: "google.com" } },
{
code: "experiment%3Dexperimental",
parsed: { experiment: "experimental" },
},
{ code: "variation%3Dvaried", parsed: { variation: "varied" } },
{
code: "ua%3DGoogle%20Chrome%20123",
parsed: { ua: "Google%20Chrome%20123" },
},
];
let invalidAttrCodes = [
// Empty string
"",
// Not escaped
"source=google.com&medium=organic&campaign=(not set)&content=(not set)",
// Too long
"campaign%3D" + "a".repeat(1000),
// Unknown key name
"source%3Dgoogle.com%26medium%3Dorganic%26large%3Dgeneticallymodified",
// Empty key name
"source%3Dgoogle.com%26medium%3Dorganic%26%3Dgeneticallymodified",
];
/**
* Arrange for each test to have a unique application path for storing
* quarantine data.
*
* The quarantine data is necessarily a shared system resource, managed by the
* OS, so we need to avoid polluting it during tests.
*
* There are at least two ways to achieve this. Here we use Sinon to stub the
* relevant accessors: this has the advantage of being local and relatively easy
* to follow. In the App Update Service tests, an `nsIDirectoryServiceProvider`
* is installed, which is global and much harder to extract for re-use.
*/
async function setupStubs() {
// Local imports to avoid polluting the global namespace.
const { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
);
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
// This depends on the caller to invoke it by name. We do try to
// prevent the most obvious incorrect invocation, namely
// `add_task(setupStubs)`.
let caller = Components.stack.caller;
const testID = caller.filename
.toString()
.split("/")
.pop()
.split(".")[0];
notEqual(testID, "head");
let applicationFile = do_get_tempdir();
applicationFile.append(testID);
applicationFile.append("App.app");
if (AppConstants.platform == "macosx") {
// We're implicitly using the fact that modules are shared between importers here.
const { MacAttribution } = ChromeUtils.import(
"resource:///modules/MacAttribution.jsm"
);
sinon
.stub(MacAttribution, "applicationPath")
.get(() => applicationFile.path);
}
// The macOS quarantine database applies to existing paths only, so make
// sure our mock application path exists. This also creates the parent
// directory for the attribution file, needed on both macOS and Windows. We
// don't ignore existing paths because we're inside a temporary directory:
// this should never be invoked twice for the same test.
await OS.File.makeDir(applicationFile.path, { from: do_get_tempdir().path });
}

Просмотреть файл

@ -0,0 +1,63 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const { AttributionCode } = ChromeUtils.import(
"resource:///modules/AttributionCode.jsm"
);
let validAttrCodes = [
{
code:
"source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D(not%20set)%26content%3D(not%20set)",
parsed: {
source: "google.com",
medium: "organic",
campaign: "(not%20set)",
content: "(not%20set)",
},
},
{
code: "source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D%26content%3D",
parsed: { source: "google.com", medium: "organic" },
},
{
code: "source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D(not%20set)",
parsed: {
source: "google.com",
medium: "organic",
campaign: "(not%20set)",
},
},
{
code: "source%3Dgoogle.com%26medium%3Dorganic",
parsed: { source: "google.com", medium: "organic" },
},
{ code: "source%3Dgoogle.com", parsed: { source: "google.com" } },
{ code: "medium%3Dgoogle.com", parsed: { medium: "google.com" } },
{ code: "campaign%3Dgoogle.com", parsed: { campaign: "google.com" } },
{ code: "content%3Dgoogle.com", parsed: { content: "google.com" } },
{
code: "experiment%3Dexperimental",
parsed: { experiment: "experimental" },
},
{ code: "variation%3Dvaried", parsed: { variation: "varied" } },
{
code: "ua%3DGoogle%20Chrome%20123",
parsed: { ua: "Google%20Chrome%20123" },
},
];
let invalidAttrCodes = [
// Empty string
"",
// Not escaped
"source=google.com&medium=organic&campaign=(not set)&content=(not set)",
// Too long
"campaign%3D" + "a".repeat(1000),
// Unknown key name
"source%3Dgoogle.com%26medium%3Dorganic%26large%3Dgeneticallymodified",
// Empty key name
"source%3Dgoogle.com%26medium%3Dorganic%26%3Dgeneticallymodified",
];

Просмотреть файл

@ -9,9 +9,17 @@ const { AppConstants } = ChromeUtils.import(
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
add_task(async () => {
await setupStubs();
});
async function writeAttributionFile(data) {
let appDir = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
let file = appDir.clone();
file.append(Services.appinfo.vendor || "mozilla");
file.append(AppConstants.MOZ_APP_NAME);
await OS.File.makeDir(file.path, { from: appDir.path, ignoreExisting: true });
file.append("postSigningData");
await OS.File.writeAtomic(file.path, data);
}
/**
* Test validation of attribution codes,
@ -20,7 +28,7 @@ add_task(async () => {
add_task(async function testValidAttrCodes() {
for (let entry of validAttrCodes) {
AttributionCode._clearCache();
await AttributionCode.writeAttributionFile(entry.code);
await writeAttributionFile(entry.code);
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
result,
@ -37,7 +45,7 @@ add_task(async function testValidAttrCodes() {
add_task(async function testInvalidAttrCodes() {
for (let code of invalidAttrCodes) {
AttributionCode._clearCache();
await AttributionCode.writeAttributionFile(code);
await writeAttributionFile(code);
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Code should have failed to parse: " + code);
}
@ -50,7 +58,7 @@ add_task(async function testInvalidAttrCodes() {
*/
add_task(async function testDeletedFile() {
// Set up the test by clearing the cache and writing a valid file.
await AttributionCode.writeAttributionFile(validAttrCodes[0].code);
await writeAttributionFile(validAttrCodes[0].code);
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
result,

Просмотреть файл

@ -1,93 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const { MacAttribution } = ChromeUtils.import(
"resource:///modules/MacAttribution.jsm"
);
add_task(async () => {
await setupStubs();
});
add_task(async function testValidAttrCodes() {
let appPath = MacAttribution.applicationPath;
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
for (let entry of validAttrCodes) {
// Set a url referrer. In the macOS quarantine database, the
// referrer URL has components that areURI-encoded. Our test data
// URI-encodes the components and also the separators (?, &, =).
// So we decode it and re-encode it to leave just the components
// URI-encoded.
let url = `http://example.com?${encodeURI(decodeURIComponent(entry.code))}`;
attributionSvc.setReferrerUrl(appPath, url, true);
let referrer = await MacAttribution.getReferrerUrl(appPath);
equal(referrer, url, "overwrite referrer url");
// Read attribution code from referrer.
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
result,
entry.parsed,
"Parsed code should match expected value, code was: " + entry.code
);
// Read attribution code from file.
AttributionCode._clearCache();
result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
result,
entry.parsed,
"Parsed code should match expected value, code was: " + entry.code
);
// Does not overwrite cached existing attribution code.
attributionSvc.setReferrerUrl(appPath, "http://test.com", false);
referrer = await MacAttribution.getReferrerUrl(appPath);
equal(referrer, url, "update referrer url");
result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
result,
entry.parsed,
"Parsed code should match expected value, code was: " + entry.code
);
}
});
add_task(async function testInvalidAttrCodes() {
let appPath = MacAttribution.applicationPath;
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
for (let code of invalidAttrCodes) {
// Set a url referrer. Not all of these invalid codes can be represented
// in the quarantine database; skip those ones.
let url = `http://example.com?${code}`;
let referrer;
try {
attributionSvc.setReferrerUrl(appPath, url, true);
referrer = await MacAttribution.getReferrerUrl(appPath);
} catch (ex) {
continue;
}
if (!referrer) {
continue;
}
equal(referrer, url, "overwrite referrer url");
// Read attribution code from referrer.
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Code should have failed to parse: " + code);
}
});

Просмотреть файл

@ -0,0 +1,39 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { AttributionCode } = ChromeUtils.import(
"resource:///modules/AttributionCode.jsm"
);
add_task(async function test_attribution() {
let appPath = Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent.path;
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
attributionSvc.setReferrerUrl(appPath, "", true);
let referrer = attributionSvc.getReferrerUrl(appPath);
equal(referrer, "", "force an empty referrer url");
// Set a url referrer, testing both utm and non-utm codes
let url = "http://example.com?content=foo&utm_source=bar&utm_content=baz";
attributionSvc.setReferrerUrl(appPath, url, true);
referrer = attributionSvc.getReferrerUrl(appPath);
equal(referrer, url, "overwrite referrer url");
// Does not overwrite existing properties.
attributionSvc.setReferrerUrl(appPath, "http://test.com", false);
referrer = attributionSvc.getReferrerUrl(appPath);
equal(referrer, url, "referrer url is not changed");
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
result,
{ content: "foo", source: "bar" },
"parsed attributes match"
);
});

Просмотреть файл

@ -21,15 +21,6 @@ add_task(async function testValidAttrCodes() {
entry.parsed,
"Parsed code should match expected value, code was: " + entry.code
);
result = AttributionCode.serializeAttributionData(entry.parsed);
if (!entry.doesNotRoundtrip) {
Assert.deepEqual(
result,
entry.code,
"Serialized data should match expected value, code was: " + entry.code
);
}
}
});

Просмотреть файл

@ -1,9 +1,12 @@
[DEFAULT]
firefox-appdir = browser
skip-if = (os != "win" && toolkit != "cocoa") # Only available on Windows and macOS
head = head.js
skip-if = toolkit == 'android'
[test_AttributionCode.js]
[test_MacAttribution.js]
head = head_win.js
skip-if = os != 'win' # windows specific tests
[test_attribution.js]
skip-if = toolkit != "cocoa" # osx specific tests
[test_attribution_parsing.js]
head = head_win.js
skip-if = os != 'win' # windows specific tests

Просмотреть файл

@ -10,6 +10,7 @@ const { XPCOMUtils } = ChromeUtils.import(
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
XPCOMUtils.defineLazyModuleGetters(this, {
AppConstants: "resource://gre/modules/AppConstants.jsm",
OS: "resource://gre/modules/osfile.jsm",
BookmarkPanelHub: "resource://activity-stream/lib/BookmarkPanelHub.jsm",
SnippetsTestMessageProvider:
"resource://activity-stream/lib/SnippetsTestMessageProvider.jsm",
@ -1721,7 +1722,18 @@ class _ASRouter {
// RTAMO messages. This should only be called from within about:newtab#asrouter
/* istanbul ignore next */
async _writeAttributionFile(data) {
await AttributionCode.writeAttributionFile(data);
let appDir = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
let file = appDir.clone();
file.append(Services.appinfo.vendor || "mozilla");
file.append(AppConstants.MOZ_APP_NAME);
await OS.File.makeDir(file.path, {
from: appDir.path,
ignoreExisting: true,
});
file.append("postSigningData");
await OS.File.writeAtomic(file.path, data);
}
/**

Просмотреть файл

@ -11,13 +11,10 @@ const { AttributionCode } = ChromeUtils.import(
const { ASRouterTargeting } = ChromeUtils.import(
"resource://activity-stream/lib/ASRouterTargeting.jsm"
);
const { MacAttribution } = ChromeUtils.import(
"resource:///modules/MacAttribution.jsm"
);
add_task(async function check_attribution_data() {
// Some setup to fake the correct attribution data
const appPath = MacAttribution.applicationPath;
const appPath = Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent.path;
const attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
@ -26,7 +23,7 @@ add_task(async function check_attribution_data() {
const referrer = `https://allizom.org/anything/?utm_campaign=${campaign}&utm_source=${source}`;
attributionSvc.setReferrerUrl(appPath, referrer, true);
AttributionCode._clearCache();
await AttributionCode.getAttrDataAsync();
AttributionCode.getAttrDataAsync();
const {
campaign: attributionCampain,

Просмотреть файл

@ -11118,14 +11118,14 @@
"BROWSER_ATTRIBUTION_ERRORS": {
"record_in_processes": ["main", "content"],
"products": ["firefox"],
"expires_in_version": "90",
"expires_in_version": "86",
"kind": "categorical",
"labels": ["read_error", "decode_error", "write_error", "quarantine_error"],
"description": "Count for the number of errors encountered trying to determine attribution data: on Windows, from the installers (stub and full); on macOS, from the quarantine database.",
"labels": ["read_error", "decode_error"],
"description": "Count for the number of errors encountered trying to read the attribution data from the stub installer.",
"releaseChannelCollection": "opt-out",
"bug_numbers": [1621402, 1525076],
"bug_numbers": [1621402],
"alert_emails": ["aoprea@mozilla.com"],
"operating_systems": ["mac", "windows"]
"operating_systems": ["windows"]
},
"MIXED_CONTENT_PAGE_LOAD": {
"record_in_processes": ["main", "content"],

Просмотреть файл

@ -340,16 +340,29 @@ function spoofPartnerInfo() {
}
}
async function spoofAttributionData() {
if (gIsWindows || gIsMac) {
function getAttributionFile() {
return FileUtils.getFile("LocalAppData", [
"mozilla",
AppConstants.MOZ_APP_NAME,
"postSigningData",
]);
}
function spoofAttributionData() {
if (gIsWindows) {
AttributionCode._clearCache();
await AttributionCode.writeAttributionFile(ATTRIBUTION_CODE);
let stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
Ci.nsIFileOutputStream
);
stream.init(getAttributionFile(), -1, -1, 0);
stream.write(ATTRIBUTION_CODE, ATTRIBUTION_CODE.length);
stream.close();
}
}
function cleanupAttributionData() {
if (gIsWindows || gIsMac) {
AttributionCode.attributionFile.remove(false);
if (gIsWindows) {
getAttributionFile().remove(false);
AttributionCode._clearCache();
}
}
@ -490,7 +503,7 @@ function checkSettingsSection(data) {
Assert.equal(typeof data.settings.defaultPrivateSearchEngineData, "object");
}
if ((gIsWindows || gIsMac) && AppConstants.MOZ_BUILD_APP == "browser") {
if (gIsWindows && AppConstants.MOZ_BUILD_APP == "browser") {
Assert.equal(typeof data.settings.attribution, "object");
Assert.equal(data.settings.attribution.source, "google.com");
}