Source code

Revision control

Copy as Markdown

Other Tools

/**
* 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, {
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "localization", () => {
return new Localization(["preview/credentialChooser.ftl"], true);
});
XPCOMUtils.defineLazyServiceGetter(
lazy,
"IDNService",
"@mozilla.org/network/idn-service;1",
"nsIIDNService"
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"TESTING_MODE",
"dom.security.credentialmanagement.chooser.testing.enabled",
false
);
/**
* Set an image element's src attribute to a data: url of the favicon for a
* given origin, defaulting to the browser default favicon.
*
* @param {HTMLImageElement} icon The image Element that should have source be the icon result.
* @param {string} origin The origin whose favicon should be used.
*/
async function setIconToFavicon(icon, origin) {
try {
let iconData = await lazy.PlacesUtils.promiseFaviconData(origin);
icon.src = iconData.uri.spec;
} catch {
icon.src = "chrome://global/skin/icons/defaultFavicon.svg";
}
}
/**
* Class implementing the nsICredentialChooserService.
*
* This class shows UI to the user for the Credential Chooser for the
* Credential Management API.
*
* @class CredentialChooserService
*/
export class CredentialChooserService {
classID = Components.ID("{673ddc19-03e2-4b30-a868-06297e8fed89}");
QueryInterface = ChromeUtils.generateQI(["nsICredentialChooserService"]);
/**
* @typedef {object} CredentialArgument
* @property {string} id - The unique identifier for the credential.
* @property {string} type - The type of the credential.
* @property {string} [origin] - The origin associated to the credential.
* @property {UIHints} [uiHints] - UI hints for the credential.
*/
/**
* @typedef {object} UIHints
* @property {string} [name] - The display name for the credential.
* @property {string} [iconURL] - The data URL of the icon for the credential.
* @property {number} [expiresAfter] - The expiration time for the UI hint.
*/
/**
* This function displays the credential chooser UI, allowing the user to make an identity choice.
* Once the user makes a choice from the credentials provided, or dismisses the prompt, we will
* call the callback with that credential, or null in the case of a dismiss.
*
* We also support UI-less testing via choices provided by picking any credential with ID 'wpt-pick-me'
* if the preference 'dom.security.credentialmanagement.chooser.testing.enabled' is true.
*
* @param {BrowsingContext} browsingContext The browsing context of the window calling the Credential Management API.
* @param {Array<CredentialArgument>} credentials The credentials the user should choose from.
* @param {nsICredentialChosenCallback} callback A callback to return the user's credential choice to.
* @returns {nsresult}
*/
async showCredentialChooser(browsingContext, credentials, callback) {
if (!callback) {
callback = { notify: () => {} };
}
if (!credentials.length) {
return Cr.NS_ERROR_INVALID_ARG;
}
// If we are not an active BC, return no choice and bail out.
if (!browsingContext?.currentWindowContext?.isActiveInTab) {
callback.notify(null);
return Cr.NS_OK;
}
if (lazy.TESTING_MODE) {
let match = credentials.find(cred => cred.id == "wpt-pick-me");
if (match) {
if (browsingContext.currentWindowGlobal?.documentPrincipal) {
Services.perms.addFromPrincipal(
browsingContext.currentWindowGlobal.documentPrincipal,
"credential-allow-silent-access^" + match.origin,
Ci.nsIPermissionManager.ALLOW_ACTION,
Ci.nsIPermissionManager.EXPIRE_SESSION
);
Services.perms.addFromPrincipal(
browsingContext.currentWindowGlobal.documentPrincipal,
"credential-allow-silent-access",
Ci.nsIPermissionManager.ALLOW_ACTION,
Ci.nsIPermissionManager.EXPIRE_SESSION
);
}
callback.notify("wpt-pick-me");
} else {
callback.notify(null);
}
return Cr.NS_OK;
}
let browser = browsingContext.topFrameElement;
if (browser?.tagName != "browser") {
return Cr.NS_ERROR_INVALID_ARG;
}
let headerTextElement = browser.ownerDocument.getElementById(
"credential-chooser-header-text"
);
let host = browser.ownerGlobal.gIdentityHandler.getHostForDisplay();
browser.ownerDocument.l10n.setAttributes(
headerTextElement,
"credential-chooser-header",
{
host,
}
);
let faviconPromises = [];
let localizationPromise = lazy.localization.formatMessages([
{ id: "credential-chooser-sign-in-button" },
{ id: "credential-chooser-cancel-button" },
]);
let listBox = browser.ownerDocument.getElementById(
"credential-chooser-entry-selector-container"
);
while (listBox.firstChild) {
listBox.removeChild(listBox.lastChild);
}
let itemTemplate = browser.ownerDocument.getElementById(
"template-credential-entry-list-item"
);
for (let [index, credential] of credentials.entries()) {
let newItem = itemTemplate.content.firstElementChild.cloneNode(true);
// Add the new radio button, including pre-selection and the callback
let [radio] = newItem.getElementsByClassName(
"identity-credential-list-item-radio"
);
radio.value = index;
if (index == 0) {
radio.checked = true;
}
let providerURL = new URL(credential.origin);
let displayDomain = lazy.IDNService.convertToDisplayIDN(
providerURL.host,
{}
);
let [primary] = newItem.getElementsByClassName(
"identity-credential-list-item-label-primary"
);
let [secondary] = newItem.getElementsByClassName(
"identity-credential-list-item-label-secondary"
);
let [icon] = newItem.getElementsByClassName(
"identity-credential-list-item-icon"
);
if (
credential.uiHints &&
(credential.uiHints.expiresAfter == null ||
credential.uiHints.expiresAfter > 0)
) {
primary.textContent = credential.uiHints.name;
browser.ownerDocument.l10n.setAttributes(
secondary,
"credential-chooser-host-descriptor",
{
provider: displayDomain,
}
);
secondary.hidden = false;
icon.src = credential.uiHints.iconURL;
} else {
let doneWithFavicon = setIconToFavicon(icon, credential.origin);
faviconPromises.push(doneWithFavicon);
browser.ownerDocument.l10n.setAttributes(
primary,
"credential-chooser-identity",
{
provider: displayDomain,
}
);
}
// Add the item to the DOM!
listBox.append(newItem);
}
// wait for the labels to be localized before showing the panel
let [accept, cancel] = await localizationPromise;
let cancelLabel = cancel.attributes.find(x => x.name == "label").value;
let cancelKey = cancel.attributes.find(x => x.name == "accesskey").value;
let acceptLabel = accept.attributes.find(x => x.name == "label").value;
let acceptKey = accept.attributes.find(x => x.name == "accesskey").value;
// wait for icons to be set to prevent favicon jank
await Promise.all(faviconPromises);
// Construct the necessary arguments for notification behavior
let options = {
hideClose: true,
removeOnDismissal: true,
eventCallback: (topic, _nextRemovalReason, _isCancel) => {
if (topic == "removed") {
callback.notify(null);
}
},
};
let mainAction = {
label: acceptLabel,
accessKey: acceptKey,
callback() {
let result = listBox.querySelector(
".identity-credential-list-item-radio:checked"
).value;
if (browsingContext.currentWindowGlobal?.documentPrincipal) {
Services.perms.addFromPrincipal(
browsingContext.currentWindowGlobal.documentPrincipal,
"credential-allow-silent-access^" +
credentials[parseInt(result)].origin,
Ci.nsIPermissionManager.ALLOW_ACTION,
Ci.nsIPermissionManager.EXPIRE_SESSION
);
Services.perms.addFromPrincipal(
browsingContext.currentWindowGlobal.documentPrincipal,
"credential-allow-silent-access",
Ci.nsIPermissionManager.ALLOW_ACTION,
Ci.nsIPermissionManager.EXPIRE_SESSION
);
}
callback.notify(credentials[parseInt(result, 10)].id);
},
};
let secondaryActions = [
{
label: cancelLabel,
accessKey: cancelKey,
callback() {
callback.notify(null);
},
},
];
browser.ownerGlobal.PopupNotifications.show(
browser,
"credential-chooser",
"",
"identity-credential-notification-icon",
mainAction,
secondaryActions,
options
);
return Cr.NS_OK;
}
/**
* Dismiss the credential chooser dialog for this browsing context's window.
*
* @param {BrowsingContext} browsingContext - The top browsing context of the window calling the Credential Management API
*/
cancelCredentialChooser(browsingContext) {
let browser = browsingContext.top.embedderElement;
if (browser?.tagName != "browser") {
return;
}
let notification = browser.ownerGlobal.PopupNotifications.getNotification(
"credential-chooser",
browser
);
if (notification) {
browser.ownerGlobal.PopupNotifications.remove(notification, true);
}
}
/**
* A service function to help any UI. Fetches and serializes images to
* data urls, which can be used in chrome UI.
*
* @param {Window} window - Window which should perform the fetch
* @param {nsIURI} uri - Icon location to be fetched from
* @returns {Promise<string, Error>} The data URI encoded as a string representing the icon that was loaded
*/
async fetchImageToDataURI(window, uri) {
if (uri.protocol === "data:") {
return uri.href;
}
let request = new window.Request(uri.spec, { mode: "cors" });
request.overrideContentPolicyType(Ci.nsIContentPolicy.TYPE_IMAGE);
let blob;
let response = await window.fetch(request);
if (!response.ok) {
return Promise.reject(new Error("HTTP failure on Fetch"));
}
blob = await response.blob();
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
}