Skip to content

Commit 16fe634

Browse files
committed
Bug 1816998: Replace macOS quarantine database-based attribution with extended attribute attribution r=nalexander
With extended attribute attribution using exactly the same attribution strings as Windows (eg: no URLs), this turns out to be quite straightforward. Once we've pulled in the attribution string from the extended attribute, the existing parsing, validation, etc. just works. The only wrinkle is that the extended attributes may have nul bytes or tabs that we need to strip away. (Tabs may be present because we use them to pad the attribution area when we prepare the DMG for attribution. Nul bytes may be present because we overwrite the entire attribution before updating the attribution data.) Differential Revision: https://phabricator.services.mozilla.com/D189258
1 parent 28077f3 commit 16fe634

10 files changed

+117
-472
lines changed

browser/components/attribution/AttributionCode.sys.mjs

Lines changed: 6 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -190,43 +190,6 @@ export var AttributionCode = {
190190
return {};
191191
},
192192

193-
/**
194-
* Returns an object containing a key-value pair for each piece of attribution
195-
* data included in the passed-in URL containing a query string encoding an
196-
* attribution code.
197-
*
198-
* We have less control of the attribution codes on macOS so we accept more
199-
* URLs than we accept attribution codes on Windows.
200-
*
201-
* If the URL is empty, returns an empty object.
202-
*
203-
* If the URL doesn't parse, throws.
204-
*/
205-
parseAttributionCodeFromUrl(url) {
206-
if (!url) {
207-
return {};
208-
}
209-
210-
let parsed = {};
211-
212-
let params = new URL(url).searchParams;
213-
for (let key of ATTR_CODE_KEYS) {
214-
// We support the key prefixed with utm_ or not, but intentionally
215-
// choose non-utm params over utm params.
216-
for (let paramKey of [`utm_${key}`, `funnel_${key}`, key]) {
217-
if (params.has(paramKey)) {
218-
// We expect URI-encoded components in our attribution codes.
219-
let value = encodeURIComponent(params.get(paramKey));
220-
if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
221-
parsed[key] = value;
222-
}
223-
}
224-
}
225-
}
226-
227-
return parsed;
228-
},
229-
230193
/**
231194
* Returns a string serializing the given attribution data.
232195
*
@@ -285,14 +248,15 @@ export var AttributionCode = {
285248
`getAttrDataAsync: macOS && !exists("${attributionFile.path}")`
286249
);
287250

288-
// On macOS, we fish the attribution data from the system quarantine DB.
251+
// On macOS, we fish the attribution data from an extended attribute on
252+
// the .app bundle directory.
289253
try {
290-
let referrer = await lazy.MacAttribution.getReferrerUrl();
254+
let attrStr = await lazy.MacAttribution.getAttributionString();
291255
lazy.log.debug(
292-
`getAttrDataAsync: macOS attribution getReferrerUrl: "${referrer}"`
256+
`getAttrDataAsync: macOS attribution getAttributionString: "${attrStr}"`
293257
);
294258

295-
gCachedAttrData = this.parseAttributionCodeFromUrl(referrer);
259+
gCachedAttrData = this.parseAttributionCode(attrStr);
296260
} catch (ex) {
297261
// Avoid partial attribution data.
298262
gCachedAttrData = {};
@@ -315,8 +279,7 @@ export var AttributionCode = {
315279
`macOS attribution data is ${JSON.stringify(gCachedAttrData)}`
316280
);
317281

318-
// We only want to try to fetch the referrer from the quarantine
319-
// database once on macOS.
282+
// We only want to try to fetch the attribution string once on macOS
320283
try {
321284
let code = this.serializeAttributionData(gCachedAttrData);
322285
lazy.log.debug(`macOS attribution data serializes as "${code}"`);

browser/components/attribution/MacAttribution.sys.mjs

Lines changed: 29 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -2,126 +2,8 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
const lazy = {};
6-
ChromeUtils.defineLazyGetter(lazy, "log", () => {
7-
let { ConsoleAPI } = ChromeUtils.importESModule(
8-
"resource://gre/modules/Console.sys.mjs"
9-
);
10-
let consoleOptions = {
11-
// tip: set maxLogLevel to "debug" and use lazy.log.debug() to create
12-
// detailed messages during development. See LOG_LEVELS in Console.sys.mjs
13-
// for details.
14-
maxLogLevel: "error",
15-
maxLogLevelPref: "browser.attribution.mac.loglevel",
16-
prefix: "MacAttribution",
17-
};
18-
return new ConsoleAPI(consoleOptions);
19-
});
20-
21-
ChromeUtils.defineESModuleGetters(lazy, {
22-
Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
23-
});
24-
25-
/**
26-
* Get the location of the user's macOS quarantine database.
27-
* @return {String} path.
28-
*/
29-
function getQuarantineDatabasePath() {
30-
let file = Services.dirsvc.get("Home", Ci.nsIFile);
31-
file.append("Library");
32-
file.append("Preferences");
33-
file.append("com.apple.LaunchServices.QuarantineEventsV2");
34-
return file.path;
35-
}
36-
37-
/**
38-
* Query given path for quarantine extended attributes.
39-
* @param {String} path of the file to query.
40-
* @return {[String, String]} pair of the quarantine data GUID and remaining
41-
* quarantine data (usually, Gatekeeper flags).
42-
* @throws NS_ERROR_NOT_AVAILABLE if there is no quarantine GUID for the given path.
43-
* @throws NS_ERROR_UNEXPECTED if there is a quarantine GUID, but it is malformed.
44-
*/
45-
async function getQuarantineAttributes(path) {
46-
let bytes = await IOUtils.getMacXAttr(path, "com.apple.quarantine");
47-
if (!bytes) {
48-
throw new Components.Exception(
49-
`No macOS quarantine xattrs found for ${path}`,
50-
Cr.NS_ERROR_NOT_AVAILABLE
51-
);
52-
}
53-
54-
let string = new TextDecoder("utf-8").decode(bytes);
55-
let parts = string.split(";");
56-
if (!parts.length) {
57-
throw new Components.Exception(
58-
`macOS quarantine data is not ; separated`,
59-
Cr.NS_ERROR_UNEXPECTED
60-
);
61-
}
62-
let guid = parts[parts.length - 1];
63-
if (guid.length != 36) {
64-
// Like "12345678-90AB-CDEF-1234-567890ABCDEF".
65-
throw new Components.Exception(
66-
`macOS quarantine data guid is not length 36: ${guid.length}`,
67-
Cr.NS_ERROR_UNEXPECTED
68-
);
69-
}
70-
71-
return { guid, parts };
72-
}
73-
74-
/**
75-
* Invoke system SQLite binary to extract the referrer URL corresponding to
76-
* the given GUID from the given macOS quarantine database.
77-
* @param {String} path of the user's macOS quarantine database.
78-
* @param {String} guid to query.
79-
* @return {String} referrer URL.
80-
*/
81-
async function queryQuarantineDatabase(
82-
guid,
83-
path = getQuarantineDatabasePath()
84-
) {
85-
let query = `SELECT COUNT(*), LSQuarantineOriginURLString
86-
FROM LSQuarantineEvent
87-
WHERE LSQuarantineEventIdentifier = '${guid}'
88-
ORDER BY LSQuarantineTimeStamp DESC LIMIT 1`;
89-
90-
let proc = await lazy.Subprocess.call({
91-
command: "/usr/bin/sqlite3",
92-
arguments: [path, query],
93-
environment: {},
94-
stderr: "stdout",
95-
});
96-
97-
let stdout = await proc.stdout.readString();
98-
99-
let { exitCode } = await proc.wait();
100-
if (exitCode != 0) {
101-
throw new Components.Exception(
102-
"Failed to run sqlite3",
103-
Cr.NS_ERROR_UNEXPECTED
104-
);
105-
}
106-
107-
// Output is like "integer|url".
108-
let parts = stdout.split("|", 2);
109-
if (parts.length != 2) {
110-
throw new Components.Exception(
111-
"Failed to parse sqlite3 output",
112-
Cr.NS_ERROR_UNEXPECTED
113-
);
114-
}
115-
116-
if (parts[0].trim() == "0") {
117-
throw new Components.Exception(
118-
`Quarantine database does not contain URL for guid ${guid}`,
119-
Cr.NS_ERROR_UNEXPECTED
120-
);
121-
}
122-
123-
return parts[1].trim();
124-
}
5+
const NUL = 0x0;
6+
const TAB = 0x9;
1257

