summaryrefslogtreecommitdiffstats
path: root/browser/extensions/screenshots/background
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/screenshots/background')
-rw-r--r--browser/extensions/screenshots/background/analytics.js55
-rw-r--r--browser/extensions/screenshots/background/communication.js69
-rw-r--r--browser/extensions/screenshots/background/deviceInfo.js39
-rw-r--r--browser/extensions/screenshots/background/main.js238
-rw-r--r--browser/extensions/screenshots/background/selectorLoader.js139
-rw-r--r--browser/extensions/screenshots/background/senderror.js144
-rw-r--r--browser/extensions/screenshots/background/startBackground.js123
-rw-r--r--browser/extensions/screenshots/background/takeshot.js85
8 files changed, 892 insertions, 0 deletions
diff --git a/browser/extensions/screenshots/background/analytics.js b/browser/extensions/screenshots/background/analytics.js
new file mode 100644
index 0000000000..20f2c99ffd
--- /dev/null
+++ b/browser/extensions/screenshots/background/analytics.js
@@ -0,0 +1,55 @@
+/* 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/. */
+
+/* globals main, browser, catcher, log */
+
+"use strict";
+
+this.analytics = (function () {
+ const exports = {};
+
+ let telemetryEnabled;
+
+ exports.incrementCount = function (scalar) {
+ const allowedScalars = [
+ "download",
+ "upload",
+ "copy",
+ "visible",
+ "full_page",
+ "custom",
+ "element",
+ ];
+ if (!allowedScalars.includes(scalar)) {
+ const err = `incrementCount passed an unrecognized scalar ${scalar}`;
+ log.warn(err);
+ return Promise.resolve();
+ }
+ return browser.telemetry
+ .scalarAdd(`screenshots.${scalar}`, 1)
+ .catch(err => {
+ log.warn(`incrementCount failed with error: ${err}`);
+ });
+ };
+
+ exports.refreshTelemetryPref = function () {
+ return browser.telemetry.canUpload().then(
+ result => {
+ telemetryEnabled = result;
+ },
+ error => {
+ // If there's an error reading the pref, we should assume that we shouldn't send data
+ telemetryEnabled = false;
+ throw error;
+ }
+ );
+ };
+
+ exports.isTelemetryEnabled = function () {
+ catcher.watchPromise(exports.refreshTelemetryPref());
+ return telemetryEnabled;
+ };
+
+ return exports;
+})();
diff --git a/browser/extensions/screenshots/background/communication.js b/browser/extensions/screenshots/background/communication.js
new file mode 100644
index 0000000000..275b8ef7f8
--- /dev/null
+++ b/browser/extensions/screenshots/background/communication.js
@@ -0,0 +1,69 @@
+/* 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/. */
+
+/* globals catcher, log */
+
+"use strict";
+
+this.communication = (function () {
+ const exports = {};
+
+ const registeredFunctions = {};
+
+ exports.onMessage = catcher.watchFunction((req, sender, sendResponse) => {
+ if (!(req.funcName in registeredFunctions)) {
+ log.error(`Received unknown internal message type ${req.funcName}`);
+ sendResponse({ type: "error", name: "Unknown message type" });
+ return;
+ }
+ if (!Array.isArray(req.args)) {
+ log.error("Received message with no .args list");
+ sendResponse({ type: "error", name: "No .args" });
+ return;
+ }
+ const func = registeredFunctions[req.funcName];
+ let result;
+ try {
+ req.args.unshift(sender);
+ result = func.apply(null, req.args);
+ } catch (e) {
+ log.error(`Error in ${req.funcName}:`, e, e.stack);
+ // FIXME: should consider using makeError from catcher here:
+ sendResponse({
+ type: "error",
+ message: e + "",
+ errorCode: e.errorCode,
+ popupMessage: e.popupMessage,
+ });
+ return;
+ }
+ if (result && result.then) {
+ result
+ .then(concreteResult => {
+ sendResponse({ type: "success", value: concreteResult });
+ })
+ .catch(errorResult => {
+ log.error(
+ `Promise error in ${req.funcName}:`,
+ errorResult,
+ errorResult && errorResult.stack
+ );
+ sendResponse({
+ type: "error",
+ message: errorResult + "",
+ errorCode: errorResult.errorCode,
+ popupMessage: errorResult.popupMessage,
+ });
+ });
+ return;
+ }
+ sendResponse({ type: "success", value: result });
+ });
+
+ exports.register = function (name, func) {
+ registeredFunctions[name] = func;
+ };
+
+ return exports;
+})();
diff --git a/browser/extensions/screenshots/background/deviceInfo.js b/browser/extensions/screenshots/background/deviceInfo.js
new file mode 100644
index 0000000000..2a4b15f5ff
--- /dev/null
+++ b/browser/extensions/screenshots/background/deviceInfo.js
@@ -0,0 +1,39 @@
+/* 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/. */
+
+/* globals catcher, browser, navigator */
+
+"use strict";
+
+this.deviceInfo = (function () {
+ const manifest = browser.runtime.getManifest();
+
+ let platformInfo = {};
+ catcher.watchPromise(
+ browser.runtime.getPlatformInfo().then(info => {
+ platformInfo = info;
+ })
+ );
+
+ return function deviceInfo() {
+ let match = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9.]{1,1000})/);
+ const chromeVersion = match ? match[1] : null;
+ match = navigator.userAgent.match(/Firefox\/([0-9.]{1,1000})/);
+ const firefoxVersion = match ? match[1] : null;
+ const appName = chromeVersion ? "chrome" : "firefox";
+
+ return {
+ addonVersion: manifest.version,
+ platform: platformInfo.os,
+ architecture: platformInfo.arch,
+ version: firefoxVersion || chromeVersion,
+ // These don't seem to apply to Chrome:
+ // build: system.build,
+ // platformVersion: system.platformVersion,
+ userAgent: navigator.userAgent,
+ appVendor: appName,
+ appName,
+ };
+ };
+})();
diff --git a/browser/extensions/screenshots/background/main.js b/browser/extensions/screenshots/background/main.js
new file mode 100644
index 0000000000..594ba798ee
--- /dev/null
+++ b/browser/extensions/screenshots/background/main.js
@@ -0,0 +1,238 @@
+/* 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/. */
+
+/* globals browser, getStrings, selectorLoader, analytics, communication, catcher, log, senderror, startBackground, blobConverters, startSelectionWithOnboarding */
+
+"use strict";
+
+this.main = (function () {
+ const exports = {};
+
+ const { incrementCount } = analytics;
+
+ const manifest = browser.runtime.getManifest();
+ let backend;
+
+ exports.setBackend = function (newBackend) {
+ backend = newBackend;
+ backend = backend.replace(/\/*$/, "");
+ };
+
+ exports.getBackend = function () {
+ return backend;
+ };
+
+ communication.register("getBackend", () => {
+ return backend;
+ });
+
+ for (const permission of manifest.permissions) {
+ if (/^https?:\/\//.test(permission)) {
+ exports.setBackend(permission);
+ break;
+ }
+ }
+
+ function toggleSelector(tab) {
+ return analytics
+ .refreshTelemetryPref()
+ .then(() => selectorLoader.toggle(tab.id))
+ .catch(error => {
+ if (
+ error.message &&
+ /Missing host permission for the tab/.test(error.message)
+ ) {
+ error.noReport = true;
+ }
+ error.popupMessage = "UNSHOOTABLE_PAGE";
+ throw error;
+ });
+ }
+
+ // This is called by startBackground.js, where is registered as a click
+ // handler for the webextension page action.
+ exports.onClicked = catcher.watchFunction(tab => {
+ _startShotFlow(tab, "toolbar-button");
+ });
+
+ exports.onClickedContextMenu = catcher.watchFunction(tab => {
+ _startShotFlow(tab, "context-menu");
+ });
+
+ exports.onShortcut = catcher.watchFunction(tab => {
+ _startShotFlow(tab, "keyboard-shortcut");
+ });
+
+ const _startShotFlow = (tab, inputType) => {
+ if (!tab) {
+ // Not in a page/tab context, ignore
+ return;
+ }
+ if (!urlEnabled(tab.url)) {
+ senderror.showError({
+ popupMessage: "UNSHOOTABLE_PAGE",
+ });
+ return;
+ }
+
+ catcher.watchPromise(
+ toggleSelector(tab).catch(error => {
+ throw error;
+ })
+ );
+ };
+
+ function urlEnabled(url) {
+ // Allow screenshots on urls related to web pages in reader mode.
+ if (url && url.startsWith("about:reader?url=")) {
+ return true;
+ }
+ if (
+ isShotOrMyShotPage(url) ||
+ /^(?:about|data|moz-extension):/i.test(url) ||
+ isBlacklistedUrl(url)
+ ) {
+ return false;
+ }
+ return true;
+ }
+
+ function isShotOrMyShotPage(url) {
+ // It's okay to take a shot of any pages except shot pages and My Shots
+ if (!url.startsWith(backend)) {
+ return false;
+ }
+ const path = url
+ .substr(backend.length)
+ .replace(/^\/*/, "")
+ .replace(/[?#].*/, "");
+ if (path === "shots") {
+ return true;
+ }
+ if (/^[^/]{1,4000}\/[^/]{1,4000}$/.test(path)) {
+ // Blocks {:id}/{:domain}, but not /, /privacy, etc
+ return true;
+ }
+ return false;
+ }
+
+ function isBlacklistedUrl(url) {
+ // These specific domains are not allowed for general WebExtension permission reasons
+ // Discussion: https://bugzilla.mozilla.org/show_bug.cgi?id=1310082
+ // List of domains copied from: https://searchfox.org/mozilla-central/source/browser/app/permissions#18-19
+ // Note we disable it here to be informative, the security check is done in WebExtension code
+ const badDomains = ["testpilot.firefox.com"];
+ let domain = url.replace(/^https?:\/\//i, "");
+ domain = domain.replace(/\/.*/, "").replace(/:.*/, "");
+ domain = domain.toLowerCase();
+ return badDomains.includes(domain);
+ }
+
+ communication.register("getStrings", (sender, ids) => {
+ return getStrings(ids.map(id => ({ id })));
+ });
+
+ communication.register("captureTelemetry", (sender, ...args) => {
+ catcher.watchPromise(incrementCount(...args));
+ });
+
+ communication.register("openShot", async (sender, { url, copied }) => {
+ if (copied) {
+ const id = crypto.randomUUID();
+ const [title, message] = await getStrings([
+ { id: "screenshots-notification-link-copied-title" },
+ { id: "screenshots-notification-link-copied-details" },
+ ]);
+ return browser.notifications.create(id, {
+ type: "basic",
+ iconUrl: "chrome://browser/content/screenshots/copied-notification.svg",
+ title,
+ message,
+ });
+ }
+ return null;
+ });
+
+ communication.register("copyShotToClipboard", async (sender, blob) => {
+ let buffer = await blobConverters.blobToArray(blob);
+ await browser.clipboard.setImageData(buffer, blob.type.split("/", 2)[1]);
+
+ const [title, message] = await getStrings([
+ { id: "screenshots-notification-image-copied-title" },
+ { id: "screenshots-notification-image-copied-details" },
+ ]);
+
+ catcher.watchPromise(incrementCount("copy"));
+ return browser.notifications.create({
+ type: "basic",
+ iconUrl: "chrome://browser/content/screenshots/copied-notification.svg",
+ title,
+ message,
+ });
+ });
+
+ communication.register("downloadShot", (sender, info) => {
+ // 'data:' urls don't work directly, let's use a Blob
+ // see http://stackoverflow.com/questions/40269862/save-data-uri-as-file-using-downloads-download-api
+ const blob = blobConverters.dataUrlToBlob(info.url);
+ const url = URL.createObjectURL(blob);
+ let downloadId;
+ const onChangedCallback = catcher.watchFunction(function (change) {
+ if (!downloadId || downloadId !== change.id) {
+ return;
+ }
+ if (change.state && change.state.current !== "in_progress") {
+ URL.revokeObjectURL(url);
+ browser.downloads.onChanged.removeListener(onChangedCallback);
+ }
+ });
+ browser.downloads.onChanged.addListener(onChangedCallback);
+ catcher.watchPromise(incrementCount("download"));
+ return browser.windows.getLastFocused().then(windowInfo => {
+ return browser.downloads
+ .download({
+ url,
+ incognito: windowInfo.incognito,
+ filename: info.filename,
+ })
+ .catch(error => {
+ // We are not logging error message when user cancels download
+ if (error && error.message && !error.message.includes("canceled")) {
+ log.error(error.message);
+ }
+ })
+ .then(id => {
+ downloadId = id;
+ });
+ });
+ });
+
+ communication.register("abortStartShot", () => {
+ // Note, we only show the error but don't report it, as we know that we can't
+ // take shots of these pages:
+ senderror.showError({
+ popupMessage: "UNSHOOTABLE_PAGE",
+ });
+ });
+
+ // A Screenshots page wants us to start/force onboarding
+ communication.register("requestOnboarding", sender => {
+ return startSelectionWithOnboarding(sender.tab);
+ });
+
+ communication.register("getPlatformOs", () => {
+ return catcher.watchPromise(
+ browser.runtime.getPlatformInfo().then(platformInfo => {
+ return platformInfo.os;
+ })
+ );
+ });
+
+ // This allows the web site show notifications through sitehelper.js
+ communication.register("showNotification", (sender, notification) => {
+ return browser.notifications.create(notification);
+ });
+
+ return exports;
+})();
diff --git a/browser/extensions/screenshots/background/selectorLoader.js b/browser/extensions/screenshots/background/selectorLoader.js
new file mode 100644
index 0000000000..4749f73b30
--- /dev/null
+++ b/browser/extensions/screenshots/background/selectorLoader.js
@@ -0,0 +1,139 @@
+/* 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/. */
+
+/* globals browser, catcher, communication, log, main */
+
+"use strict";
+
+// eslint-disable-next-line no-var
+var global = this;
+
+this.selectorLoader = (function () {
+ const exports = {};
+
+ // These modules are loaded in order, first standardScripts and then selectorScripts
+ // The order is important due to dependencies
+ const standardScripts = [
+ "log.js",
+ "catcher.js",
+ "assertIsTrusted.js",
+ "assertIsBlankDocument.js",
+ "blobConverters.js",
+ "background/selectorLoader.js",
+ "selector/callBackground.js",
+ "selector/util.js",
+ ];
+
+ const selectorScripts = [
+ "clipboard.js",
+ "build/selection.js",
+ "build/shot.js",
+ "randomString.js",
+ "domainFromUrl.js",
+ "build/inlineSelectionCss.js",
+ "selector/documentMetadata.js",
+ "selector/ui.js",
+ "selector/shooter.js",
+ "selector/uicontrol.js",
+ ];
+
+ exports.unloadIfLoaded = function (tabId) {
+ return browser.tabs
+ .executeScript(tabId, {
+ code: "this.selectorLoader && this.selectorLoader.unloadModules()",
+ runAt: "document_start",
+ })
+ .then(result => {
+ return result && result[0];
+ });
+ };
+
+ exports.testIfLoaded = function (tabId) {
+ if (loadingTabs.has(tabId)) {
+ return true;
+ }
+ return browser.tabs
+ .executeScript(tabId, {
+ code: "!!this.selectorLoader",
+ runAt: "document_start",
+ })
+ .then(result => {
+ return result && result[0];
+ });
+ };
+
+ const loadingTabs = new Set();
+
+ exports.loadModules = function (tabId) {
+ loadingTabs.add(tabId);
+ catcher.watchPromise(
+ executeModules(tabId, standardScripts.concat(selectorScripts)).then(
+ () => {
+ loadingTabs.delete(tabId);
+ }
+ )
+ );
+ };
+
+ function executeModules(tabId, scripts) {
+ let lastPromise = Promise.resolve(null);
+ scripts.forEach(file => {
+ lastPromise = lastPromise.then(() => {
+ return browser.tabs
+ .executeScript(tabId, {
+ file,
+ runAt: "document_start",
+ })
+ .catch(error => {
+ log.error("error in script:", file, error);
+ error.scriptName = file;
+ throw error;
+ });
+ });
+ });
+ return lastPromise.then(
+ () => {
+ log.debug("finished loading scripts:", scripts.join(" "));
+ },
+ error => {
+ exports.unloadIfLoaded(tabId);
+ catcher.unhandled(error);
+ throw error;
+ }
+ );
+ }
+
+ exports.unloadModules = function () {
+ const watchFunction = catcher.watchFunction;
+ const allScripts = standardScripts.concat(selectorScripts);
+ const moduleNames = allScripts.map(filename =>
+ filename.replace(/^.*\//, "").replace(/\.js$/, "")
+ );
+ moduleNames.reverse();
+ for (const moduleName of moduleNames) {
+ const moduleObj = global[moduleName];
+ if (moduleObj && moduleObj.unload) {
+ try {
+ watchFunction(moduleObj.unload)();
+ } catch (e) {
+ // ignore (watchFunction handles it)
+ }
+ }
+ delete global[moduleName];
+ }
+ return true;
+ };
+
+ exports.toggle = function (tabId) {
+ return exports.unloadIfLoaded(tabId).then(wasLoaded => {
+ if (!wasLoaded) {
+ exports.loadModules(tabId);
+ }
+ return !wasLoaded;
+ });
+ };
+
+ return exports;
+})();
+null;
diff --git a/browser/extensions/screenshots/background/senderror.js b/browser/extensions/screenshots/background/senderror.js
new file mode 100644
index 0000000000..3d5eae5ec6
--- /dev/null
+++ b/browser/extensions/screenshots/background/senderror.js
@@ -0,0 +1,144 @@
+/* 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/. */
+
+/* globals startBackground, analytics, communication, catcher, log, browser, getStrings */
+
+"use strict";
+
+this.senderror = (function () {
+ const exports = {};
+
+ // Do not show an error more than every ERROR_TIME_LIMIT milliseconds:
+ const ERROR_TIME_LIMIT = 3000;
+
+ const messages = {
+ REQUEST_ERROR: {
+ titleKey: "screenshots-request-error-title",
+ infoKey: "screenshots-request-error-details",
+ },
+ CONNECTION_ERROR: {
+ titleKey: "screenshots-connection-error-title",
+ infoKey: "screenshots-connection-error-details",
+ },
+ LOGIN_ERROR: {
+ titleKey: "screenshots-request-error-title",
+ infoKey: "screenshots-login-error-details",
+ },
+ LOGIN_CONNECTION_ERROR: {
+ titleKey: "screenshots-connection-error-title",
+ infoKey: "screenshots-connection-error-details",
+ },
+ UNSHOOTABLE_PAGE: {
+ titleKey: "screenshots-unshootable-page-error-title",
+ infoKey: "screenshots-unshootable-page-error-details",
+ },
+ EMPTY_SELECTION: {
+ titleKey: "screenshots-empty-selection-error-title",
+ },
+ PRIVATE_WINDOW: {
+ titleKey: "screenshots-private-window-error-title",
+ infoKey: "screenshots-private-window-error-details",
+ },
+ generic: {
+ titleKey: "screenshots-generic-error-title",
+ infoKey: "screenshots-generic-error-details",
+ showMessage: true,
+ },
+ };
+
+ communication.register("reportError", (sender, error) => {
+ catcher.unhandled(error);
+ });
+
+ let lastErrorTime;
+
+ exports.showError = async function (error) {
+ if (lastErrorTime && Date.now() - lastErrorTime < ERROR_TIME_LIMIT) {
+ return;
+ }
+ lastErrorTime = Date.now();
+ const id = crypto.randomUUID();
+ let popupMessage = error.popupMessage || "generic";
+ if (!messages[popupMessage]) {
+ popupMessage = "generic";
+ }
+
+ let item = messages[popupMessage];
+ if (!("title" in item)) {
+ let keys = [{ id: item.titleKey }];
+ if ("infoKey" in item) {
+ keys.push({ id: item.infoKey });
+ }
+
+ [item.title, item.info] = await getStrings(keys);
+ }
+
+ let title = item.title;
+ let message = item.info || "";
+ const showMessage = item.showMessage;
+ if (error.message && showMessage) {
+ if (message) {
+ message += "\n" + error.message;
+ } else {
+ message = error.message;
+ }
+ }
+ if (Date.now() - startBackground.startTime > 5 * 1000) {
+ browser.notifications.create(id, {
+ type: "basic",
+ // FIXME: need iconUrl for an image, see #2239
+ title,
+ message,
+ });
+ }
+ };
+
+ exports.reportError = function (e) {
+ if (!analytics.isTelemetryEnabled()) {
+ log.error("Telemetry disabled. Not sending critical error:", e);
+ return;
+ }
+ const exception = new Error(e.message);
+ exception.stack = e.multilineStack || e.stack || undefined;
+
+ // To improve Sentry reporting & grouping, replace the
+ // moz-extension://$uuid base URL with a generic resource:// URL.
+ if (exception.stack) {
+ exception.stack = exception.stack.replace(
+ /moz-extension:\/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g,
+ "resource://screenshots-addon"
+ );
+ }
+ const rest = {};
+ for (const attr in e) {
+ if (
+ ![
+ "name",
+ "message",
+ "stack",
+ "multilineStack",
+ "popupMessage",
+ "version",
+ "sentryPublicDSN",
+ "help",
+ "fromMakeError",
+ ].includes(attr)
+ ) {
+ rest[attr] = e[attr];
+ }
+ }
+ rest.stack = exception.stack;
+ };
+
+ catcher.registerHandler(errorObj => {
+ if (!errorObj.noPopup) {
+ exports.showError(errorObj);
+ }
+ if (!errorObj.noReport) {
+ exports.reportError(errorObj);
+ }
+ });
+
+ return exports;
+})();
diff --git a/browser/extensions/screenshots/background/startBackground.js b/browser/extensions/screenshots/background/startBackground.js
new file mode 100644
index 0000000000..5d35db3638
--- /dev/null
+++ b/browser/extensions/screenshots/background/startBackground.js
@@ -0,0 +1,123 @@
+/* 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/. */
+
+/* globals browser, main, communication, manifest */
+
+/* This file handles:
+ clicks on the WebExtension page action
+ browser.contextMenus.onClicked
+ browser.runtime.onMessage
+ and loads the rest of the background page in response to those events, forwarding
+ the events to main.onClicked, main.onClickedContextMenu, or communication.onMessage
+*/
+
+const startTime = Date.now();
+
+// Set up to be able to use fluent:
+(function () {
+ let link = document.createElement("link");
+ link.setAttribute("rel", "localization");
+ link.setAttribute("href", "browser/screenshots.ftl");
+ document.head.appendChild(link);
+
+ link = document.createElement("link");
+ link.setAttribute("rel", "localization");
+ link.setAttribute("href", "toolkit/branding/brandings.ftl");
+ document.head.appendChild(link);
+})();
+
+this.getStrings = async function (ids) {
+ if (document.readyState != "complete") {
+ await new Promise(resolve =>
+ window.addEventListener("load", resolve, { once: true })
+ );
+ }
+ await document.l10n.ready;
+ return document.l10n.formatValues(ids);
+};
+
+let zoomFactor = 1;
+this.getZoomFactor = function () {
+ return zoomFactor;
+};
+
+this.startBackground = (function () {
+ const exports = { startTime };
+
+ const backgroundScripts = [
+ "log.js",
+ "catcher.js",
+ "blobConverters.js",
+ "background/selectorLoader.js",
+ "background/communication.js",
+ "background/senderror.js",
+ "build/shot.js",
+ "build/thumbnailGenerator.js",
+ "background/analytics.js",
+ "background/deviceInfo.js",
+ "background/takeshot.js",
+ "background/main.js",
+ ];
+
+ browser.experiments.screenshots.onScreenshotCommand.addListener(
+ async type => {
+ try {
+ let [[tab]] = await Promise.all([
+ browser.tabs.query({ currentWindow: true, active: true }),
+ loadIfNecessary(),
+ ]);
+ zoomFactor = await browser.tabs.getZoom(tab.id);
+ if (type === "contextMenu") {
+ main.onClickedContextMenu(tab);
+ } else if (type === "toolbar" || type === "quickaction") {
+ main.onClicked(tab);
+ } else if (type === "shortcut") {
+ main.onShortcut(tab);
+ }
+ } catch (error) {
+ console.error("Error loading Screenshots:", error);
+ }
+ }
+ );
+
+ browser.runtime.onMessage.addListener((req, sender, sendResponse) => {
+ loadIfNecessary()
+ .then(() => {
+ return communication.onMessage(req, sender, sendResponse);
+ })
+ .catch(error => {
+ console.error("Error loading Screenshots:", error);
+ });
+ return true;
+ });
+
+ let loadedPromise;
+
+ function loadIfNecessary() {
+ if (loadedPromise) {
+ return loadedPromise;
+ }
+ loadedPromise = Promise.resolve();
+ backgroundScripts.forEach(script => {
+ loadedPromise = loadedPromise.then(() => {
+ return new Promise((resolve, reject) => {
+ const tag = document.createElement("script");
+ tag.src = browser.runtime.getURL(script);
+ tag.onload = () => {
+ resolve();
+ };
+ tag.onerror = error => {
+ const exc = new Error(`Error loading script: ${error.message}`);
+ exc.scriptName = script;
+ reject(exc);
+ };
+ document.head.appendChild(tag);
+ });
+ });
+ });
+ return loadedPromise;
+ }
+
+ return exports;
+})();
diff --git a/browser/extensions/screenshots/background/takeshot.js b/browser/extensions/screenshots/background/takeshot.js
new file mode 100644
index 0000000000..c8e229247b
--- /dev/null
+++ b/browser/extensions/screenshots/background/takeshot.js
@@ -0,0 +1,85 @@
+/* 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/. */
+
+/* globals browser, communication, getZoomFactor, shot, main, catcher, analytics, blobConverters, thumbnailGenerator */
+
+"use strict";
+
+this.takeshot = (function () {
+ const exports = {};
+ const MAX_CANVAS_DIMENSION = 32767;
+
+ communication.register(
+ "screenshotPage",
+ (sender, selectedPos, screenshotType, devicePixelRatio) => {
+ return screenshotPage(selectedPos, screenshotType, devicePixelRatio);
+ }
+ );
+
+ communication.register("getZoomFactor", sender => {
+ return getZoomFactor();
+ });
+
+ function screenshotPage(pos, screenshotType, devicePixelRatio) {
+ pos.width = Math.min(pos.right - pos.left, MAX_CANVAS_DIMENSION);
+ pos.height = Math.min(pos.bottom - pos.top, MAX_CANVAS_DIMENSION);
+
+ // If we are printing the full page or a truncated full page,
+ // we must pass in this rectangle to preview the entire image
+ let options = { format: "png" };
+ if (
+ screenshotType === "fullPage" ||
+ screenshotType === "fullPageTruncated"
+ ) {
+ let rectangle = {
+ x: 0,
+ y: 0,
+ width: pos.width,
+ height: pos.height,
+ };
+ options.rect = rectangle;
+ options.resetScrollPosition = true;
+ } else if (screenshotType != "visible") {
+ let rectangle = {
+ x: pos.left,
+ y: pos.top,
+ width: pos.width,
+ height: pos.height,
+ };
+ options.rect = rectangle;
+ }
+
+ return catcher.watchPromise(
+ browser.tabs.captureTab(null, options).then(dataUrl => {
+ const image = new Image();
+ image.src = dataUrl;
+ return new Promise((resolve, reject) => {
+ image.onload = catcher.watchFunction(() => {
+ const xScale = devicePixelRatio;
+ const yScale = devicePixelRatio;
+ const canvas = document.createElement("canvas");
+ canvas.height = pos.height * yScale;
+ canvas.width = pos.width * xScale;
+ const context = canvas.getContext("2d");
+ context.drawImage(
+ image,
+ 0,
+ 0,
+ pos.width * xScale,
+ pos.height * yScale,
+ 0,
+ 0,
+ pos.width * xScale,
+ pos.height * yScale
+ );
+ const result = canvas.toDataURL();
+ resolve(result);
+ });
+ });
+ })
+ );
+ }
+
+ return exports;
+})();