This is imported from https://github.com/mozilla-services/screenshots/. It has been reviewed as patches landed, but also reviewed by Mossop and kmag. This also includes the patch from bug 1356394 MozReview-Commit-ID: FXIVw7WjxlN
277 lines
8.6 KiB
JavaScript
277 lines
8.6 KiB
JavaScript
/* globals browser, console, XMLHttpRequest, Image, document, setTimeout, navigator */
|
|
/* globals selectorLoader, analytics, communication, catcher, makeUuid, auth, senderror */
|
|
|
|
"use strict";
|
|
|
|
this.main = (function () {
|
|
let exports = {};
|
|
|
|
const pasteSymbol = (window.navigator.platform.match(/Mac/i)) ? "\u2318" : "Ctrl";
|
|
const { sendEvent } = analytics;
|
|
|
|
let manifest = browser.runtime.getManifest();
|
|
let backend;
|
|
|
|
let hasSeenOnboarding;
|
|
|
|
browser.storage.local.get(["hasSeenOnboarding"]).then((result) => {
|
|
hasSeenOnboarding = !! result.hasSeenOnboarding;
|
|
if (! hasSeenOnboarding) {
|
|
setIconActive(false, null);
|
|
// Note that the branded name 'Firefox Screenshots' is not localized:
|
|
browser.browserAction.setTitle({
|
|
title: "Firefox Screenshots"
|
|
});
|
|
}
|
|
}).catch((error) => {
|
|
log.error("Error getting hasSeenOnboarding:", error);
|
|
});
|
|
|
|
exports.setBackend = function (newBackend) {
|
|
backend = newBackend;
|
|
backend = backend.replace(/\/*$/, "");
|
|
};
|
|
|
|
exports.getBackend = function () {
|
|
return backend;
|
|
};
|
|
|
|
communication.register("getBackend", () => {
|
|
return backend;
|
|
});
|
|
|
|
function getOnboardingUrl() {
|
|
return backend + "/#hello";
|
|
}
|
|
|
|
for (let permission of manifest.permissions) {
|
|
if (/^https?:\/\//.test(permission)) {
|
|
exports.setBackend(permission);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function setIconActive(active, tabId) {
|
|
let path = active ? "icons/icon-highlight-38.png" : "icons/icon-38.png";
|
|
if ((! hasSeenOnboarding) && ! active) {
|
|
path = "icons/icon-starred-38.png";
|
|
}
|
|
browser.browserAction.setIcon({path, tabId});
|
|
}
|
|
|
|
function toggleSelector(tab) {
|
|
return analytics.refreshTelemetryPref()
|
|
.then(() => selectorLoader.toggle(tab.id, hasSeenOnboarding))
|
|
.then(active => {
|
|
setIconActive(active, tab.id);
|
|
return active;
|
|
})
|
|
.catch((error) => {
|
|
error.popupMessage = "UNSHOOTABLE_PAGE";
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
function shouldOpenMyShots(url) {
|
|
return /^about:(?:newtab|blank)/i.test(url) || /^resource:\/\/activity-streams\//i.test(url);
|
|
}
|
|
|
|
browser.browserAction.onClicked.addListener(catcher.watchFunction((tab) => {
|
|
if (shouldOpenMyShots(tab.url)) {
|
|
if (! hasSeenOnboarding) {
|
|
catcher.watchPromise(analytics.refreshTelemetryPref().then(() => {
|
|
sendEvent("goto-onboarding", "selection-button");
|
|
return forceOnboarding();
|
|
}));
|
|
return;
|
|
}
|
|
catcher.watchPromise(analytics.refreshTelemetryPref().then(() => {
|
|
sendEvent("goto-myshots", "about-newtab");
|
|
}));
|
|
catcher.watchPromise(
|
|
auth.authHeaders()
|
|
.then(() => browser.tabs.update({url: backend + "/shots"})));
|
|
} else {
|
|
catcher.watchPromise(
|
|
toggleSelector(tab)
|
|
.then(active => {
|
|
const event = active ? "start-shot" : "cancel-shot";
|
|
sendEvent(event, "toolbar-button");
|
|
}, (error) => {
|
|
if ((! hasSeenOnboarding) && error.popupMessage == "UNSHOOTABLE_PAGE") {
|
|
sendEvent("goto-onboarding", "selection-button");
|
|
return forceOnboarding();
|
|
}
|
|
throw error;
|
|
}));
|
|
}
|
|
}));
|
|
|
|
function forceOnboarding() {
|
|
return browser.tabs.create({url: getOnboardingUrl()}).then((tab) => {
|
|
return toggleSelector(tab);
|
|
});
|
|
}
|
|
|
|
browser.contextMenus.create({
|
|
id: "create-screenshot",
|
|
title: browser.i18n.getMessage("contextMenuLabel"),
|
|
contexts: ["page"],
|
|
documentUrlPatterns: ["<all_urls>"]
|
|
}, () => {
|
|
// Note: unlike most browser.* functions this one does not return a promise
|
|
if (browser.runtime.lastError) {
|
|
catcher.unhandled(new Error(browser.runtime.lastError.message));
|
|
}
|
|
});
|
|
|
|
browser.contextMenus.onClicked.addListener(catcher.watchFunction((info, tab) => {
|
|
if (! tab) {
|
|
// Not in a page/tab context, ignore
|
|
return;
|
|
}
|
|
catcher.watchPromise(
|
|
toggleSelector(tab)
|
|
.then(() => sendEvent("start-shot", "context-menu")));
|
|
}));
|
|
|
|
function urlEnabled(url) {
|
|
if (shouldOpenMyShots(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;
|
|
}
|
|
let path = url.substr(backend.length).replace(/^\/*/, "").replace(/#.*/, "").replace(/\?.*/, "");
|
|
if (path == "shots") {
|
|
return true;
|
|
}
|
|
if (/^[^/]+\/[^/]+$/.test(url)) {
|
|
// 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://dxr.mozilla.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 = ["addons.mozilla.org", "testpilot.firefox.com"];
|
|
let domain = url.replace(/^https?:\/\//i, "");
|
|
domain = domain.replace(/\/.*/, "").replace(/:.*/, "");
|
|
domain = domain.toLowerCase();
|
|
return badDomains.includes(domain);
|
|
}
|
|
|
|
browser.tabs.onUpdated.addListener(catcher.watchFunction((id, info, tab) => {
|
|
if (info.url && tab.active) {
|
|
if (urlEnabled(info.url)) {
|
|
browser.browserAction.enable(tab.id);
|
|
} else if (hasSeenOnboarding) {
|
|
browser.browserAction.disable(tab.id);
|
|
}
|
|
}
|
|
}));
|
|
|
|
browser.tabs.onActivated.addListener(catcher.watchFunction(({tabId, windowId}) => {
|
|
catcher.watchPromise(browser.tabs.get(tabId).then((tab) => {
|
|
// onActivated may fire before the url is set
|
|
if (!tab.url) {
|
|
return;
|
|
}
|
|
if (urlEnabled(tab.url)) {
|
|
browser.browserAction.enable(tabId);
|
|
} else if (hasSeenOnboarding) {
|
|
browser.browserAction.disable(tabId);
|
|
}
|
|
}));
|
|
}));
|
|
|
|
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.authHeaders()
|
|
.then(() => browser.tabs.create({url: backend + "/shots"})));
|
|
});
|
|
|
|
communication.register("openShot", (sender, {url, copied}) => {
|
|
if (copied) {
|
|
const id = makeUuid();
|
|
return browser.notifications.create(id, {
|
|
type: "basic",
|
|
iconUrl: "../icons/copy.png",
|
|
title: browser.i18n.getMessage("notificationLinkCopiedTitle"),
|
|
message: browser.i18n.getMessage("notificationLinkCopiedDetails", pasteSymbol)
|
|
});
|
|
}
|
|
});
|
|
|
|
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 binary = atob(info.url.split(',')[1]); // just the base64 data
|
|
const data = Uint8Array.from(binary, char => char.charCodeAt(0))
|
|
const blob = new Blob([data], {type: "image/png"})
|
|
return browser.downloads.download({
|
|
url: URL.createObjectURL(blob),
|
|
filename: info.filename
|
|
});
|
|
});
|
|
|
|
communication.register("closeSelector", (sender) => {
|
|
setIconActive(false, sender.tab.id)
|
|
});
|
|
|
|
catcher.watchPromise(communication.sendToBootstrap("getOldDeviceInfo").then((deviceInfo) => {
|
|
if (deviceInfo === communication.NO_BOOTSTRAP || ! deviceInfo) {
|
|
return;
|
|
}
|
|
deviceInfo = JSON.parse(deviceInfo);
|
|
if (deviceInfo && typeof deviceInfo == "object") {
|
|
return auth.setDeviceInfoFromOldAddon(deviceInfo).then((updated) => {
|
|
if (updated === communication.NO_BOOTSTRAP) {
|
|
throw new Error("bootstrap.js disappeared unexpectedly");
|
|
}
|
|
if (updated) {
|
|
return communication.sendToBootstrap("removeOldAddon");
|
|
}
|
|
});
|
|
}
|
|
}));
|
|
|
|
communication.register("hasSeenOnboarding", () => {
|
|
hasSeenOnboarding = true;
|
|
catcher.watchPromise(browser.storage.local.set({hasSeenOnboarding}));
|
|
setIconActive(false, null);
|
|
browser.browserAction.setTitle({
|
|
title: browser.i18n.getMessage("contextMenuLabel")
|
|
});
|
|
});
|
|
|
|
communication.register("abortFrameset", () => {
|
|
sendEvent("abort-start-shot", "frame-page");
|
|
// 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"
|
|
});
|
|
});
|
|
|
|
return exports;
|
|
})();
|