forked from mirrors/gecko-dev
Differential Revision: https://phabricator.services.mozilla.com/D37217 --HG-- extra : moz-landing-system : lando
287 lines
9.6 KiB
JavaScript
287 lines
9.6 KiB
JavaScript
/* 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 global, documentMetadata, util, uicontrol, ui, catcher */
|
|
/* globals buildSettings, domainFromUrl, randomString, shot, blobConverters */
|
|
|
|
"use strict";
|
|
|
|
this.shooter = (function() { // eslint-disable-line no-unused-vars
|
|
const exports = {};
|
|
const { AbstractShot } = shot;
|
|
|
|
const RANDOM_STRING_LENGTH = 16;
|
|
const MAX_CANVAS_DIMENSION = 32767;
|
|
let backend;
|
|
let shotObject;
|
|
let supportsDrawWindow;
|
|
const callBackground = global.callBackground;
|
|
const clipboard = global.clipboard;
|
|
|
|
function regexpEscape(str) {
|
|
// http://stackoverflow.com/questions/3115150/how-to-escape-regular-expression-special-characters-using-javascript
|
|
return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
}
|
|
|
|
function sanitizeError(data) {
|
|
const href = new RegExp(regexpEscape(window.location.href), "g");
|
|
const origin = new RegExp(`${regexpEscape(window.location.origin)}[^ \t\n\r",>]*`, "g");
|
|
const json = JSON.stringify(data)
|
|
.replace(href, "REDACTED_HREF")
|
|
.replace(origin, "REDACTED_URL");
|
|
const result = JSON.parse(json);
|
|
return result;
|
|
}
|
|
|
|
catcher.registerHandler((errorObj) => {
|
|
callBackground("reportError", sanitizeError(errorObj));
|
|
});
|
|
|
|
catcher.watchFunction(() => {
|
|
const canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
|
const ctx = canvas.getContext("2d");
|
|
supportsDrawWindow = !!ctx.drawWindow;
|
|
})();
|
|
|
|
function captureToCanvas(selectedPos, captureType) {
|
|
let height = selectedPos.bottom - selectedPos.top;
|
|
let width = selectedPos.right - selectedPos.left;
|
|
const canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
// Scale the canvas for high-density displays, except for full-page shots.
|
|
let expand = window.devicePixelRatio !== 1;
|
|
if (captureType === "fullPage" || captureType === "fullPageTruncated") {
|
|
expand = false;
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
} else {
|
|
canvas.width = width * window.devicePixelRatio;
|
|
canvas.height = height * window.devicePixelRatio;
|
|
}
|
|
if (expand) {
|
|
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
|
}
|
|
|
|
// Double-check canvas width and height are within the canvas pixel limit.
|
|
// If the canvas dimensions are too great, crop the canvas and also crop
|
|
// the selection by a devicePixelRatio-scaled amount.
|
|
if (canvas.width > MAX_CANVAS_DIMENSION) {
|
|
canvas.width = MAX_CANVAS_DIMENSION;
|
|
width = expand ? Math.floor(canvas.width / window.devicePixelRatio) : canvas.width;
|
|
}
|
|
if (canvas.height > MAX_CANVAS_DIMENSION) {
|
|
canvas.height = MAX_CANVAS_DIMENSION;
|
|
height = expand ? Math.floor(canvas.height / window.devicePixelRatio) : canvas.height;
|
|
}
|
|
|
|
ui.iframe.hide();
|
|
ctx.drawWindow(window, selectedPos.left, selectedPos.top, width, height, "#fff");
|
|
return canvas;
|
|
}
|
|
|
|
const screenshotPage = exports.screenshotPage = function(selectedPos, captureType) {
|
|
if (!supportsDrawWindow) {
|
|
return null;
|
|
}
|
|
const canvas = captureToCanvas(selectedPos, captureType);
|
|
const limit = buildSettings.pngToJpegCutoff;
|
|
let dataUrl = canvas.toDataURL();
|
|
if (limit && dataUrl.length > limit) {
|
|
const jpegDataUrl = canvas.toDataURL("image/jpeg");
|
|
if (jpegDataUrl.length < dataUrl.length) {
|
|
// Only use the JPEG if it is actually smaller
|
|
dataUrl = jpegDataUrl;
|
|
}
|
|
}
|
|
return dataUrl;
|
|
};
|
|
|
|
function screenshotPageAsync(selectedPos, captureType) {
|
|
if (!supportsDrawWindow) {
|
|
return Promise.resolve(null);
|
|
}
|
|
const canvas = captureToCanvas(selectedPos, captureType);
|
|
ui.iframe.showLoader();
|
|
const imageData = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height);
|
|
return callBackground("canvasToDataURL", imageData);
|
|
}
|
|
|
|
let isSaving = null;
|
|
|
|
exports.takeShot = function(captureType, selectedPos, url) {
|
|
// isSaving indicates we're aleady in the middle of saving
|
|
// we use a timeout so in the case of a failure the button will
|
|
// still start working again
|
|
if (Math.floor(selectedPos.left) === Math.floor(selectedPos.right) ||
|
|
Math.floor(selectedPos.top) === Math.floor(selectedPos.bottom)) {
|
|
const exc = new Error("Empty selection");
|
|
exc.popupMessage = "EMPTY_SELECTION";
|
|
exc.noReport = true;
|
|
catcher.unhandled(exc);
|
|
return;
|
|
}
|
|
let imageBlob;
|
|
const uicontrol = global.uicontrol;
|
|
let deactivateAfterFinish = true;
|
|
if (isSaving) {
|
|
return;
|
|
}
|
|
isSaving = setTimeout(() => {
|
|
if (typeof ui !== "undefined") {
|
|
// ui might disappear while the timer is running because the save succeeded
|
|
ui.Box.clearSaveDisabled();
|
|
}
|
|
isSaving = null;
|
|
}, 1000);
|
|
selectedPos = selectedPos.toJSON();
|
|
let captureText = "";
|
|
if (buildSettings.captureText) {
|
|
captureText = util.captureEnclosedText(selectedPos);
|
|
}
|
|
const dataUrl = url || screenshotPage(selectedPos, captureType);
|
|
let type = blobConverters.getTypeFromDataUrl(dataUrl);
|
|
type = type ? type.split("/", 2)[1] : null;
|
|
if (dataUrl) {
|
|
imageBlob = buildSettings.uploadBinary ? blobConverters.dataUrlToBlob(dataUrl) : null;
|
|
shotObject.delAllClips();
|
|
shotObject.addClip({
|
|
createdDate: Date.now(),
|
|
image: {
|
|
url: buildSettings.uploadBinary ? "" : dataUrl,
|
|
type,
|
|
captureType,
|
|
text: captureText,
|
|
location: selectedPos,
|
|
dimensions: {
|
|
x: selectedPos.right - selectedPos.left,
|
|
y: selectedPos.bottom - selectedPos.top,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
catcher.watchPromise(callBackground("takeShot", {
|
|
captureType,
|
|
captureText,
|
|
scroll: {
|
|
scrollX: window.scrollX,
|
|
scrollY: window.scrollY,
|
|
innerHeight: window.innerHeight,
|
|
innerWidth: window.innerWidth,
|
|
},
|
|
selectedPos,
|
|
shotId: shotObject.id,
|
|
shot: shotObject.toJSON(),
|
|
imageBlob,
|
|
}).then((url) => {
|
|
return clipboard.copy(url).then((copied) => {
|
|
return callBackground("openShot", { url, copied });
|
|
});
|
|
}, (error) => {
|
|
if ("popupMessage" in error && (error.popupMessage === "REQUEST_ERROR" || error.popupMessage === "CONNECTION_ERROR")) {
|
|
// The error has been signaled to the user, but unlike other errors (or
|
|
// success) we should not abort the selection
|
|
deactivateAfterFinish = false;
|
|
// We need to unhide the UI since screenshotPage() hides it.
|
|
ui.iframe.unhide();
|
|
return;
|
|
}
|
|
if (error.name !== "BackgroundError") {
|
|
// BackgroundError errors are reported in the Background page
|
|
throw error;
|
|
}
|
|
}).then(() => {
|
|
if (deactivateAfterFinish) {
|
|
uicontrol.deactivate();
|
|
}
|
|
}));
|
|
};
|
|
|
|
exports.downloadShot = function(selectedPos, previewDataUrl, type) {
|
|
const shotPromise = previewDataUrl ? Promise.resolve(previewDataUrl) : screenshotPageAsync(selectedPos, type);
|
|
catcher.watchPromise(shotPromise.then(dataUrl => {
|
|
let promise = Promise.resolve(dataUrl);
|
|
if (!dataUrl) {
|
|
promise = callBackground(
|
|
"screenshotPage",
|
|
selectedPos.toJSON(),
|
|
{
|
|
scrollX: window.scrollX,
|
|
scrollY: window.scrollY,
|
|
innerHeight: window.innerHeight,
|
|
innerWidth: window.innerWidth,
|
|
});
|
|
}
|
|
catcher.watchPromise(promise.then((dataUrl) => {
|
|
let type = blobConverters.getTypeFromDataUrl(dataUrl);
|
|
type = type ? type.split("/", 2)[1] : null;
|
|
shotObject.delAllClips();
|
|
shotObject.addClip({
|
|
createdDate: Date.now(),
|
|
image: {
|
|
url: dataUrl,
|
|
type,
|
|
location: selectedPos,
|
|
},
|
|
});
|
|
ui.triggerDownload(dataUrl, shotObject.filename);
|
|
uicontrol.deactivate();
|
|
}));
|
|
}));
|
|
};
|
|
|
|
let copyInProgress = null;
|
|
exports.copyShot = function(selectedPos, previewDataUrl, type) {
|
|
// This is pretty slow. We'll ignore additional user triggered copy events
|
|
// while it is in progress.
|
|
if (copyInProgress) {
|
|
return;
|
|
}
|
|
// A max of five seconds in case some error occurs.
|
|
copyInProgress = setTimeout(() => {
|
|
copyInProgress = null;
|
|
}, 5000);
|
|
|
|
const unsetCopyInProgress = () => {
|
|
if (copyInProgress) {
|
|
clearTimeout(copyInProgress);
|
|
copyInProgress = null;
|
|
}
|
|
};
|
|
const shotPromise = previewDataUrl ? Promise.resolve(previewDataUrl) : screenshotPageAsync(selectedPos, type);
|
|
catcher.watchPromise(shotPromise.then(dataUrl => {
|
|
const blob = blobConverters.dataUrlToBlob(dataUrl);
|
|
catcher.watchPromise(callBackground("copyShotToClipboard", blob).then(() => {
|
|
uicontrol.deactivate();
|
|
unsetCopyInProgress();
|
|
}, unsetCopyInProgress));
|
|
}));
|
|
};
|
|
|
|
exports.sendEvent = function(...args) {
|
|
const maybeOptions = args[args.length - 1];
|
|
|
|
if (typeof maybeOptions === "object") {
|
|
maybeOptions.incognito = browser.extension.inIncognitoContext;
|
|
} else {
|
|
args.push({incognito: browser.extension.inIncognitoContext});
|
|
}
|
|
|
|
callBackground("sendEvent", ...args);
|
|
};
|
|
|
|
catcher.watchFunction(() => {
|
|
shotObject = new AbstractShot(
|
|
backend,
|
|
randomString(RANDOM_STRING_LENGTH) + "/" + domainFromUrl(location),
|
|
{
|
|
origin: shot.originFromUrl(location.href),
|
|
}
|
|
);
|
|
shotObject.update(documentMetadata());
|
|
})();
|
|
|
|
return exports;
|
|
})();
|
|
null;
|