Bug 1805514 - Part 3: Allow JS opaque relaunch data and actions. r=nrishel

This commit replaces two existing launch argument keys, `launchURL`
and `privilegedName`, with an opaque string of data.  Here opaque
means, "does not need to be inspected by the Windows notification
server DLL" (and in general, by the system backend components).

The existing `action` argument key was always intended for this
purpose but was not used in the first implementation.  Here, we make
`action` a stringified JSON object, which is easy for API consumers to
manage and generalizes to (mostly) arbitrary relaunch data.

This JSON object is a compound `notificationData` object containing
both:
- the consumer-provided `opaqueRelaunchData` (generally, an action);
- and implementation-provided details (the alert's name, if
  privileged, etc).
This compound object and the fact that everything transits as strings
makes everything a little more confusing than it really is.

The API to this opaque relaunch data is based on strings for
convenience.  It would be possible to manage JSON objects, perhaps by
using `nsIStructuredCloneContainer` to serialize "structured clone
encodable" JS objects across the process boundaries, but managing the
objects and container in that approach is much more effort than having
the API consumer stringify as desired.

In addition, this patch makes the notification server extract the
Firefox `action` data from the Windows toast `arguments` passed to the
server callback.  Since this fallback data is now provided to Firefox
at launch, there's no need to fetch it from the Windows notification
object; we simply need to know whether to pass through to a Windows
8.1 callback (`tagWasHandled=true`) or to act on the fallback data
(`tagWasHandled=false`).  This is simpler than teaching Firefox to
extract the arguments for toast itself or the appropriate action
button.

Differential Revision: https://phabricator.services.mozilla.com/D182314
This commit is contained in:
Nick Alexander
2023-07-15 02:34:06 +00:00
parent e66481faac
commit 9f2bea61fe
13 changed files with 434 additions and 265 deletions

View File

@@ -17,6 +17,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
ShellService: "resource:///modules/ShellService.sys.mjs",
SpecialMessageActions:
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
UpdatePing: "resource://gre/modules/UpdatePing.sys.mjs",
});
@@ -1057,6 +1059,22 @@ nsDefaultCommandLineHandler.prototype = {
break;
}
// All notifications will invoke Firefox with an action. Prior to Bug 1805514,
// this data was extracted from the Windows toast object directly (keyed by the
// notification ID) and not passed over the command line. This is acceptable
// because the data passed is chrome-controlled, but if we implement the `actions`
// part of the DOM Web Notifications API, this will no longer be true:
// content-controlled data might transit over the command line. This could lead
// to escaping bugs and overflows. In the future, we intend to avoid any such
// issue by once again extracting all such data from the Windows toast object.
let notificationData = cmdLine.handleFlagWithParam(
"notification-windowsAction",
false
);
if (!notificationData) {
break;
}
let alertService = lazy.gWindowsAlertsService;
if (!alertService) {
console.error("Windows alert service not available.");
@@ -1064,34 +1082,66 @@ nsDefaultCommandLineHandler.prototype = {
}
async function handleNotification() {
const { launchUrl, privilegedName } =
await alertService.handleWindowsTag(tag);
let { tagWasHandled } = await alertService.handleWindowsTag(tag);
// If `launchUrl` or `privilegedName` are provided, then the
// notification was from a prior instance of the application and we
// need to handled fallback behavior.
if (launchUrl || privilegedName) {
// If the tag was not handled via callback, then the notification was
// from a prior instance of the application and we need to handle
// fallback behavior.
if (!tagWasHandled) {
console.info(
`Completing Windows notification (tag=${JSON.stringify(
tag
)}, launchUrl=${JSON.stringify(
launchUrl
)}, privilegedName=${JSON.stringify(privilegedName)}))`
)}, notificationData=${notificationData})`
);
try {
notificationData = JSON.parse(notificationData);
} catch (e) {
console.error(
`Completing Windows notification (tag=${JSON.stringify(
tag
)}, failed to parse (notificationData=${notificationData})`
);
}
}
if (privilegedName) {
// This is awkward: the relaunch data set by the caller is _wrapped_
// into a compound object that includes additional notification data,
// and everything is exchanged as strings. Unwrap and parse here.
let opaqueRelaunchData = null;
if (notificationData?.opaqueRelaunchData) {
try {
opaqueRelaunchData = JSON.parse(
notificationData.opaqueRelaunchData
);
} catch (e) {
console.error(
`Completing Windows notification (tag=${JSON.stringify(
tag
)}, failed to parse (opaqueRelaunchData=${
notificationData.opaqueRelaunchData
})`
);
}
}
if (notificationData?.privilegedName) {
Services.telemetry.setEventRecordingEnabled(
"browser.launched_to_handle",
true
);
Glean.browserLaunchedToHandle.systemNotification.record({
name: privilegedName,
name: notificationData.privilegedName,
});
}
if (launchUrl) {
let uri = resolveURIInternal(cmdLine, launchUrl);
// If we have an action in the notification data, this will be the
// window to perform the action in.
let winForAction;
if (notificationData?.launchUrl && !opaqueRelaunchData) {
// Unprivileged Web Notifications contain a launch URL and are handled
// slightly differently than privileged notifications with actions.
let uri = resolveURIInternal(cmdLine, notificationData.launchUrl);
if (cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
// Try to find an existing window and load our URI into the current
// tab, new tab, or new window as prefs determine.
@@ -1113,7 +1163,35 @@ nsDefaultCommandLineHandler.prototype = {
} else if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
// No URL provided, but notification was interacted with while the
// application was closed. Fall back to opening the browser without url.
openBrowserWindow(cmdLine, lazy.gSystemPrincipal);
winForAction = openBrowserWindow(cmdLine, lazy.gSystemPrincipal);
await new Promise(resolve => {
Services.obs.addObserver(function observe(subject) {
if (subject == winForAction) {
Services.obs.removeObserver(
observe,
"browser-delayed-startup-finished"
);
resolve();
}
}, "browser-delayed-startup-finished");
});
} else {
// Relaunch in private windows only if we're in perma-private mode.
let allowPrivate =
lazy.PrivateBrowsingUtils.permanentPrivateBrowsing;
winForAction = lazy.BrowserWindowTracker.getTopWindow({
private: allowPrivate,
});
}
if (opaqueRelaunchData && winForAction) {
// Without dispatch, `OPEN_URL` with `where: "tab"` does not work on relaunch.
Services.tm.dispatchToMainThread(() => {
lazy.SpecialMessageActions.handleAction(
opaqueRelaunchData,
winForAction.gBrowser
);
});
}
}