/* 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/. */
const lazy = {};
ChromeUtils.defineLazyGetter(
lazy,
"l10n",
() =>
new Localization(
["branding/brand.ftl", "messenger/oauthResultPage.ftl"],
false
)
);
// We can cache these resources, since they don't change during runtime of this
// script.
ChromeUtils.defineLazyGetter(lazy, "brandLogo", () =>
getSVG("chrome://branding/content/about-logo.svg")
);
ChromeUtils.defineLazyGetter(lazy, "brandWordmark", () =>
getSVG("chrome://branding/content/about-wordmark.svg")
);
ChromeUtils.defineLazyGetter(lazy, "favicon", () =>
getImage("chrome://branding/content/icon32.png")
);
ChromeUtils.defineLazyGetter(lazy, "htmlTemplate", () =>
getTextContent("chrome://messenger/content/oauthResult.html")
);
/**
* Escape a string for display in raw HTML. String shouldn't already contain
* entity encoding. Converts only characters used for HTML markup into entities:
* - &: used to start an entity
* - >: ending a tag
* - <: starting a tag
* - ": attribute value start/end
* - ': attribute value start/end
*
* @param {string} str - Raw string to display in HTML.
* @returns {string} HTML-safe presentation of the string.
*/
function htmlEscape(str) {
return str
.replaceAll("&", "&")
.replaceAll(">", ">")
.replaceAll("<", "<")
.replaceAll('"', """)
.replaceAll("'", "'");
}
/**
* Format a fluent string to include an element, linking to a specific URL
* without allowing the string to modify the actual link. This passes a
* linkStart and linkEnd parameter to the string. New lines (\n) in the string
* are preserved by converting them into
elements.
*
* @param {string} stringId - ID of the fluent string.
* @param {string} url - The URL to embed as link.
* @returns {string} The translated string.
*/
async function formatTranslationWithLink(stringId, url) {
const formattedString = await lazy.l10n.formatValue(stringId, {
linkStart: ``,
linkEnd: "",
});
return formattedString.replaceAll("\n", "
");
}
/**
* Convert an image into a data: URL to embed.
*
* @param {string} url - The URL of the image.
* @returns {string} data: URL representation of the string.
*/
async function getImage(url) {
const response = await fetch(url);
const blob = await response.blob();
const dataURL = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
return dataURL;
}
/**
* Get the contents of a stylesheet.
*
* @param {string} url - URL of the stylesheet.
* @returns {string} Raw CSS text of the stylehseet.
*/
async function getTextContent(url) {
const response = await fetch(url);
const content = await response.text();
return content;
}
/**
* Get the SVG element from an SVG file.
*
* @param {string} url
* @returns {SVGElement}
*/
async function getSVG(url) {
const source = await getTextContent(url);
const parser = new DOMParser();
const svgDocument = parser.parseFromString(source, "image/svg+xml");
return svgDocument.documentElement;
}
/**
* Uses the oauthResult.html template to generate a page with the given content.
* Also adds the brand SVGs, a bunch of metadata and footer content that isn't
* contained in the template.
*
* @param {string} title - The title of the page.
* @param {string} subtitle - The subtitle of the page.
* @param {string} body - The body of the page. If this contains raw HTML, pass
* rawBody as true.
* @param {boolean} [rawBody=false] - Whether the body parameter is expected to
* contain raw HTML.
* @returns {string} HTML markup for the entire page.
*/
async function getBasePage(title, subtitle, body, rawBody = false) {
const [wordmarkAlt, footerContent] = await lazy.l10n.formatValues([
{ id: "oauth-result-wordmark-alt" },
{ id: "oauth-result-footer-text" },
]);
const parser = new DOMParser();
const doc = parser.parseFromString(await lazy.htmlTemplate, "text/html");
doc.title = title;
doc.dir = Services.locale.isAppLocaleRTL ? "rtl" : "ltr";
doc.documentElement.lang = Services.locale.appLocaleAsBCP47;
const favicon = doc.createElement("link");
favicon.rel = "icon";
favicon.setAttribute("sizes", "32");
favicon.type = "image/png";
favicon.href = await lazy.favicon;
doc.head.append(favicon);
// These SVGs have to be inserted here, inlining at build time would mean
// knowing branding-specific things during pre-processing.
const brandSvg = (await lazy.brandLogo).cloneNode(true);
brandSvg.role = "image";
doc.getElementById("brandLogo").append(brandSvg);
const wordmarkSvg = (await lazy.brandWordmark).cloneNode(true);
wordmarkSvg.role = "image";
wordmarkSvg.ariaLabel = wordmarkAlt;
doc.getElementById("wordmark").append(wordmarkSvg);
doc.getElementById("title").textContent = title;
doc.getElementById("subtitle").textContent = subtitle;
if (!rawBody) {
doc.getElementById("body").textContent = body;
} else {
// This is safe, since body is a value we control in the caller, and should
// typically be a fluent string.
// eslint-disable-next-line no-unsanitized/property
doc.getElementById("body").innerHTML = body;
}
doc.getElementById("footerLink").textContent = footerContent;
const serializer = new XMLSerializer();
return serializer.serializeToString(doc);
}
export const OAuth2PageGenerator = {
/**
* Generate HTML for a success page after OAuth. The result shouldn't be
* cached, in case localization details change between calls.
*
* @returns {string} HTML for a self-contained success page.
*/
async generateSuccessPage() {
const [title, subtitle, body] = await lazy.l10n.formatValues([
{ id: "oauth-success-title" },
{ id: "oauth-success-subtitle" },
{ id: "oauth-success-body" },
]);
return getBasePage(title, subtitle, body);
},
/**
* Generate HTML for an error page after OAuth. The result shouldn't be
* cached, in case localization details change between calls.
*
* @returns {string} HTML for a self-contained error page.
*/
async generateErrorPage() {
const [title, subtitle] = await lazy.l10n.formatValues([
{ id: "oauth-error-title" },
{ id: "oauth-error-subtitle" },
]);
const body = await formatTranslationWithLink(
"oauth-error-body",
"https://support.thunderbird.net/kb/tb-oauth"
);
return getBasePage(title, subtitle, body, true);
},
};