Source code

Revision control

Copy as Markdown

Other Tools

/* 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 => {
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
// 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, { 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",
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",
title,
message,
});
});
communication.register("downloadShot", (sender, info) => {
// 'data:' urls don't work directly, let's use a Blob
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;
})();