1268
export var MacAttribution = {
1279
/**
@@ -132,44 +14,36 @@ export var MacAttribution = {
13214
return Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent.path;
13315
},
13416

135-
/**
136-
* Used by the Attributions system to get the download referrer.
137-
*
138-
* @param {String} path to get the quarantine data from.
139-
* Usually this is a `.app` directory but can be any
140-
* (existing) file or directory. Default: `this.applicationPath`.
141-
* @return {String} referrer URL.
142-
* @throws NS_ERROR_NOT_AVAILABLE if there is no quarantine GUID for the given path.
143-
* @throws NS_ERROR_UNEXPECTED if there is a quarantine GUID, but no corresponding referrer URL is known.
144-
*/
145-
async getReferrerUrl(path = this.applicationPath) {
146-
lazy.log.debug(`getReferrerUrl(${JSON.stringify(path)})`);
17+
async setAttributionString(aAttrStr, path = this.applicationPath) {
18+
return IOUtils.setMacXAttr(
19+
path,
20+
"com.apple.application-instance",
21+
new TextEncoder().encode(aAttrStr)
22+
);
23+
},
14724

148-
// First, determine the quarantine GUID assigned by macOS to the given path.
149-
let guid;
150-
try {
151-
guid = (await getQuarantineAttributes(path)).guid;
152-
} catch (ex) {
153-
throw new Components.Exception(
154-
`No macOS quarantine GUID found for ${path}`,
155-
Cr.NS_ERROR_NOT_AVAILABLE
25+
async getAttributionString(path = this.applicationPath) {
26+
let promise = IOUtils.getMacXAttr(path, "com.apple.application-instance");
27+
return promise.then(bytes => {
28+
// We need to process the extended attribute a little bit to isolate
29+
// the attribution string:
30+
// - nul bytes and tabs may be present in raw attribution strings, but are
31+
// never part of the attribution data
32+
// - attribution data is expected to be preceeded by the string `__MOZCUSTOM__`
33+
let attrStr = new TextDecoder().decode(
34+
bytes.filter(b => b != NUL && b != TAB)
15635
);
157-
}
158-
lazy.log.debug(`getReferrerUrl: guid: ${guid}`);
15936

160-
// Second, fish the relevant record from the quarantine database.
161-
let url = "";
162-
try {
163-
url = await queryQuarantineDatabase(guid);
164-
lazy.log.debug(`getReferrerUrl: url: ${url}`);
165-
} catch (ex) {
166-
// This path is known to macOS but we failed to extract a referrer -- be noisy.
167-
throw new Components.Exception(
168-
`No macOS quarantine referrer URL found for ${path} with GUID ${guid}`,
169-
Cr.NS_ERROR_UNEXPECTED
170-
);
171-
}
37+
if (attrStr.startsWith("__MOZCUSTOM__")) {
38+
// Return everything after __MOZCUSTOM__
39+
return attrStr.slice(13);
40+
}
41+
42+
throw new Error(`No attribution data found in ${path}`);
43+
});
44+
},
17245

173-
return url;
46+
async delAttributionString(path = this.applicationPath) {
47+
return IOUtils.delMacXAttr(path, "com.apple.application-instance");
17448
},
17549
};

browser/components/attribution/moz.build

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,6 @@ EXTRA_JS_MODULES += [
1818
SPHINX_TREES["docs"] = "docs"
1919

2020
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
21-
XPIDL_SOURCES += [
22-
"nsIMacAttribution.idl",
23-
]
24-
25-
XPIDL_MODULE = "attribution"
26-
27-
EXPORTS += [
28-
"nsMacAttribution.h",
29-
]
30-
31-
SOURCES += [
32-
"nsMacAttribution.cpp",
33-
]
34-
3521
FINAL_LIBRARY = "browsercomps"
3622

3723
EXTRA_JS_MODULES += [

browser/components/attribution/nsIMacAttribution.idl

Lines changed: 0 additions & 23 deletions
This file was deleted.

browser/components/attribution/nsMacAttribution.cpp

Lines changed: 0 additions & 50 deletions
This file was deleted.

browser/components/attribution/nsMacAttribution.h

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)