summaryrefslogtreecommitdiffstats
path: root/browser/extensions/screenshots/background/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/screenshots/background/main.js')
-rw-r--r--browser/extensions/screenshots/background/main.js291
1 files changed, 291 insertions, 0 deletions
diff --git a/browser/extensions/screenshots/background/main.js b/browser/extensions/screenshots/background/main.js
new file mode 100644
index 0000000000..25f1cd6979
--- /dev/null
+++ b/browser/extensions/screenshots/background/main.js
@@ -0,0 +1,291 @@
+/* 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 selectorLoader, analytics, communication, catcher, log, makeUuid, auth, senderror, startBackground, blobConverters buildSettings */
+
+"use strict";
+
+this.main = (function() {
+ const exports = {};
+
+ const pasteSymbol = (window.navigator.platform.match(/Mac/i)) ? "\u2318" : "Ctrl";
+ const { sendEvent, incrementCount } = analytics;
+
+ const manifest = browser.runtime.getManifest();
+ let backend;
+
+ exports.hasAnyShots = function() {
+ return false;
+ };
+
+ 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 setIconActive(active, tabId) {
+ const path = active ? "icons/icon-highlight-32-v2.svg" : "icons/icon-v2.svg";
+ browser.pageAction.setIcon({tabId, path});
+ }
+
+ function toggleSelector(tab) {
+ return analytics.refreshTelemetryPref()
+ .then(() => selectorLoader.toggle(tab.id))
+ .then(active => {
+ setIconActive(active, tab.id);
+ return active;
+ })
+ .catch((error) => {
+ if (error.message && /Missing host permission for the tab/.test(error.message)) {
+ error.noReport = true;
+ }
+ error.popupMessage = "UNSHOOTABLE_PAGE";
+ throw error;
+ });
+ }
+
+ function shouldOpenMyShots(url) {
+ return /^about:(?:newtab|blank|home)/i.test(url) || /^resource:\/\/activity-streams\//i.test(url);
+ }
+
+ // 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((info, tab) => {
+ _startShotFlow(tab, "context-menu");
+ });
+
+ exports.onCommand = catcher.watchFunction((tab) => {
+ _startShotFlow(tab, "keyboard-shortcut");
+ });
+
+ const _openMyShots = (tab, inputType) => {
+ catcher.watchPromise(analytics.refreshTelemetryPref().then(() => {
+ sendEvent("goto-myshots", inputType, {incognito: tab.incognito});
+ }));
+ catcher.watchPromise(
+ auth.maybeLogin()
+ .then(() => browser.tabs.update({url: backend + "/shots"})));
+ };
+
+ const _startShotFlow = (tab, inputType) => {
+ if (!tab) {
+ // Not in a page/tab context, ignore
+ return;
+ }
+ if (!urlEnabled(tab.url)) {
+ senderror.showError({
+ popupMessage: "UNSHOOTABLE_PAGE",
+ });
+ return;
+ } else if (shouldOpenMyShots(tab.url)) {
+ _openMyShots(tab, inputType);
+ return;
+ }
+
+ catcher.watchPromise(toggleSelector(tab)
+ .then(active => {
+ let event = "start-shot";
+ if (inputType !== "context-menu") {
+ event = active ? "start-shot" : "cancel-shot";
+ }
+ sendEvent(event, inputType, {incognito: tab.incognito});
+ }).catch((error) => {
+ throw error;
+ }));
+ };
+
+ function urlEnabled(url) {
+ if (shouldOpenMyShots(url)) {
+ return true;
+ }
+ // 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("sendEvent", (sender, ...args) => {
+ catcher.watchPromise(sendEvent(...args));
+ // We don't wait for it to complete:
+ return null;
+ });
+
+ communication.register("openMyShots", (sender) => {
+ return catcher.watchPromise(
+ auth.maybeLogin()
+ .then(() => browser.tabs.create({url: backend + "/shots"})));
+ });
+
+ communication.register("openShot", async (sender, {url, copied}) => {
+ if (copied) {
+ const id = makeUuid();
+ 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: "../icons/copied-notification.svg",
+ title,
+ message,
+ });
+ }
+ return null;
+ });
+
+ // This is used for truncated full page downloads and copy to clipboards.
+ // Those longer operations need to display an animated spinner/loader, so
+ // it's preferable to perform toDataURL() in the background.
+ communication.register("canvasToDataURL", (sender, imageData) => {
+ const canvas = document.createElement("canvas");
+ canvas.width = imageData.width;
+ canvas.height = imageData.height;
+ canvas.getContext("2d").putImageData(imageData, 0, 0);
+ let dataUrl = canvas.toDataURL();
+ if (buildSettings.pngToJpegCutoff && dataUrl.length > buildSettings.pngToJpegCutoff) {
+ const jpegDataUrl = canvas.toDataURL("image/jpeg");
+ if (jpegDataUrl.length < dataUrl.length) {
+ // Only use the JPEG if it is actually smaller
+ dataUrl = jpegDataUrl;
+ }
+ }
+ return dataUrl;
+ });
+
+ 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: "../icons/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("closeSelector", (sender) => {
+ setIconActive(false, sender.tab.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;
+})();