/* 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";
const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PrivateBrowsingUtils:
"resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetter(
lazy,
"IDNService",
"@mozilla.org/network/idn-service;1",
Ci.nsIIDNService
);
const QRCodeDialog = {
_url:
null,
_qrCodeDataURI:
null,
/** Initializes the dialog from window arguments. */
init() {
if (window.arguments && window.arguments[
0]) {
const params = window.arguments[
0];
this._url = params.url;
this._qrCodeDataURI = params.qrCodeDataURI;
}
document.mozSubdialogReady =
this.setupDialog();
const copyButton = document.getElementById(
"copy-button");
document.subDialogSetDefaultFocus = () => copyButton.focus();
copyButton.addEventListener(
"click", () =>
this.copyImage());
document
.getElementById(
"save-button")
.addEventListener(
"click", () =>
this.saveImage());
document
.getElementById(
"close-button")
.addEventListener(
"click", () => window.close());
document.addEventListener(
"keydown", event => {
if (
event.key ===
"Enter" &&
!event.defaultPrevented &&
!event.target.closest(
"moz-button")
) {
event.preventDefault();
this.copyImage();
}
});
},
/**
* Populates the dialog with the QR code image and URL, or shows an error
* if the QR code data URI is missing.
*/
async setupDialog() {
const successContainer = document.getElementById(
"success-container");
if (!
this._qrCodeDataURI) {
this.showFeedback(
"error",
"qrcode-panel-error");
return;
}
const imageElement = document.getElementById(
"qrcode-image");
imageElement.src =
this._qrCodeDataURI;
successContainer.hidden =
false;
const urlElement = document.getElementById(
"qrcode-url");
urlElement.textContent =
this._url;
urlElement.title =
this._url;
},
/**
* Shows a feedback message bar in the dialog and resizes the dialog to fit.
*
* @param {string} type - The message bar type ("success" or "error").
* @param {string} l10nId - The Fluent localization ID for the message.
*/
async showFeedback(type, l10nId) {
let bar = document.getElementById(
"feedback-bar");
if (!bar) {
bar = document.createElement(
"moz-message-bar");
bar.id =
"feedback-bar";
bar.setAttribute(
"role",
"alert");
bar.setAttribute(
"data-l10n-attrs",
"message");
bar.setAttribute(
"dismissable",
"");
bar.addEventListener(
"message-bar:user-dismissed", () => {
requestAnimationFrame(() => window.resizeDialog?.());
});
let content = document.getElementById(
"qrcode-dialog-content");
content.appendChild(bar);
}
bar.type = type;
document.l10n.setAttributes(bar, l10nId);
await bar.updateComplete;
window.resizeDialog?.();
},
/**
* Strips the data URI prefix from the stored QR code data URI, base64-decodes
* the payload, and returns the raw PNG bytes as a Uint8Array.
*
* @returns {Uint8Array} The decoded PNG image bytes.
* @throws {Error} If the data URI is missing or not a base64-encoded PNG.
*/
decodeDataURI() {
const dataPrefix =
"data:image/png;base64,";
if (!
this._qrCodeDataURI?.startsWith(dataPrefix)) {
throw new Error(
"Invalid QR code image data");
}
return Uint8Array.fromBase64(
this._qrCodeDataURI.slice(dataPrefix.length));
},
/** Copies the QR code image to the clipboard as a PNG. */
async copyImage() {
try {
const qrCodeBytes =
this.decodeDataURI();
const item =
new ClipboardItem({
"image/png":
new Blob([qrCodeBytes], { type:
"image/png" }),
});
await navigator.clipboard.write([item]);
this.showFeedback(
"success",
"qrcode-copy-success");
}
catch (error) {
console.error(
"Failed to copy QR code:", error);
this.showFeedback(
"error",
"qrcode-copy-error");
}
},
/** Saves the QR code image as a PNG file via the standard download flow. */
async saveImage() {
let domain =
"";
let uri;
try {
uri = Services.io.newURI(
this._url);
domain = lazy.IDNService.domainToDisplay(
Services.eTLD.getSchemelessSite(uri)
);
}
catch (e) {
if (uri) {
domain = uri.host;
}
}
const filenameMessage = domain
? { id:
"qrcode-save-filename-with-domain-base", args: { domain } }
:
"qrcode-save-filename-base";
const [defaultFilename] = await document.l10n.formatValues([
filenameMessage,
]);
// Append .png so the QR code for "firefox.com" defaults to
// "qrcode-firefox.com.png".
const filename = `${defaultFilename}.png`;
const chromeWindow = window.browsingContext.topChromeWindow;
chromeWindow.internalSave(
this._qrCodeDataURI,
null,
// originalURL
null,
// document
filename,
null,
// content disposition
"image/png",
true,
// bypass cache, since it's a data: URI
"SaveImageTitle",
null,
// chosen data
null,
// referrer info
null,
// cookie jar settings
null,
// initiating document
false,
// don't skip the prompt for where to save
null,
// cache key
lazy.PrivateBrowsingUtils.isWindowPrivate(chromeWindow),
Services.scriptSecurityManager.getSystemPrincipal()
);
},
};
// Exposed on `window` for browser-chrome tests.
window.QRCodeDialog = QRCodeDialog;
window.addEventListener(
"DOMContentLoaded", () => {
QRCodeDialog.init();
});