diff options
Diffstat (limited to 'browser/extensions/screenshots/background')
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; +})(); |