PK !<+g¨¹assertIsBlankDocument.js/* 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 */ /** For use inside an iframe onload function, throws an Error if iframe src is not blank.html Should be applied *inside* catcher.watchFunction */ this.assertIsBlankDocument = function assertIsBlankDocument(doc) { if (doc.documentURI !== browser.runtime.getURL("blank.html")) { const exc = new Error("iframe URL does not match expected blank.html"); exc.foundURL = doc.documentURI; throw exc; } }; null; PK !<«õ|jhhassertIsTrusted.js/* 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/. */ /** For use with addEventListener, assures that any events have event.isTrusted set to true https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted Should be applied *inside* catcher.watchFunction */ this.assertIsTrusted = function assertIsTrusted(handlerFunction) { return function (event) { if (!event) { const exc = new Error("assertIsTrusted did not get an event"); exc.noPopup = true; throw exc; } if (!event.isTrusted) { const exc = new Error(`Received untrusted event (type: ${event.type})`); exc.noPopup = true; throw exc; } return handlerFunction.call(this, event); }; }; null; PK ! { 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; })(); PK !< üõ½ååbackground/communication.js/* 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; })(); PK !<àˆ:ÊÊbackground/deviceInfo.js/* 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, }; }; })(); PK ! { 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; })(); PK !<È'< ç ç background/selectorLoader.js/* 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; PK !<ÈŠ—:ËËbackground/senderror.js/* 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; })(); PK ! 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; })(); PK !< ßUr  background/takeshot.js/* 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; })(); PK !<9–0Aÿÿ blank.html PK !<‡|uø¾¾blobConverters.js/* 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/. */ this.blobConverters = (function () { const exports = {}; exports.dataUrlToBlob = function (url) { const binary = atob(url.split(",", 2)[1]); let contentType = exports.getTypeFromDataUrl(url); if (contentType !== "image/png" && contentType !== "image/jpeg") { contentType = "image/png"; } const data = Uint8Array.from(binary, char => char.charCodeAt(0)); const blob = new Blob([data], { type: contentType }); return blob; }; exports.getTypeFromDataUrl = function (url) { let contentType = url.split(",", 1)[0]; contentType = contentType.split(";", 1)[0]; contentType = contentType.split(":", 2)[1]; return contentType; }; exports.blobToArray = function (blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.addEventListener("loadend", function () { resolve(reader.result); }); reader.readAsArrayBuffer(blob); }); }; exports.blobToDataUrl = function (blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.addEventListener("loadend", function () { resolve(reader.result); }); reader.readAsDataURL(blob); }); }; return exports; })(); null; PK !<ùKøzQzQbuild/inlineSelectionCss.js/* 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/. */ /* Created from build/server/static/css/inline-selection.css */ window.inlineSelectionCss = ` .button, .highlight-button-cancel, .highlight-button-download, .highlight-button-copy { display: flex; align-items: center; justify-content: center; column-gap: 8px; border: 0; border-radius: 3px; cursor: pointer; font-size: 16px; font-weight: 400; height: 40px; min-width: 40px; outline: none; padding: 0 10px; position: relative; text-align: center; text-decoration: none; transition: background 150ms cubic-bezier(0.07, 0.95, 0, 1), border 150ms cubic-bezier(0.07, 0.95, 0, 1); user-select: none; white-space: nowrap; } .button.hidden, .hidden.highlight-button-cancel, .hidden.highlight-button-download, .hidden.highlight-button-copy { display: none; } .button.small, .small.highlight-button-cancel, .small.highlight-button-download, .small.highlight-button-copy { height: 32px; line-height: 32px; padding: 0 8px; } .button.active, .active.highlight-button-cancel, .active.highlight-button-download, .active.highlight-button-copy { background-color: #dedede; } .button.tiny, .tiny.highlight-button-cancel, .tiny.highlight-button-download, .tiny.highlight-button-copy { font-size: 14px; height: 26px; border: 1px solid #c7c7c7; } .button.tiny:hover, .tiny.highlight-button-cancel:hover, .tiny.highlight-button-download:hover, .tiny.highlight-button-copy:hover, .button.tiny:focus, .tiny.highlight-button-cancel:focus, .tiny.highlight-button-download:focus, .tiny.highlight-button-copy:focus { background: #ededf0; border-color: #989898; } .button.tiny:active, .tiny.highlight-button-cancel:active, .tiny.highlight-button-download:active, .tiny.highlight-button-copy:active { background: #dedede; border-color: #989898; } .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-download, .block-button.highlight-button-copy { display: flex; align-items: center; justify-content: center; box-sizing: border-box; border: 0; border-inline-end: 1px solid #c7c7c7; box-shadow: 0; border-radius: 0; flex-shrink: 0; font-size: 20px; height: 100px; line-height: 100%; overflow: hidden; } @media (max-width: 719px) { .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-download, .block-button.highlight-button-copy { justify-content: flex-start; font-size: 16px; height: 72px; margin-inline-end: 10px; padding: 0 5px; } } .button.block-button:hover, .block-button.highlight-button-cancel:hover, .block-button.highlight-button-download:hover, .block-button.highlight-button-copy:hover { background: #ededf0; } .button.block-button:active, .block-button.highlight-button-cancel:active, .block-button.highlight-button-download:active, .block-button.highlight-button-copy:active { background: #dedede; } .button.download, .download.highlight-button-cancel, .download.highlight-button-download, .download.highlight-button-copy, .button.edit, .edit.highlight-button-cancel, .edit.highlight-button-download, .edit.highlight-button-copy, .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-download, .trash.highlight-button-copy, .button.share, .share.highlight-button-cancel, .share.highlight-button-download, .share.highlight-button-copy, .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-download, .flag.highlight-button-copy { background-repeat: no-repeat; background-size: 50%; background-position: center; margin-inline-end: 10px; transition: background-color 150ms cubic-bezier(0.07, 0.95, 0, 1); } .button.download, .download.highlight-button-cancel, .download.highlight-button-download, .download.highlight-button-copy { background-image: url("chrome://browser/content/screenshots/download.svg"); } .button.download:hover, .download.highlight-button-cancel:hover, .download.highlight-button-download:hover, .download.highlight-button-copy:hover { background-color: #ededf0; } .button.download:active, .download.highlight-button-cancel:active, .download.highlight-button-download:active, .download.highlight-button-copy:active { background-color: #dedede; } .button.share, .share.highlight-button-cancel, .share.highlight-button-download, .share.highlight-button-copy { background-image: url("../img/icon-share.svg"); } .button.share:hover, .share.highlight-button-cancel:hover, .share.highlight-button-download:hover, .share.highlight-button-copy:hover { background-color: #ededf0; } .button.share.active, .share.active.highlight-button-cancel, .share.active.highlight-button-download, .share.active.highlight-button-copy, .button.share:active, .share.highlight-button-cancel:active, .share.highlight-button-download:active, .share.highlight-button-copy:active { background-color: #dedede; } .button.share.newicon, .share.newicon.highlight-button-cancel, .share.newicon.highlight-button-download, .share.newicon.highlight-button-copy { background-image: url("../img/icon-share-alternate.svg"); } .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-download, .trash.highlight-button-copy { background-image: url("../img/icon-trash.svg"); } .button.trash:hover, .trash.highlight-button-cancel:hover, .trash.highlight-button-download:hover, .trash.highlight-button-copy:hover { background-color: #ededf0; } .button.trash:active, .trash.highlight-button-cancel:active, .trash.highlight-button-download:active, .trash.highlight-button-copy:active { background-color: #dedede; } .button.edit, .edit.highlight-button-cancel, .edit.highlight-button-download, .edit.highlight-button-copy { background-image: url("../img/icon-edit.svg"); } .button.edit:hover, .edit.highlight-button-cancel:hover, .edit.highlight-button-download:hover, .edit.highlight-button-copy:hover { background-color: #ededf0; } .button.edit:active, .edit.highlight-button-cancel:active, .edit.highlight-button-download:active, .edit.highlight-button-copy:active { background-color: #dedede; } .app-body { background: #f9f9fa; color: #38383d; } .app-body a { color: #0a84ff; } .highlight-color-scheme { background: #0a84ff; color: #fff; } .highlight-color-scheme a { color: #fff; text-decoration: underline; } .alt-color-scheme { background: #38383d; color: #f9f9fa; } .alt-color-scheme h1 { color: #6f7fb6; } .alt-color-scheme a { color: #e1e1e6; text-decoration: underline; } .button.primary, .primary.highlight-button-cancel, .highlight-button-download, .primary.highlight-button-copy { background-color: #0a84ff; color: #fff; } .button.primary:hover, .primary.highlight-button-cancel:hover, .highlight-button-download:hover, .primary.highlight-button-copy:hover, .button.primary:focus, .primary.highlight-button-cancel:focus, .highlight-button-download:focus, .primary.highlight-button-copy:focus { background-color: #0072e5; } .button.primary:active, .primary.highlight-button-cancel:active, .highlight-button-download:active, .primary.highlight-button-copy:active { background-color: #0065cc; } .button.secondary, .highlight-button-cancel, .secondary.highlight-button-download, .highlight-button-copy { background-color: #f9f9fa; color: #38383d; } .button.secondary:hover, .highlight-button-cancel:hover, .secondary.highlight-button-download:hover, .highlight-button-copy:hover { background-color: #ededf0; } .button.secondary:active, .highlight-button-cancel:active, .secondary.highlight-button-download:active, .highlight-button-copy:active { background-color: #dedede; } .button.transparent, .transparent.highlight-button-cancel, .transparent.highlight-button-download, .transparent.highlight-button-copy { background-color: transparent; color: #38383d; } .button.transparent:hover, .transparent.highlight-button-cancel:hover, .transparent.highlight-button-download:hover, .transparent.highlight-button-copy:hover { background-color: #ededf0; } .button.transparent:focus, .transparent.highlight-button-cancel:focus, .transparent.highlight-button-download:focus, .transparent.highlight-button-copy:focus, .button.transparent:active, .transparent.highlight-button-cancel:active, .transparent.highlight-button-download:active, .transparent.highlight-button-copy:active { background-color: #dedede; } .button.warning, .warning.highlight-button-cancel, .warning.highlight-button-download, .warning.highlight-button-copy { color: #fff; background: #d92215; } .button.warning:hover, .warning.highlight-button-cancel:hover, .warning.highlight-button-download:hover, .warning.highlight-button-copy:hover, .button.warning:focus, .warning.highlight-button-cancel:focus, .warning.highlight-button-download:focus, .warning.highlight-button-copy:focus { background: #b81d12; } .button.warning:active, .warning.highlight-button-cancel:active, .warning.highlight-button-download:active, .warning.highlight-button-copy:active { background: #a11910; } .subtitle-link { color: #0a84ff; } .loader { background: rgba(12, 12, 13, 0.2); border-radius: 2px; height: 4px; overflow: hidden; position: relative; width: 200px; } .loader-inner { animation: bounce infinite alternate 1250ms cubic-bezier(0.7, 0, 0.3, 1); background: #45a1ff; border-radius: 2px; height: 4px; transform: translateX(-40px); width: 50px; } @keyframes bounce { 0% { transform: translateX(-40px); } 100% { transform: translate(190px); } } @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes pop { 0% { transform: scale(1); } 97% { transform: scale(1.04); } 100% { transform: scale(1); } } @keyframes pulse { 0% { opacity: 0.3; transform: scale(1); } 70% { opacity: 0.25; transform: scale(1.04); } 100% { opacity: 0.3; transform: scale(1); } } @keyframes slide-left { 0% { opacity: 0; transform: translate3d(160px, 0, 0); } 100% { opacity: 1; transform: translate3d(0, 0, 0); } } @keyframes bounce-in { 0% { opacity: 0; transform: scale(1); } 60% { opacity: 1; transform: scale(1.02); } 100% { transform: scale(1); } } .mover-target { display: flex; align-items: center; justify-content: center; pointer-events: auto; position: absolute; z-index: 5; } .highlight, .mover-target { background-color: transparent; background-image: none; } .mover-target, .bghighlight { border: 0; } .hover-highlight { animation: fade-in 125ms forwards cubic-bezier(0.07, 0.95, 0, 1); background: rgba(255, 255, 255, 0.2); border-radius: 1px; pointer-events: none; position: absolute; z-index: 10000000000; } .hover-highlight::before { border: 2px dashed rgba(255, 255, 255, 0.4); bottom: 0; content: ""; inset-inline-start: 0; position: absolute; inset-inline-end: 0; top: 0; } /* When prefers contrast is fully supported, we should change these quereies to cover both high and low prefers contrast cases */ @media (forced-colors: active) { .hover-highlight { background-color: white; opacity: 0.2; } } .mover-target.direction-topLeft { cursor: nwse-resize; height: 60px; left: -30px; top: -30px; width: 60px; } .mover-target.direction-top { cursor: ns-resize; height: 60px; inset-inline-start: 0; top: -30px; width: 100%; z-index: 4; } .mover-target.direction-topRight { cursor: nesw-resize; height: 60px; right: -30px; top: -30px; width: 60px; } .mover-target.direction-left { cursor: ew-resize; height: 100%; left: -30px; top: 0; width: 60px; z-index: 4; } .mover-target.direction-right { cursor: ew-resize; height: 100%; right: -30px; top: 0; width: 60px; z-index: 4; } .mover-target.direction-bottomLeft { bottom: -30px; cursor: nesw-resize; height: 60px; left: -30px; width: 60px; } .mover-target.direction-bottom { bottom: -30px; cursor: ns-resize; height: 60px; inset-inline-start: 0; width: 100%; z-index: 4; } .mover-target.direction-bottomRight { bottom: -30px; cursor: nwse-resize; height: 60px; right: -30px; width: 60px; } .mover-target:hover .mover { transform: scale(1.05); } .mover { background-color: #fff; border-radius: 50%; box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); height: 16px; opacity: 1; position: relative; transition: transform 125ms cubic-bezier(0.07, 0.95, 0, 1); width: 16px; } .small-selection .mover { height: 10px; width: 10px; } .direction-topLeft .mover, .direction-left .mover, .direction-bottomLeft .mover { left: -1px; } .direction-topLeft .mover, .direction-top .mover, .direction-topRight .mover { top: -1px; } .direction-topRight .mover, .direction-right .mover, .direction-bottomRight .mover { right: -1px; } .direction-bottomRight .mover, .direction-bottom .mover, .direction-bottomLeft .mover { bottom: -1px; } .bghighlight { background-color: rgba(0, 0, 0, 0.7); position: absolute; z-index: 9999999999; } /* When prefers contrast is fully supported, we should change these quereies to cover both high and low prefers contrast cases */ @media (forced-colors: active) { .bghighlight { background-color: black; opacity: 0.7; } } .preview-overlay { align-items: center; background-color: rgba(0, 0, 0, 0.7); display: flex; height: 100%; justify-content: center; inset-inline-start: 0; margin: 0; padding: 0; position: fixed; top: 0; width: 100%; z-index: 9999999999; } /* When prefers contrast is fully supported, we should change these quereies to cover both high and low prefers contrast cases */ @media (forced-colors: active) { .preview-overlay { background-color: black; opacity: 0.7; } } .precision-cursor { cursor: crosshair; } .highlight { border-radius: 1px; border: 2px dashed rgba(255, 255, 255, 0.8); box-sizing: border-box; cursor: move; position: absolute; z-index: 9999999999; } /* When prefers contrast is fully supported, we should change these quereies to cover both high and low prefers contrast cases */ @media (forced-colors: active) { .highlight { border: 2px dashed white; opacity: 1.0; } } .highlight-buttons { display: flex; align-items: center; justify-content: center; bottom: -58px; position: absolute; inset-inline-end: 5px; z-index: 6; } .bottom-selection .highlight-buttons { bottom: 5px; } .left-selection .highlight-buttons { inset-inline-end: auto; inset-inline-start: 5px; } .highlight-buttons > button { box-shadow: 0 0 0 1px rgba(12, 12, 13, 0.1), 0 2px 8px rgba(12, 12, 13, 0.1); } .highlight-button-cancel { margin: 5px; width: 40px; } .highlight-button-download { margin: 5px; width: auto; font-size: 18px; } .highlight-button-download img { height: 16px; width: 16px; } .highlight-button-download:-moz-locale-dir(rtl) { flex-direction: reverse; } .highlight-button-download img:-moz-locale-dir(ltr) { padding-inline-end: 8px; } .highlight-button-download img:-moz-locale-dir(rtl) { padding-inline-start: 8px; } .highlight-button-copy { margin: 5px; width: auto; } .highlight-button-copy img { height: 16px; width: 16px; } .highlight-button-copy:-moz-locale-dir(rtl) { flex-direction: reverse; } .highlight-button-copy img:-moz-locale-dir(ltr) { padding-inline-end: 8px; } .highlight-button-copy img:-moz-locale-dir(rtl) { padding-inline-start: 8px; } .pixel-dimensions { position: absolute; pointer-events: none; font-weight: bold; font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif; font-size: 70%; color: #000; text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; } .preview-buttons { display: flex; align-items: center; justify-content: flex-end; padding-inline-end: 4px; inset-inline-end: 0; width: 100%; position: absolute; height: 60px; border-radius: 4px 4px 0 0; background: rgba(249, 249, 250, 0.8); top: 0; border: 1px solid rgba(249, 249, 250, 0.2); border-bottom: 0; box-sizing: border-box; } .preview-image { display: flex; align-items: center; flex-direction: column; justify-content: center; margin: 24px auto; position: relative; max-width: 80%; max-height: 95%; text-align: center; animation-delay: 50ms; display: flex; } .preview-image-wrapper { background: rgba(249, 249, 250, 0.8); border-radius: 0 0 4px 4px; display: block; height: auto; max-width: 100%; min-width: 320px; overflow-y: scroll; padding: 0 60px; margin-top: 60px; border: 1px solid rgba(249, 249, 250, 0.2); border-top: 0; } .preview-image-wrapper > img { box-shadow: 0 0 0 1px rgba(12, 12, 13, 0.1), 0 2px 8px rgba(12, 12, 13, 0.1); height: auto; margin-bottom: 60px; max-width: 100%; width: 100%; } .fixed-container { align-items: center; display: flex; flex-direction: column; height: 100vh; justify-content: center; inset-inline-start: 0; margin: 0; padding: 0; pointer-events: none; position: fixed; top: 0; width: 100%; } .face-container { position: relative; width: 64px; height: 64px; } .face { width: 62.4px; height: 62.4px; display: block; background-image: url("chrome://browser/content/screenshots/icon-welcome-face-without-eyes.svg"); } .eye { background-color: #fff; width: 10.8px; height: 14.6px; position: absolute; border-radius: 100%; overflow: hidden; inset-inline-start: 16.4px; top: 19.8px; } .eyeball { position: absolute; width: 6px; height: 6px; background-color: #000; border-radius: 50%; inset-inline-start: 2.4px; top: 4.3px; z-index: 10; } .left { margin-inline-start: 0; } .right { margin-inline-start: 20px; } .preview-instructions { display: flex; align-items: center; justify-content: center; animation: pulse 125mm cubic-bezier(0.07, 0.95, 0, 1); color: #fff; font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif; font-size: 24px; line-height: 32px; text-align: center; padding-top: 20px; width: 400px; user-select: none; } .cancel-shot { background-color: transparent; cursor: pointer; outline: none; border-radius: 3px; border: 1px #9b9b9b solid; color: #fff; cursor: pointer; font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif; font-size: 16px; margin-top: 40px; padding: 10px 25px; pointer-events: all; } .all-buttons-container { display: flex; flex-direction: row-reverse; background: #f5f5f5; border-radius: 2px; box-sizing: border-box; height: 80px; padding: 8px; position: absolute; inset-inline-end: 8px; top: 8px; box-shadow: 0 0 0 1px rgba(12, 12, 13, 0.1), 0 2px 8px rgba(12, 12, 13, 0.1); } .all-buttons-container .spacer { background-color: #c9c9c9; flex: 0 0 1px; height: 80px; margin: 0 10px; position: relative; top: -8px; } .all-buttons-container button { display: flex; align-items: center; flex-direction: column; justify-content: flex-end; color: #3e3d40; background-color: #f5f5f5; background-position: center top; background-repeat: no-repeat; background-size: 46px 46px; border: 1px solid transparent; cursor: pointer; height: 100%; min-width: 90px; padding: 46px 5px 5px; pointer-events: all; transition: border 150ms cubic-bezier(0.07, 0.95, 0, 1), background-color 150ms cubic-bezier(0.07, 0.95, 0, 1); white-space: nowrap; } .all-buttons-container button:hover { background-color: #ebebeb; border: 1px solid #c7c7c7; } .all-buttons-container button:active { background-color: #dedede; border: 1px solid #989898; } .all-buttons-container .full-page { background-image: url("chrome://browser/content/screenshots/menu-fullpage.svg"); } .all-buttons-container .visible { background-image: url("chrome://browser/content/screenshots/menu-visible.svg"); } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.06); } 100% { transform: scale(1); } } @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } `; null; PK !<€­‘³e e build/selection.js/* 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/. */ this.selection = (function () { let exports = {}; class Selection { constructor(x1, y1, x2, y2) { this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; } get top() { return Math.min(this.y1, this.y2); } set top(val) { if (this.y1 < this.y2) { this.y1 = val; } else { this.y2 = val; } } get bottom() { return Math.max(this.y1, this.y2); } set bottom(val) { if (this.y1 > this.y2) { this.y1 = val; } else { this.y2 = val; } } get left() { return Math.min(this.x1, this.x2); } set left(val) { if (this.x1 < this.x2) { this.x1 = val; } else { this.x2 = val; } } get right() { return Math.max(this.x1, this.x2); } set right(val) { if (this.x1 > this.x2) { this.x1 = val; } else { this.x2 = val; } } get width() { return Math.abs(this.x2 - this.x1); } get height() { return Math.abs(this.y2 - this.y1); } rect() { return { top: Math.floor(this.top), left: Math.floor(this.left), bottom: Math.floor(this.bottom), right: Math.floor(this.right), }; } union(other) { return new Selection( Math.min(this.left, other.left), Math.min(this.top, other.top), Math.max(this.right, other.right), Math.max(this.bottom, other.bottom) ); } /** Sort x1/x2 and y1/y2 so x1 this.x2) { [this.x1, this.x2] = [this.x2, this.x1]; } if (this.y1 > this.y2) { [this.y1, this.y2] = [this.y2, this.y1]; } } clone() { return new Selection(this.x1, this.y1, this.x2, this.y2); } toJSON() { return { left: this.left, right: this.right, top: this.top, bottom: this.bottom, }; } static getBoundingClientRect(el) { if (!el.getBoundingClientRect) { // Typically the element or somesuch return null; } const rect = el.getBoundingClientRect(); if (!rect) { return null; } return new Selection(rect.left, rect.top, rect.right, rect.bottom); } } if (typeof exports !== "undefined") { exports.Selection = Selection; } return exports; })(); null; PK !<ðSÀÿ`ÿ` build/shot.js/* 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 process, require */ this.shot = (function () { let exports = {}; // Note: in this library we can't use any "system" dependencies because this can be used from multiple // environments const isNode = typeof process !== "undefined" && Object.prototype.toString.call(process) === "[object process]"; const URL = (isNode && require("url").URL) || window.URL; /** Throws an error if the condition isn't true. Any extra arguments after the condition are used as console.error() arguments. */ function assert(condition, ...args) { if (condition) { return; } console.error("Failed assertion", ...args); throw new Error(`Failed assertion: ${args.join(" ")}`); } /** True if `url` is a valid URL */ function isUrl(url) { try { const parsed = new URL(url); if (parsed.protocol === "view-source:") { return isUrl(url.substr("view-source:".length)); } return true; } catch (e) { return false; } } function isValidClipImageUrl(url) { return isUrl(url) && !(url.indexOf(")") > -1); } function assertUrl(url) { if (!url) { throw new Error("Empty value is not URL"); } if (!isUrl(url)) { const exc = new Error("Not a URL"); exc.scheme = url.split(":")[0]; throw exc; } } function isSecureWebUri(url) { return isUrl(url) && url.toLowerCase().startsWith("https"); } function assertOrigin(url) { assertUrl(url); if (url.search(/^https?:/i) !== -1) { let newUrl = new URL(url); if (newUrl.pathname != "/") { throw new Error("Bad origin, might include path"); } } } function originFromUrl(url) { if (!url) { return null; } if (url.search(/^https?:/i) === -1) { // Non-HTTP URLs don't have an origin return null; } try { let tryUrl = new URL(url); return tryUrl.origin; } catch { return null; } } /** Check if the given object has all of the required attributes, and no extra attributes exception those in optional */ function checkObject(obj, required, optional) { if (typeof obj !== "object" || obj === null) { throw new Error( "Cannot check non-object: " + typeof obj + " that is " + JSON.stringify(obj) ); } required = required || []; for (const attr of required) { if (!(attr in obj)) { return false; } } optional = optional || []; for (const attr in obj) { if (!required.includes(attr) && !optional.includes(attr)) { return false; } } return true; } /** Create a JSON object from a normal object, given the required and optional attributes (filtering out any other attributes). Optional attributes are only kept when they are truthy. */ function jsonify(obj, required, optional) { required = required || []; const result = {}; for (const attr of required) { result[attr] = obj[attr]; } optional = optional || []; for (const attr of optional) { if (obj[attr]) { result[attr] = obj[attr]; } } return result; } /** True if the two objects look alike. Null, undefined, and absent properties are all treated as equivalent. Traverses objects and arrays */ function deepEqual(a, b) { if ((a === null || a === undefined) && (b === null || b === undefined)) { return true; } if (typeof a !== "object" || typeof b !== "object") { return a === b; } if (Array.isArray(a)) { if (!Array.isArray(b)) { return false; } if (a.length !== b.length) { return false; } for (let i = 0; i < a.length; i++) { if (!deepEqual(a[i], b[i])) { return false; } } } if (Array.isArray(b)) { return false; } const seen = new Set(); for (const attr of Object.keys(a)) { if (!deepEqual(a[attr], b[attr])) { return false; } seen.add(attr); } for (const attr of Object.keys(b)) { if (!seen.has(attr)) { if (!deepEqual(a[attr], b[attr])) { return false; } } } return true; } function makeRandomId() { // Note: this isn't for secure contexts, only for non-conflicting IDs let id = ""; while (id.length < 12) { let num; if (!id) { num = Date.now() % Math.pow(36, 3); } else { num = Math.floor(Math.random() * Math.pow(36, 3)); } id += num.toString(36); } return id; } class AbstractShot { constructor(backend, id, attrs) { attrs = attrs || {}; assert( /^[a-zA-Z0-9]{1,4000}\/[a-z0-9._-]{1,4000}$/.test(id), "Bad ID (should be alphanumeric):", JSON.stringify(id) ); this._backend = backend; this._id = id; this.origin = attrs.origin || null; this.fullUrl = attrs.fullUrl || null; if (!attrs.fullUrl && attrs.url) { console.warn("Received deprecated attribute .url"); this.fullUrl = attrs.url; } if (this.origin && !isSecureWebUri(this.origin)) { this.origin = ""; } if (this.fullUrl && !isSecureWebUri(this.fullUrl)) { this.fullUrl = ""; } this.docTitle = attrs.docTitle || null; this.userTitle = attrs.userTitle || null; this.createdDate = attrs.createdDate || Date.now(); this.siteName = attrs.siteName || null; this.images = []; if (attrs.images) { this.images = attrs.images.map(json => new this.Image(json)); } this.openGraph = attrs.openGraph || null; this.twitterCard = attrs.twitterCard || null; this.documentSize = attrs.documentSize || null; this.thumbnail = attrs.thumbnail || null; this.abTests = attrs.abTests || null; this.firefoxChannel = attrs.firefoxChannel || null; this._clips = {}; if (attrs.clips) { for (const clipId in attrs.clips) { const clip = attrs.clips[clipId]; this._clips[clipId] = new this.Clip(this, clipId, clip); } } const isProd = typeof process !== "undefined" && process.env.NODE_ENV === "production"; for (const attr in attrs) { if ( attr !== "clips" && attr !== "id" && !this.REGULAR_ATTRS.includes(attr) && !this.DEPRECATED_ATTRS.includes(attr) ) { if (isProd) { console.warn("Unexpected attribute: " + attr); } else { throw new Error("Unexpected attribute: " + attr); } } else if (attr === "id") { console.warn("passing id in attrs in AbstractShot constructor"); console.trace(); assert(attrs.id === this.id); } } } /** Update any and all attributes in the json object, with deep updating of `json.clips` */ update(json) { const ALL_ATTRS = ["clips"].concat(this.REGULAR_ATTRS); assert( checkObject(json, [], ALL_ATTRS), "Bad attr to new Shot():", Object.keys(json) ); for (const attr in json) { if (attr === "clips") { continue; } if ( typeof json[attr] === "object" && typeof this[attr] === "object" && this[attr] !== null ) { let val = this[attr]; if (val.toJSON) { val = val.toJSON(); } if (!deepEqual(json[attr], val)) { this[attr] = json[attr]; } } else if (json[attr] !== this[attr] && (json[attr] || this[attr])) { this[attr] = json[attr]; } } if (json.clips) { for (const clipId in json.clips) { if (!json.clips[clipId]) { this.delClip(clipId); } else if (!this.getClip(clipId)) { this.setClip(clipId, json.clips[clipId]); } else if ( !deepEqual(this.getClip(clipId).toJSON(), json.clips[clipId]) ) { this.setClip(clipId, json.clips[clipId]); } } } } /** Returns a JSON version of this shot */ toJSON() { const result = {}; for (const attr of this.REGULAR_ATTRS) { let val = this[attr]; if (val && val.toJSON) { val = val.toJSON(); } result[attr] = val; } result.clips = {}; for (const attr in this._clips) { result.clips[attr] = this._clips[attr].toJSON(); } return result; } /** A more minimal JSON representation for creating indexes of shots */ asRecallJson() { const result = { clips: {} }; for (const attr of this.RECALL_ATTRS) { let val = this[attr]; if (val && val.toJSON) { val = val.toJSON(); } result[attr] = val; } for (const name of this.clipNames()) { result.clips[name] = this.getClip(name).toJSON(); } return result; } get backend() { return this._backend; } get id() { return this._id; } get url() { return this.fullUrl || this.origin; } set url(val) { throw new Error(".url is read-only"); } get fullUrl() { return this._fullUrl; } set fullUrl(val) { if (val) { assertUrl(val); } this._fullUrl = val || undefined; } get origin() { return this._origin; } set origin(val) { if (val) { assertOrigin(val); } this._origin = val || undefined; } get isOwner() { return this._isOwner; } set isOwner(val) { this._isOwner = val || undefined; } get filename() { let filenameTitle = this.title; const date = new Date(this.createdDate); /* eslint-disable no-control-regex */ filenameTitle = filenameTitle .replace(/[\\/]/g, "_") .replace(/[\u200e\u200f\u202a-\u202e]/g, "") .replace(/[\x00-\x1f\x7f-\x9f:*?|"<>;,+=\[\]]+/g, " ") .replace(/^[\s\u180e.]+|[\s\u180e.]+$/g, ""); /* eslint-enable no-control-regex */ filenameTitle = filenameTitle.replace(/\s{1,4000}/g, " "); const currentDateTime = new Date( date.getTime() - date.getTimezoneOffset() * 60 * 1000 ).toISOString(); const filenameDate = currentDateTime.substring(0, 10); const filenameTime = currentDateTime.substring(11, 19).replace(/:/g, "-"); let clipFilename = `Screenshot ${filenameDate} at ${filenameTime} ${filenameTitle}`; // Crop the filename size at less than 246 bytes, so as to leave // room for the extension and an ellipsis [...]. Note that JS // strings are UTF16 but the filename will be converted to UTF8 // when saving which could take up more space, and we want a // maximum of 255 bytes (not characters). Here, we iterate // and crop at shorter and shorter points until we fit into // 255 bytes. let suffix = ""; for (let cropSize = 246; cropSize >= 0; cropSize -= 32) { if (new Blob([clipFilename]).size > 246) { clipFilename = clipFilename.substring(0, cropSize); suffix = "[...]"; } else { break; } } clipFilename += suffix; const clip = this.getClip(this.clipNames()[0]); let extension = ".png"; if (clip && clip.image && clip.image.type) { if (clip.image.type === "jpeg") { extension = ".jpg"; } } return clipFilename + extension; } get urlDisplay() { if (!this.url) { return null; } if (/^https?:\/\//i.test(this.url)) { let txt = this.url; txt = txt.replace(/^[a-z]{1,4000}:\/\//i, ""); txt = txt.replace(/\/.{0,4000}/, ""); txt = txt.replace(/^www\./i, ""); return txt; } else if (this.url.startsWith("data:")) { return "data:url"; } let txt = this.url; txt = txt.replace(/\?.{0,4000}/, ""); return txt; } get viewUrl() { const url = this.backend + "/" + this.id; return url; } get creatingUrl() { let url = `${this.backend}/creating/${this.id}`; url += `?title=${encodeURIComponent(this.title || "")}`; url += `&url=${encodeURIComponent(this.url)}`; return url; } get jsonUrl() { return this.backend + "/data/" + this.id; } get oembedUrl() { return this.backend + "/oembed?url=" + encodeURIComponent(this.viewUrl); } get docTitle() { return this._title; } set docTitle(val) { assert(val === null || typeof val === "string", "Bad docTitle:", val); this._title = val; } get openGraph() { return this._openGraph || null; } set openGraph(val) { assert(val === null || typeof val === "object", "Bad openGraph:", val); if (val) { assert( checkObject(val, [], this._OPENGRAPH_PROPERTIES), "Bad attr to openGraph:", Object.keys(val) ); this._openGraph = val; } else { this._openGraph = null; } } get twitterCard() { return this._twitterCard || null; } set twitterCard(val) { assert(val === null || typeof val === "object", "Bad twitterCard:", val); if (val) { assert( checkObject(val, [], this._TWITTERCARD_PROPERTIES), "Bad attr to twitterCard:", Object.keys(val) ); this._twitterCard = val; } else { this._twitterCard = null; } } get userTitle() { return this._userTitle; } set userTitle(val) { assert(val === null || typeof val === "string", "Bad userTitle:", val); this._userTitle = val; } get title() { // FIXME: we shouldn't support both openGraph.title and ogTitle const ogTitle = this.openGraph && this.openGraph.title; const twitterTitle = this.twitterCard && this.twitterCard.title; let title = this.userTitle || ogTitle || twitterTitle || this.docTitle || this.url; if (Array.isArray(title)) { title = title[0]; } if (!title) { title = "Screenshot"; } return title; } get createdDate() { return this._createdDate; } set createdDate(val) { assert(val === null || typeof val === "number", "Bad createdDate:", val); this._createdDate = val; } clipNames() { const names = Object.getOwnPropertyNames(this._clips); names.sort(function (a, b) { return a.sortOrder < b.sortOrder ? 1 : 0; }); return names; } getClip(name) { return this._clips[name]; } addClip(val) { const name = makeRandomId(); this.setClip(name, val); return name; } setClip(name, val) { const clip = new this.Clip(this, name, val); this._clips[name] = clip; } delClip(name) { if (!this._clips[name]) { throw new Error("No existing clip with id: " + name); } delete this._clips[name]; } delAllClips() { this._clips = {}; } biggestClipSortOrder() { let biggest = 0; for (const clipId in this._clips) { biggest = Math.max(biggest, this._clips[clipId].sortOrder); } return biggest; } updateClipUrl(clipId, clipUrl) { const clip = this.getClip(clipId); if (clip && clip.image) { clip.image.url = clipUrl; } else { console.warn("Tried to update the url of a clip with no image:", clip); } } get siteName() { return this._siteName || null; } set siteName(val) { assert(typeof val === "string" || !val); this._siteName = val; } get documentSize() { return this._documentSize; } set documentSize(val) { assert(typeof val === "object" || !val); if (val) { assert( checkObject( val, ["height", "width"], "Bad attr to documentSize:", Object.keys(val) ) ); assert(typeof val.height === "number"); assert(typeof val.width === "number"); this._documentSize = val; } else { this._documentSize = null; } } get thumbnail() { return this._thumbnail; } set thumbnail(val) { assert(typeof val === "string" || !val); if (val) { assert(isUrl(val)); this._thumbnail = val; } else { this._thumbnail = null; } } get abTests() { return this._abTests; } set abTests(val) { if (val === null || val === undefined) { this._abTests = null; return; } assert( typeof val === "object", "abTests should be an object, not:", typeof val ); assert(!Array.isArray(val), "abTests should not be an Array"); for (const name in val) { assert( val[name] && typeof val[name] === "string", `abTests.${name} should be a string:`, typeof val[name] ); } this._abTests = val; } get firefoxChannel() { return this._firefoxChannel; } set firefoxChannel(val) { if (val === null || val === undefined) { this._firefoxChannel = null; return; } assert( typeof val === "string", "firefoxChannel should be a string, not:", typeof val ); this._firefoxChannel = val; } } AbstractShot.prototype.REGULAR_ATTRS = ` origin fullUrl docTitle userTitle createdDate images siteName openGraph twitterCard documentSize thumbnail abTests firefoxChannel `.split(/\s+/g); // Attributes that will be accepted in the constructor, but ignored/dropped AbstractShot.prototype.DEPRECATED_ATTRS = ` microdata history ogTitle createdDevice head body htmlAttrs bodyAttrs headAttrs readable hashtags comments showPage isPublic resources url fullScreenThumbnail favicon `.split(/\s+/g); AbstractShot.prototype.RECALL_ATTRS = ` url docTitle userTitle createdDate openGraph twitterCard images thumbnail `.split(/\s+/g); AbstractShot.prototype._OPENGRAPH_PROPERTIES = ` title type url image audio description determiner locale site_name video image:secure_url image:type image:width image:height video:secure_url video:type video:width image:height audio:secure_url audio:type article:published_time article:modified_time article:expiration_time article:author article:section article:tag book:author book:isbn book:release_date book:tag profile:first_name profile:last_name profile:username profile:gender `.split(/\s+/g); AbstractShot.prototype._TWITTERCARD_PROPERTIES = ` card site title description image player player:width player:height player:stream player:stream:content_type `.split(/\s+/g); /** Represents one found image in the document (not a clip) */ class _Image { // FIXME: either we have to notify the shot of updates, or make // this read-only constructor(json) { assert(typeof json === "object", "Clip Image given a non-object", json); assert( checkObject(json, ["url"], ["dimensions", "title", "alt"]), "Bad attrs for Image:", Object.keys(json) ); assert(isUrl(json.url), "Bad Image url:", json.url); this.url = json.url; assert( !json.dimensions || (typeof json.dimensions.x === "number" && typeof json.dimensions.y === "number"), "Bad Image dimensions:", json.dimensions ); this.dimensions = json.dimensions; assert( typeof json.title === "string" || !json.title, "Bad Image title:", json.title ); this.title = json.title; assert( typeof json.alt === "string" || !json.alt, "Bad Image alt:", json.alt ); this.alt = json.alt; } toJSON() { return jsonify(this, ["url"], ["dimensions"]); } } AbstractShot.prototype.Image = _Image; /** Represents a clip, either a text or image clip */ class _Clip { constructor(shot, id, json) { this._shot = shot; assert( checkObject(json, ["createdDate", "image"], ["sortOrder"]), "Bad attrs for Clip:", Object.keys(json) ); assert(typeof id === "string" && id, "Bad Clip id:", id); this._id = id; this.createdDate = json.createdDate; if ("sortOrder" in json) { assert( typeof json.sortOrder === "number" || !json.sortOrder, "Bad Clip sortOrder:", json.sortOrder ); } if ("sortOrder" in json) { this.sortOrder = json.sortOrder; } else { const biggestOrder = shot.biggestClipSortOrder(); this.sortOrder = biggestOrder + 100; } this.image = json.image; } toString() { return `[Shot Clip id=${this.id} sortOrder=${this.sortOrder} image ${this.image.dimensions.x}x${this.image.dimensions.y}]`; } toJSON() { return jsonify(this, ["createdDate"], ["sortOrder", "image"]); } get id() { return this._id; } get createdDate() { return this._createdDate; } set createdDate(val) { assert(typeof val === "number" || !val, "Bad Clip createdDate:", val); this._createdDate = val; } get image() { return this._image; } set image(image) { if (!image) { this._image = undefined; return; } assert( checkObject( image, ["url"], ["dimensions", "text", "location", "captureType", "type"] ), "Bad attrs for Clip Image:", Object.keys(image) ); assert(isValidClipImageUrl(image.url), "Bad Clip image URL:", image.url); assert( image.captureType === "madeSelection" || image.captureType === "selection" || image.captureType === "visible" || image.captureType === "auto" || image.captureType === "fullPage" || image.captureType === "fullPageTruncated" || !image.captureType, "Bad image.captureType:", image.captureType ); assert( typeof image.text === "string" || !image.text, "Bad Clip image text:", image.text ); if (image.dimensions) { assert( typeof image.dimensions.x === "number" && typeof image.dimensions.y === "number", "Bad Clip image dimensions:", image.dimensions ); } if (image.type) { assert( image.type === "png" || image.type === "jpeg", "Unexpected image type:", image.type ); } assert( image.location && typeof image.location.left === "number" && typeof image.location.right === "number" && typeof image.location.top === "number" && typeof image.location.bottom === "number", "Bad Clip image pixel location:", image.location ); if ( image.location.topLeftElement || image.location.topLeftOffset || image.location.bottomRightElement || image.location.bottomRightOffset ) { assert( typeof image.location.topLeftElement === "string" && image.location.topLeftOffset && typeof image.location.topLeftOffset.x === "number" && typeof image.location.topLeftOffset.y === "number" && typeof image.location.bottomRightElement === "string" && image.location.bottomRightOffset && typeof image.location.bottomRightOffset.x === "number" && typeof image.location.bottomRightOffset.y === "number", "Bad Clip image element location:", image.location ); } this._image = image; } isDataUrl() { if (this.image) { return this.image.url.startsWith("data:"); } return false; } get sortOrder() { return this._sortOrder || null; } set sortOrder(val) { assert(typeof val === "number" || !val, "Bad Clip sortOrder:", val); this._sortOrder = val; } } AbstractShot.prototype.Clip = _Clip; if (typeof exports !== "undefined") { exports.AbstractShot = AbstractShot; exports.originFromUrl = originFromUrl; exports.isValidClipImageUrl = isValidClipImageUrl; } return exports; })(); null; PK !<®-ìW„„build/thumbnailGenerator.js/* 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/. */ this.thumbnailGenerator = (function () { let exports = {}; // This is used in webextension/background/takeshot.js, // server/src/pages/shot/controller.js, and // server/scr/pages/shotindex/view.js. It is used in a browser // environment. // Resize down 1/2 at a time produces better image quality. // Not quite as good as using a third-party filter (which will be // slower), but good enough. const maxResizeScaleFactor = 0.5; // The shot will be scaled or cropped down to 210px on x, and cropped or // scaled down to a maximum of 280px on y. // x: 210 // y: <= 280 const maxThumbnailWidth = 210; const maxThumbnailHeight = 280; /** * @param {int} imageHeight Height in pixels of the original image. * @param {int} imageWidth Width in pixels of the original image. * @returns {width, height, scaledX, scaledY} */ function getThumbnailDimensions(imageWidth, imageHeight) { const displayAspectRatio = 3 / 4; const imageAspectRatio = imageWidth / imageHeight; let thumbnailImageWidth, thumbnailImageHeight; let scaledX, scaledY; if (imageAspectRatio > displayAspectRatio) { // "Landscape" mode // Scale on y, crop on x const yScaleFactor = imageHeight > maxThumbnailHeight ? maxThumbnailHeight / imageHeight : 1.0; thumbnailImageHeight = scaledY = Math.round(imageHeight * yScaleFactor); scaledX = Math.round(imageWidth * yScaleFactor); thumbnailImageWidth = Math.min(scaledX, maxThumbnailWidth); } else { // "Portrait" mode // Scale on x, crop on y const xScaleFactor = imageWidth > maxThumbnailWidth ? maxThumbnailWidth / imageWidth : 1.0; thumbnailImageWidth = scaledX = Math.round(imageWidth * xScaleFactor); scaledY = Math.round(imageHeight * xScaleFactor); // The CSS could widen the image, in which case we crop more off of y. thumbnailImageHeight = Math.min( scaledY, maxThumbnailHeight, maxThumbnailHeight / (maxThumbnailWidth / imageWidth) ); } return { width: thumbnailImageWidth, height: thumbnailImageHeight, scaledX, scaledY, }; } /** * @param {dataUrl} String Data URL of the original image. * @param {int} imageHeight Height in pixels of the original image. * @param {int} imageWidth Width in pixels of the original image. * @param {String} urlOrBlob 'blob' for a blob, otherwise data url. * @returns A promise that resolves to the data URL or blob of the thumbnail image, or null. */ function createThumbnail(dataUrl, imageWidth, imageHeight, urlOrBlob) { // There's cost associated with generating, transmitting, and storing // thumbnails, so we'll opt out if the image size is below a certain threshold const thumbnailThresholdFactor = 1.2; const thumbnailWidthThreshold = maxThumbnailWidth * thumbnailThresholdFactor; const thumbnailHeightThreshold = maxThumbnailHeight * thumbnailThresholdFactor; if ( imageWidth <= thumbnailWidthThreshold && imageHeight <= thumbnailHeightThreshold ) { // Do not create a thumbnail. return Promise.resolve(null); } const thumbnailDimensions = getThumbnailDimensions(imageWidth, imageHeight); return new Promise((resolve, reject) => { const thumbnailImage = new Image(); let srcWidth = imageWidth; let srcHeight = imageHeight; let destWidth, destHeight; thumbnailImage.onload = function () { destWidth = Math.round(srcWidth * maxResizeScaleFactor); destHeight = Math.round(srcHeight * maxResizeScaleFactor); if ( destWidth <= thumbnailDimensions.scaledX || destHeight <= thumbnailDimensions.scaledY ) { srcWidth = Math.round( srcWidth * (thumbnailDimensions.width / thumbnailDimensions.scaledX) ); srcHeight = Math.round( srcHeight * (thumbnailDimensions.height / thumbnailDimensions.scaledY) ); destWidth = thumbnailDimensions.width; destHeight = thumbnailDimensions.height; } const thumbnailCanvas = document.createElement("canvas"); thumbnailCanvas.width = destWidth; thumbnailCanvas.height = destHeight; const ctx = thumbnailCanvas.getContext("2d"); ctx.imageSmoothingEnabled = false; ctx.drawImage( thumbnailImage, 0, 0, srcWidth, srcHeight, 0, 0, destWidth, destHeight ); if ( thumbnailCanvas.width <= thumbnailDimensions.width || thumbnailCanvas.height <= thumbnailDimensions.height ) { if (urlOrBlob === "blob") { thumbnailCanvas.toBlob(blob => { resolve(blob); }); } else { resolve(thumbnailCanvas.toDataURL("image/png")); } return; } srcWidth = destWidth; srcHeight = destHeight; thumbnailImage.src = thumbnailCanvas.toDataURL(); }; thumbnailImage.src = dataUrl; }); } function createThumbnailUrl(shot) { const image = shot.getClip(shot.clipNames()[0]).image; if (!image.url) { return Promise.resolve(null); } return createThumbnail( image.url, image.dimensions.x, image.dimensions.y, "dataurl" ); } function createThumbnailBlobFromPromise(shot, blobToUrlPromise) { return blobToUrlPromise.then(dataUrl => { const image = shot.getClip(shot.clipNames()[0]).image; return createThumbnail( dataUrl, image.dimensions.x, image.dimensions.y, "blob" ); }); } if (typeof exports !== "undefined") { exports.getThumbnailDimensions = getThumbnailDimensions; exports.createThumbnailUrl = createThumbnailUrl; exports.createThumbnailBlobFromPromise = createThumbnailBlobFromPromise; } return exports; })(); null; PK !<{ÊÊ catcher.js/* 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/. */ "use strict"; // eslint-disable-next-line no-var var global = this; this.catcher = (function () { const exports = {}; let handler; let queue = []; const log = global.log; exports.unhandled = function (error, info) { if (!error.noReport) { log.error("Unhandled error:", error, info); } const e = makeError(error, info); if (!handler) { queue.push(e); } else { handler(e); } }; /** Turn an exception into an error object */ function makeError(exc, info) { let result; if (exc.fromMakeError) { result = exc; } else { result = { fromMakeError: true, name: exc.name || "ERROR", message: String(exc), stack: exc.stack, }; for (const attr in exc) { result[attr] = exc[attr]; } } if (info) { for (const attr of Object.keys(info)) { result[attr] = info[attr]; } } return result; } /** Wrap the function, and if it raises any exceptions then call unhandled() */ exports.watchFunction = function watchFunction(func, quiet) { return function () { try { return func.apply(this, arguments); } catch (e) { if (!quiet) { exports.unhandled(e); } throw e; } }; }; exports.watchPromise = function watchPromise(promise, quiet) { return promise.catch(e => { if (quiet) { if (!e.noReport) { log.debug("------Error in promise:", e); log.debug(e.stack); } } else { if (!e.noReport) { log.error("------Error in promise:", e); log.error(e.stack); } exports.unhandled(makeError(e)); } throw e; }); }; exports.registerHandler = function (h) { if (handler) { log.error("registerHandler called after handler was already registered"); return; } handler = h; for (const error of queue) { handler(error); } queue = []; }; return exports; })(); null; PK !<u·(( clipboard.js/* 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, assertIsBlankDocument, browser */ "use strict"; this.clipboard = (function () { const exports = {}; exports.copy = function (text) { return new Promise((resolve, reject) => { const element = document.createElement("iframe"); element.src = browser.runtime.getURL("blank.html"); // We can't actually hide the iframe while copying, but we can make // it close to invisible: element.style.opacity = "0"; element.style.width = "1px"; element.style.height = "1px"; element.style.display = "block"; element.addEventListener( "load", catcher.watchFunction(() => { try { const doc = element.contentDocument; assertIsBlankDocument(doc); const el = doc.createElement("textarea"); doc.body.appendChild(el); el.value = text; if (!text) { const exc = new Error("Clipboard copy given empty text"); exc.noPopup = true; catcher.unhandled(exc); } el.select(); if (doc.activeElement !== el) { const unhandledTag = doc.activeElement ? doc.activeElement.tagName : "No active element"; const exc = new Error("Clipboard el.select failed"); exc.activeElement = unhandledTag; exc.noPopup = true; catcher.unhandled(exc); } const copied = doc.execCommand("copy"); if (!copied) { catcher.unhandled(new Error("Clipboard copy failed")); } el.remove(); resolve(copied); } finally { element.remove(); } }), { once: true } ); document.body.appendChild(element); }); }; return exports; })(); null; PK !<퉇JdomainFromUrl.js/* 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/. */ /** Returns the domain of a URL, but safely and in ASCII; URLs without domains (such as about:blank) return the scheme, Unicode domains get stripped down to ASCII */ "use strict"; this.domainFromUrl = (function () { return function urlDomainForId(location) { // eslint-disable-line no-unused-vars let domain = location.hostname; if (!domain) { domain = location.origin.split(":")[0]; if (!domain) { domain = "unknown"; } } if (domain.search(/^[a-z0-9._-]{1,1000}$/i) === -1) { // Probably a unicode domain; we could use punycode but it wouldn't decode // well in the URL anyway. Instead we'll punt. domain = domain.replace(/[^a-z0-9._-]/gi, ""); if (!domain) { domain = "site"; } } return domain; }; })(); null; PK ! { let observer = (subject, topic, data) => { let type = data; fire.sync(type); }; Services.obs.addObserver(observer, TOPIC); return () => { Services.obs.removeObserver(observer, TOPIC); }; }, }).api(), }, }, }; } }; PK !<šÃLq‚‚#experiments/screenshots/schema.json[ { "namespace": "experiments.screenshots", "description": "Firefox Screenshots internal API", "functions": [ { "name": "getUpdateChannel", "type": "function", "description": "Returns the Firefox channel (AppConstants.MOZ_UPDATE_CHANNEL)", "parameters": [], "async": true }, { "name": "isHistoryEnabled", "type": "function", "description": "Returns the value of the 'places.history.enabled' preference", "parameters": [], "async": true } ], "events": [ { "name": "onScreenshotCommand", "type": "function", "description": "Fired when the command event for the Screenshots menuitem is fired.", "parameters": [ { "name": "isContextMenuClick", "type": "boolean" } ] } ] } ] PK !", "homepage_url": "https://github.com/mozilla-services/screenshots", "incognito": "spanning", "browser_specific_settings": { "gecko": { "id": "screenshots@mozilla.org", "strict_min_version": "57.0a1" } }, "l10n_resources": ["browser/screenshots.ftl"], "background": { "scripts": ["background/startBackground.js"] }, "content_scripts": [ { "matches": ["https://screenshots.firefox.com/*"], "js": [ "log.js", "catcher.js", "selector/callBackground.js", "sitehelper.js" ] } ], "web_accessible_resources": ["blank.html"], "permissions": [ "activeTab", "downloads", "tabs", "storage", "notifications", "clipboardWrite", "contextMenus", "mozillaAddons", "telemetry", "", "https://screenshots.firefox.com/", "resource://pdf.js/", "about:reader*" ], "experiment_apis": { "screenshots": { "schema": "experiments/screenshots/schema.json", "parent": { "scopes": ["addon_parent"], "script": "experiments/screenshots/api.js", "paths": [["experiments", "screenshots"]] } } } } PK !<ÉŽ]   moz.build# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # 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/. with Files("**"): BUG_COMPONENT = ("Firefox", "Screenshots") # This file list is automatically generated by Screenshots' export scripts. # AUTOMATIC INSERTION START FINAL_TARGET_FILES.features["screenshots@mozilla.org"] += [ "assertIsBlankDocument.js", "assertIsTrusted.js", "blank.html", "blobConverters.js", "catcher.js", "clipboard.js", "domainFromUrl.js", "log.js", "manifest.json", "moz.build", "randomString.js", "sitehelper.js", ] FINAL_TARGET_FILES.features["screenshots@mozilla.org"]["background"] += [ "background/analytics.js", "background/communication.js", "background/deviceInfo.js", "background/main.js", "background/selectorLoader.js", "background/senderror.js", "background/startBackground.js", "background/takeshot.js", ] FINAL_TARGET_FILES.features["screenshots@mozilla.org"]["build"] += [ "build/inlineSelectionCss.js", "build/selection.js", "build/shot.js", "build/thumbnailGenerator.js", ] FINAL_TARGET_FILES.features["screenshots@mozilla.org"]["experiments"][ "screenshots" ] += ["experiments/screenshots/api.js", "experiments/screenshots/schema.json"] FINAL_TARGET_FILES.features["screenshots@mozilla.org"]["selector"] += [ "selector/callBackground.js", "selector/documentMetadata.js", "selector/shooter.js", "selector/ui.js", "selector/uicontrol.js", "selector/util.js", ] # AUTOMATIC INSERTION END BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"] PK !<4”ù¶RRrandomString.js/* 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/. */ /* exported randomString */ "use strict"; this.randomString = function randomString(length, chars) { const randomStringChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; chars = chars || randomStringChars; let result = ""; for (let i = 0; i < length; i++) { result += chars[Math.floor(Math.random() * chars.length)]; } return result; }; null; PK !< 2&^~~selector/callBackground.js/* 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 log, browser */ "use strict"; this.callBackground = function callBackground(funcName, ...args) { return browser.runtime.sendMessage({ funcName, args }).then(result => { if (result && result.type === "success") { return result.value; } else if (result && result.type === "error") { const exc = new Error(result.message || "Unknown error"); exc.name = "BackgroundError"; if ("errorCode" in result) { exc.errorCode = result.errorCode; } if ("popupMessage" in result) { exc.popupMessage = result.popupMessage; } throw exc; } else { log.error("Unexpected background result:", result); const exc = new Error( `Bad response type from background page: ${ (result && result.type) || undefined }` ); exc.resultType = result ? result.type || "undefined" : "undefined result"; throw exc; } }); }; null; PK !<÷•ßU U selector/documentMetadata.js/* 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/. */ "use strict"; this.documentMetadata = (function () { function findSiteName() { let el = document.querySelector("meta[property~='og:site_name'][content]"); if (el) { return el.getAttribute("content"); } // nytimes.com uses this property: el = document.querySelector("meta[name='cre'][content]"); if (el) { return el.getAttribute("content"); } return null; } function getOpenGraph() { const openGraph = {}; // If you update this, also update _OPENGRAPH_PROPERTIES in shot.js: const forceSingle = `title type url`.split(" "); const openGraphProperties = ` title type url image audio description determiner locale site_name video image:secure_url image:type image:width image:height video:secure_url video:type video:width image:height audio:secure_url audio:type article:published_time article:modified_time article:expiration_time article:author article:section article:tag book:author book:isbn book:release_date book:tag profile:first_name profile:last_name profile:username profile:gender `.split(/\s+/g); for (const prop of openGraphProperties) { let elems = document.querySelectorAll( `meta[property~='og:${prop}'][content]` ); if (forceSingle.includes(prop) && elems.length > 1) { elems = [elems[0]]; } let value; if (elems.length > 1) { value = []; for (const elem of elems) { const v = elem.getAttribute("content"); if (v) { value.push(v); } } if (!value.length) { value = null; } } else if (elems.length === 1) { value = elems[0].getAttribute("content"); } if (value) { openGraph[prop] = value; } } return openGraph; } function getTwitterCard() { const twitterCard = {}; // If you update this, also update _TWITTERCARD_PROPERTIES in shot.js: const properties = ` card site title description image player player:width player:height player:stream player:stream:content_type `.split(/\s+/g); for (const prop of properties) { const elem = document.querySelector( `meta[name='twitter:${prop}'][content]` ); if (elem) { const value = elem.getAttribute("content"); if (value) { twitterCard[prop] = value; } } } return twitterCard; } return function documentMetadata() { const result = {}; result.docTitle = document.title; result.siteName = findSiteName(); result.openGraph = getOpenGraph(); result.twitterCard = getTwitterCard(); return result; }; })(); null; PK !<þ|µHselector/shooter.js/* 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, browser, documentMetadata, util, uicontrol, ui, catcher */ /* globals 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; let backend; let shotObject; const callBackground = global.callBackground; 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)); }); function hideUIFrame() { ui.iframe.hide(); return Promise.resolve(null); } function screenshotPage(dataUrl, selectedPos, type, screenshotTaskFn) { let promise = Promise.resolve(dataUrl); if (!dataUrl) { promise = callBackground( "screenshotPage", selectedPos.toJSON(), type, window.devicePixelRatio ); } catcher.watchPromise( promise.then(dataLoc => { screenshotTaskFn(dataLoc); }) ); } exports.downloadShot = function (selectedPos, previewDataUrl, type) { const shotPromise = previewDataUrl ? Promise.resolve(previewDataUrl) : hideUIFrame(); catcher.watchPromise( shotPromise.then(dataUrl => { screenshotPage(dataUrl, selectedPos, type, url => { let typeFromDataUrl = blobConverters.getTypeFromDataUrl(url); typeFromDataUrl = typeFromDataUrl ? typeFromDataUrl.split("/", 2)[1] : null; shotObject.delAllClips(); shotObject.addClip({ createdDate: Date.now(), image: { url, type: typeFromDataUrl, location: selectedPos, }, }); ui.triggerDownload(url, shotObject.filename); uicontrol.deactivate(); }); }) ); }; exports.preview = function (selectedPos, type) { catcher.watchPromise( hideUIFrame().then(dataUrl => { screenshotPage(dataUrl, selectedPos, type, url => { ui.iframe.usePreview(); ui.Preview.display(url); }); }) ); }; 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) : hideUIFrame(); catcher.watchPromise( shotPromise.then(dataUrl => { screenshotPage(dataUrl, selectedPos, type, url => { const blob = blobConverters.dataUrlToBlob(url); 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 }); } }; catcher.watchFunction(() => { shotObject = new AbstractShot( backend, randomString(RANDOM_STRING_LENGTH) + "/" + domainFromUrl(location), { origin: shot.originFromUrl(location.href), } ); shotObject.update(documentMetadata()); })(); return exports; })(); null; PK !<œdÉ};q;qselector/ui.js/* 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, log, util, catcher, inlineSelectionCss, callBackground, assertIsTrusted, assertIsBlankDocument, blobConverters */ "use strict"; this.ui = (function () { // eslint-disable-line no-unused-vars const exports = {}; const SAVE_BUTTON_HEIGHT = 50; const { watchFunction } = catcher; exports.isHeader = function (el) { while (el) { if ( el.classList && (el.classList.contains("visible") || el.classList.contains("full-page") || el.classList.contains("cancel-shot")) ) { return true; } el = el.parentNode; } return false; }; const substitutedCss = inlineSelectionCss.replace( /MOZ_EXTENSION([^"]+)/g, (match, filename) => { return browser.runtime.getURL(filename); } ); function makeEl(tagName, className) { if (!iframe.document()) { throw new Error("Attempted makeEl before iframe was initialized"); } const el = iframe.document().createElement(tagName); if (className) { el.className = className; } return el; } function onResize() { if (this.sizeTracking.windowDelayer) { clearTimeout(this.sizeTracking.windowDelayer); } this.sizeTracking.windowDelayer = setTimeout( watchFunction(() => { this.updateElementSize(true); }), 50 ); } function initializeIframe() { const el = document.createElement("iframe"); el.src = browser.runtime.getURL("blank.html"); el.style.zIndex = "99999999999"; el.style.border = "none"; el.style.top = "0"; el.style.left = "0"; el.style.margin = "0"; el.scrolling = "no"; el.style.clip = "auto"; el.style.backgroundColor = "transparent"; el.style.colorScheme = "light"; return el; } const iframeSelection = (exports.iframeSelection = { element: null, addClassName: "", sizeTracking: { timer: null, windowDelayer: null, lastHeight: null, lastWidth: null, }, document: null, window: null, display(installHandlerOnDocument) { return new Promise((resolve, reject) => { if (!this.element) { this.element = initializeIframe(); this.element.id = "firefox-screenshots-selection-iframe"; this.element.style.display = "none"; this.element.style.setProperty("max-width", "none", "important"); this.element.style.setProperty("max-height", "none", "important"); this.element.style.setProperty("position", "absolute", "important"); this.element.setAttribute("role", "dialog"); this.updateElementSize(); this.element.addEventListener( "load", watchFunction(() => { this.document = this.element.contentDocument; this.window = this.element.contentWindow; assertIsBlankDocument(this.document); // eslint-disable-next-line no-unsanitized/property this.document.documentElement.innerHTML = ` `; installHandlerOnDocument(this.document); if (this.addClassName) { this.document.body.className = this.addClassName; } this.document.documentElement.dir = browser.i18n.getMessage("@@bidi_dir"); this.document.documentElement.lang = browser.i18n.getMessage("@@ui_locale"); resolve(); }), { once: true } ); document.body.appendChild(this.element); } else { resolve(); } }); }, hide() { this.element.style.display = "none"; this.stopSizeWatch(); }, unhide() { this.updateElementSize(); this.element.style.display = "block"; this.initSizeWatch(); this.element.focus(); }, updateElementSize(force) { // Note: if someone sizes down the page, then the iframe will keep the // document from naturally shrinking. We use force to temporarily hide // the element so that we can tell if the document shrinks const visible = this.element.style.display !== "none"; if (force && visible) { this.element.style.display = "none"; } const height = Math.max( document.documentElement.clientHeight, document.body.clientHeight, document.documentElement.scrollHeight, document.body.scrollHeight ); if (height !== this.sizeTracking.lastHeight) { this.sizeTracking.lastHeight = height; this.element.style.height = height + "px"; } // Do not use window.innerWidth since that includes the width of the // scroll bar. const width = Math.max( document.documentElement.clientWidth, document.body.clientWidth, document.documentElement.scrollWidth, document.body.scrollWidth ); if (width !== this.sizeTracking.lastWidth) { this.sizeTracking.lastWidth = width; this.element.style.width = width + "px"; // Since this frame has an absolute position relative to the parent // document, if the parent document's body has a relative position and // left and/or top not at 0, then the left and/or top of the parent // document's body is not at (0, 0) of the viewport. That makes the // frame shifted relative to the viewport. These margins negates that. if (window.getComputedStyle(document.body).position === "relative") { const docBoundingRect = document.documentElement.getBoundingClientRect(); const bodyBoundingRect = document.body.getBoundingClientRect(); this.element.style.marginLeft = `-${ bodyBoundingRect.left - docBoundingRect.left }px`; this.element.style.marginTop = `-${ bodyBoundingRect.top - docBoundingRect.top }px`; } } if (force && visible) { this.element.style.display = "block"; } }, initSizeWatch() { this.stopSizeWatch(); this.sizeTracking.timer = setInterval( watchFunction(this.updateElementSize.bind(this)), 2000 ); window.addEventListener("resize", this.onResize, true); }, stopSizeWatch() { if (this.sizeTracking.timer) { clearTimeout(this.sizeTracking.timer); this.sizeTracking.timer = null; } if (this.sizeTracking.windowDelayer) { clearTimeout(this.sizeTracking.windowDelayer); this.sizeTracking.windowDelayer = null; } this.sizeTracking.lastHeight = this.sizeTracking.lastWidth = null; window.removeEventListener("resize", this.onResize, true); }, getElementFromPoint(x, y) { this.element.style.pointerEvents = "none"; let el; try { el = document.elementFromPoint(x, y); } finally { this.element.style.pointerEvents = ""; } return el; }, remove() { this.stopSizeWatch(); util.removeNode(this.element); this.element = this.document = this.window = null; }, }); iframeSelection.onResize = watchFunction( assertIsTrusted(onResize.bind(iframeSelection)), true ); const iframePreSelection = (exports.iframePreSelection = { element: null, document: null, window: null, display(installHandlerOnDocument, standardOverlayCallbacks) { return new Promise((resolve, reject) => { if (!this.element) { this.element = initializeIframe(); this.element.id = "firefox-screenshots-preselection-iframe"; this.element.style.setProperty("position", "fixed", "important"); this.element.style.width = "100%"; this.element.style.height = "100%"; this.element.style.setProperty("max-width", "none", "important"); this.element.style.setProperty("max-height", "none", "important"); this.element.setAttribute("role", "dialog"); this.element.addEventListener( "load", watchFunction(() => { this.document = this.element.contentDocument; this.window = this.element.contentWindow; assertIsBlankDocument(this.document); // eslint-disable-next-line no-unsanitized/property this.document.documentElement.innerHTML = `
`; installHandlerOnDocument(this.document); if (this.addClassName) { this.document.body.className = this.addClassName; } this.document.documentElement.dir = browser.i18n.getMessage("@@bidi_dir"); this.document.documentElement.lang = browser.i18n.getMessage("@@ui_locale"); const overlay = this.document.querySelector(".preview-overlay"); overlay .querySelector(".visible") .addEventListener( "click", watchFunction( assertIsTrusted(standardOverlayCallbacks.onClickVisible) ) ); overlay .querySelector(".full-page") .addEventListener( "click", watchFunction( assertIsTrusted(standardOverlayCallbacks.onClickFullPage) ) ); overlay .querySelector(".cancel-shot") .addEventListener( "click", watchFunction( assertIsTrusted(standardOverlayCallbacks.onClickCancel) ) ); resolve(); }), { once: true } ); document.body.appendChild(this.element); } else { resolve(); } }); }, hide() { window.removeEventListener( "scroll", watchFunction(assertIsTrusted(this.onScroll)) ); window.removeEventListener("resize", this.onResize, true); if (this.element) { this.element.style.display = "none"; } }, unhide() { window.addEventListener( "scroll", watchFunction(assertIsTrusted(this.onScroll)) ); window.addEventListener("resize", this.onResize, true); this.element.style.display = "block"; this.element.focus(); }, onScroll() { exports.HoverBox.hide(); }, getElementFromPoint(x, y) { this.element.style.pointerEvents = "none"; let el; try { el = document.elementFromPoint(x, y); } finally { this.element.style.pointerEvents = ""; } return el; }, remove() { this.hide(); util.removeNode(this.element); this.element = this.document = this.window = null; }, }); let msgsPromise = callBackground("getStrings", [ "screenshots-cancel-button", "screenshots-copy-button-tooltip", "screenshots-download-button-tooltip", "screenshots-copy-button", "screenshots-download-button", ]); const iframePreview = (exports.iframePreview = { element: null, document: null, window: null, display(installHandlerOnDocument, standardOverlayCallbacks) { return new Promise((resolve, reject) => { if (!this.element) { this.element = initializeIframe(); this.element.id = "firefox-screenshots-preview-iframe"; this.element.style.display = "none"; this.element.style.setProperty("position", "fixed", "important"); this.element.style.height = "100%"; this.element.style.width = "100%"; this.element.style.setProperty("max-width", "none", "important"); this.element.style.setProperty("max-height", "none", "important"); this.element.setAttribute("role", "dialog"); this.element.onload = watchFunction(() => { msgsPromise.then(([cancelTitle, copyTitle, downloadTitle]) => { assertIsBlankDocument(this.element.contentDocument); this.document = this.element.contentDocument; this.window = this.element.contentWindow; // eslint-disable-next-line no-unsanitized/property this.document.documentElement.innerHTML = `
`; installHandlerOnDocument(this.document); this.document.documentElement.dir = browser.i18n.getMessage("@@bidi_dir"); this.document.documentElement.lang = browser.i18n.getMessage("@@ui_locale"); const overlay = this.document.querySelector(".preview-overlay"); overlay .querySelector(".highlight-button-copy") .addEventListener( "click", watchFunction( assertIsTrusted(standardOverlayCallbacks.onCopyPreview) ) ); overlay .querySelector(".highlight-button-download") .addEventListener( "click", watchFunction( assertIsTrusted(standardOverlayCallbacks.onDownloadPreview) ) ); overlay .querySelector(".highlight-button-cancel") .addEventListener( "click", watchFunction( assertIsTrusted(standardOverlayCallbacks.cancel) ) ); resolve(); }); }); document.body.appendChild(this.element); } else { resolve(); } }); }, hide() { if (this.element) { this.element.style.display = "none"; } }, unhide() { this.element.style.display = "block"; this.element.focus(); }, showLoader() { this.document.body.querySelector(".preview-image").style.display = "none"; this.document.body.querySelector(".loader").style.display = ""; }, remove() { this.hide(); util.removeNode(this.element); this.element = null; this.document = null; }, }); iframePreSelection.onResize = watchFunction( onResize.bind(iframePreSelection), true ); const iframe = (exports.iframe = { currentIframe: iframePreSelection, display(installHandlerOnDocument, standardOverlayCallbacks) { return iframeSelection .display(installHandlerOnDocument) .then(() => iframePreSelection.display( installHandlerOnDocument, standardOverlayCallbacks ) ) .then(() => iframePreview.display( installHandlerOnDocument, standardOverlayCallbacks ) ); }, hide() { this.currentIframe.hide(); }, unhide() { this.currentIframe.unhide(); }, showLoader() { if (this.currentIframe.showLoader) { this.currentIframe.showLoader(); this.currentIframe.unhide(); } }, getElementFromPoint(x, y) { return this.currentIframe.getElementFromPoint(x, y); }, remove() { iframeSelection.remove(); iframePreSelection.remove(); iframePreview.remove(); }, getContentWindow() { return this.currentIframe.element.contentWindow; }, document() { return this.currentIframe.document; }, useSelection() { if ( this.currentIframe === iframePreSelection || this.currentIframe === iframePreview ) { this.hide(); } this.currentIframe = iframeSelection; this.unhide(); }, usePreSelection() { if ( this.currentIframe === iframeSelection || this.currentIframe === iframePreview ) { this.hide(); } this.currentIframe = iframePreSelection; this.unhide(); }, usePreview() { if ( this.currentIframe === iframeSelection || this.currentIframe === iframePreSelection ) { this.hide(); } this.currentIframe = iframePreview; this.unhide(); }, }); const movements = [ "topLeft", "top", "topRight", "left", "right", "bottomLeft", "bottom", "bottomRight", ]; /** Creates the selection box */ exports.Box = { async display(pos, callbacks) { await this._createEl(); if (callbacks !== undefined && callbacks.cancel) { // We use onclick here because we don't want addEventListener // to add multiple event handlers to the same button this.cancel.onclick = watchFunction(assertIsTrusted(callbacks.cancel)); this.cancel.style.display = ""; } else { this.cancel.style.display = "none"; } if (callbacks !== undefined && callbacks.download) { this.download.removeAttribute("disabled"); this.download.onclick = watchFunction( assertIsTrusted(e => { this.download.setAttribute("disabled", true); callbacks.download(e); e.preventDefault(); e.stopPropagation(); return false; }) ); this.download.style.display = ""; } else { this.download.style.display = "none"; } if (callbacks !== undefined && callbacks.copy) { this.copy.removeAttribute("disabled"); this.copy.onclick = watchFunction( assertIsTrusted(e => { this.copy.setAttribute("disabled", true); callbacks.copy(e); e.preventDefault(); e.stopPropagation(); }) ); this.copy.style.display = ""; } else { this.copy.style.display = "none"; } const winBottom = window.innerHeight; const pageYOffset = window.pageYOffset; if (pos.right - pos.left < 78 || pos.bottom - pos.top < 78) { this.el.classList.add("small-selection"); } else { this.el.classList.remove("small-selection"); } // if the selection bounding box is w/in SAVE_BUTTON_HEIGHT px of the bottom of // the window, flip controls into the box if (pos.bottom > winBottom + pageYOffset - SAVE_BUTTON_HEIGHT) { this.el.classList.add("bottom-selection"); } else { this.el.classList.remove("bottom-selection"); } if (pos.right < 200) { this.el.classList.add("left-selection"); } else { this.el.classList.remove("left-selection"); } this.el.style.top = `${pos.top}px`; this.el.style.left = `${pos.left}px`; this.el.style.height = `${pos.bottom - pos.top}px`; this.el.style.width = `${pos.right - pos.left}px`; this.bgTop.style.top = "0px"; this.bgTop.style.height = `${pos.top}px`; this.bgTop.style.left = "0px"; this.bgTop.style.width = "100%"; this.bgBottom.style.top = `${pos.bottom}px`; this.bgBottom.style.height = `calc(100vh - ${pos.bottom}px)`; this.bgBottom.style.left = "0px"; this.bgBottom.style.width = "100%"; this.bgLeft.style.top = `${pos.top}px`; this.bgLeft.style.height = `${pos.bottom - pos.top}px`; this.bgLeft.style.left = "0px"; this.bgLeft.style.width = `${pos.left}px`; this.bgRight.style.top = `${pos.top}px`; this.bgRight.style.height = `${pos.bottom - pos.top}px`; this.bgRight.style.left = `${pos.right}px`; this.bgRight.style.width = `calc(100% - ${pos.right}px)`; }, // used to eventually move the download-only warning // when a user ends scrolling or ends resizing a window delayExecution(delay, cb) { let timer; return function () { if (typeof timer !== "undefined") { clearTimeout(timer); } timer = setTimeout(cb, delay); }; }, remove() { for (const name of ["el", "bgTop", "bgLeft", "bgRight", "bgBottom"]) { if (name in this) { util.removeNode(this[name]); this[name] = null; } } }, async _createEl() { let boxEl = this.el; if (boxEl) { return; } let [cancelTitle, copyTitle, downloadTitle, copyText, downloadText] = await msgsPromise; boxEl = makeEl("div", "highlight"); const buttons = makeEl("div", "highlight-buttons"); const cancel = makeEl("button", "highlight-button-cancel"); const cancelImg = makeEl("img"); cancelImg.src = "chrome://browser/content/screenshots/cancel.svg"; cancel.title = cancelTitle; cancel.appendChild(cancelImg); buttons.appendChild(cancel); const copy = makeEl("button", "highlight-button-copy"); copy.title = copyTitle; const copyImg = makeEl("img"); const copyString = makeEl("span"); copyString.textContent = copyText; copyImg.src = "chrome://browser/content/screenshots/copy.svg"; copy.appendChild(copyImg); copy.appendChild(copyString); buttons.appendChild(copy); const download = makeEl("button", "highlight-button-download"); const downloadImg = makeEl("img"); downloadImg.src = "chrome://browser/content/screenshots/download-white.svg"; download.appendChild(downloadImg); download.append(downloadText); download.title = downloadTitle; buttons.appendChild(download); this.cancel = cancel; this.download = download; this.copy = copy; boxEl.appendChild(buttons); for (const name of movements) { const elTarget = makeEl("div", "mover-target direction-" + name); const elMover = makeEl("div", "mover"); elTarget.appendChild(elMover); boxEl.appendChild(elTarget); } this.bgTop = makeEl("div", "bghighlight"); iframe.document().body.appendChild(this.bgTop); this.bgLeft = makeEl("div", "bghighlight"); iframe.document().body.appendChild(this.bgLeft); this.bgRight = makeEl("div", "bghighlight"); iframe.document().body.appendChild(this.bgRight); this.bgBottom = makeEl("div", "bghighlight"); iframe.document().body.appendChild(this.bgBottom); iframe.document().body.appendChild(boxEl); this.el = boxEl; }, draggerDirection(target) { while (target) { if (target.nodeType === document.ELEMENT_NODE) { if (target.classList.contains("mover-target")) { for (const name of movements) { if (target.classList.contains("direction-" + name)) { return name; } } catcher.unhandled(new Error("Surprising mover element"), { element: target.outerHTML, }); log.warn("Got mover-target that wasn't a specific direction"); } } target = target.parentNode; } return null; }, isSelection(target) { while (target) { if (target.tagName === "BUTTON") { return false; } if ( target.nodeType === document.ELEMENT_NODE && target.classList.contains("highlight") ) { return true; } target = target.parentNode; } return false; }, isControl(target) { while (target) { if ( target.nodeType === document.ELEMENT_NODE && target.classList.contains("highlight-buttons") ) { return true; } target = target.parentNode; } return false; }, el: null, boxTopEl: null, boxLeftEl: null, boxRightEl: null, boxBottomEl: null, }; exports.HoverBox = { el: null, display(rect) { if (!this.el) { this.el = makeEl("div", "hover-highlight"); iframe.document().body.appendChild(this.el); } this.el.style.display = ""; this.el.style.top = rect.top - 1 + "px"; this.el.style.left = rect.left - 1 + "px"; this.el.style.width = rect.right - rect.left + 2 + "px"; this.el.style.height = rect.bottom - rect.top + 2 + "px"; }, hide() { if (this.el) { this.el.style.display = "none"; } }, remove() { util.removeNode(this.el); this.el = null; }, }; exports.PixelDimensions = { el: null, xEl: null, yEl: null, display(xPos, yPos, x, y) { if (!this.el) { this.el = makeEl("div", "pixel-dimensions"); this.xEl = makeEl("div"); this.el.appendChild(this.xEl); this.yEl = makeEl("div"); this.el.appendChild(this.yEl); iframe.document().body.appendChild(this.el); } this.xEl.textContent = Math.round(x); this.yEl.textContent = Math.round(y); this.el.style.top = yPos + 12 + "px"; this.el.style.left = xPos + 12 + "px"; }, remove() { util.removeNode(this.el); this.el = this.xEl = this.yEl = null; }, }; exports.Preview = { display(dataUrl) { const img = makeEl("IMG"); const imgBlob = blobConverters.dataUrlToBlob(dataUrl); img.src = iframe.getContentWindow().URL.createObjectURL(imgBlob); iframe .document() .querySelector(".preview-image-wrapper") .appendChild(img); }, }; /** Removes every UI this module creates */ exports.remove = function () { for (const name in exports) { if (name.startsWith("iframe")) { continue; } if (typeof exports[name] === "object" && exports[name].remove) { exports[name].remove(); } } exports.iframe.remove(); }; exports.triggerDownload = function (url, filename) { return catcher.watchPromise( callBackground("downloadShot", { url, filename }) ); }; exports.unload = exports.remove; return exports; })(); null; PK ! { sendEvent("cancel-shot", "overlay-cancel-button"); exports.deactivate(); }, download: () => { sendEvent("download-shot", "overlay-download-button"); downloadShot(); }, copy: () => { sendEvent("copy-shot", "overlay-copy-button"); copyShot(); }, }; const standardOverlayCallbacks = { cancel: () => { sendEvent("cancel-shot", "cancel-preview-button"); exports.deactivate(); }, onClickCancel: e => { sendEvent("cancel-shot", "cancel-selection-button"); e.preventDefault(); e.stopPropagation(); exports.deactivate(); }, onClickVisible: () => { callBackground("captureTelemetry", "visible"); sendEvent("capture-visible", "selection-button"); selectedPos = new Selection( window.scrollX, window.scrollY, window.scrollX + document.documentElement.clientWidth, window.scrollY + window.innerHeight ); captureType = "visible"; setState("previewing"); }, onClickFullPage: () => { callBackground("captureTelemetry", "full_page"); sendEvent("capture-full-page", "selection-button"); captureType = "fullPage"; const width = getDocumentWidth(); if (width > MAX_PAGE_WIDTH) { captureType = "fullPageTruncated"; } const height = getDocumentHeight(); if (height > MAX_PAGE_HEIGHT) { captureType = "fullPageTruncated"; } selectedPos = new Selection(0, 0, width, height); setState("previewing"); }, onDownloadPreview: () => { sendEvent( `download-${captureType .replace(/([a-z])([A-Z])/g, "$1-$2") .toLowerCase()}`, "download-preview-button" ); downloadShot(); }, onCopyPreview: () => { sendEvent( `copy-${captureType.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()}`, "copy-preview-button" ); copyShot(); }, }; /** Holds all the objects that handle events for each state: */ const stateHandlers = {}; function getState() { return getState.state; } getState.state = "cancel"; function setState(s) { if (!stateHandlers[s]) { throw new Error("Unknown state: " + s); } const cur = getState.state; const handler = stateHandlers[cur]; if (handler.end) { handler.end(); } getState.state = s; if (stateHandlers[s].start) { stateHandlers[s].start(); } } /** Various values that the states use: */ let mousedownPos; let selectedPos; let resizeDirection; let resizeStartPos; let resizeStartSelected; let resizeHasMoved; let mouseupNoAutoselect = false; let autoDetectRect; /** Represents a single x/y point, typically for a mouse click that doesn't have a drag: */ class Pos { constructor(x, y) { this.x = x; this.y = y; } elementFromPoint() { return ui.iframe.getElementFromPoint( this.x - window.pageXOffset, this.y - window.pageYOffset ); } distanceTo(x, y) { return Math.sqrt(Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2)); } } /** ********************************************* * all stateHandlers */ let dataUrl; stateHandlers.previewing = { start() { shooter.preview(selectedPos, captureType); }, }; stateHandlers.crosshairs = { cachedEl: null, start() { selectedPos = mousedownPos = null; this.cachedEl = null; watchPromise( ui.iframe .display(installHandlersOnDocument, standardOverlayCallbacks) .then(() => { ui.iframe.usePreSelection(); ui.Box.remove(); }) ); }, mousemove(event) { ui.PixelDimensions.display( event.pageX, event.pageY, event.pageX, event.pageY ); if ( event.target.classList && !event.target.classList.contains("preview-overlay") ) { // User is hovering over a toolbar button or control autoDetectRect = null; if (this.cachedEl) { this.cachedEl = null; } ui.HoverBox.hide(); return; } let el; if ( event.target.classList && event.target.classList.contains("preview-overlay") ) { // The hover is on the overlay, so we need to figure out the real element el = ui.iframe.getElementFromPoint( event.pageX + window.scrollX - window.pageXOffset, event.pageY + window.scrollY - window.pageYOffset ); const xpos = Math.floor( (10 * (event.pageX - window.innerWidth / 2)) / window.innerWidth ); const ypos = Math.floor( (10 * (event.pageY - window.innerHeight / 2)) / window.innerHeight ); for (let i = 0; i < 2; i++) { const move = `translate(${xpos}px, ${ypos}px)`; event.target.getElementsByClassName("eyeball")[i].style.transform = move; } } else { // The hover is on the element we care about, so we use that el = event.target; } if (this.cachedEl && this.cachedEl === el) { // Still hovering over the same element return; } this.cachedEl = el; this.setAutodetectBasedOnElement(el); }, setAutodetectBasedOnElement(el) { let lastRect; let lastNode; let rect; let attemptExtend = false; let node = el; while (node) { rect = Selection.getBoundingClientRect(node); if (!rect) { rect = lastRect; break; } if (rect.width < MIN_DETECT_WIDTH || rect.height < MIN_DETECT_HEIGHT) { // Avoid infinite loop for elements with zero or nearly zero height, // like non-clearfixed float parents with or without borders. break; } if (rect.width > MAX_DETECT_WIDTH || rect.height > MAX_DETECT_HEIGHT) { // Then the last rectangle is better rect = lastRect; attemptExtend = true; break; } if ( rect.width >= MIN_DETECT_WIDTH && rect.height >= MIN_DETECT_HEIGHT ) { if (!doNotAutoselectTags[node.tagName]) { break; } } lastRect = rect; lastNode = node; node = node.parentNode; } if (rect && node) { const evenBetter = this.evenBetterElement(node, rect); if (evenBetter) { node = lastNode = evenBetter; rect = Selection.getBoundingClientRect(evenBetter); attemptExtend = false; } } if (rect && attemptExtend) { let extendNode = lastNode.nextSibling; while (extendNode) { if (extendNode.nodeType === document.ELEMENT_NODE) { break; } extendNode = extendNode.nextSibling; if (!extendNode) { const parent = lastNode.parentNode; for (let i = 0; i < parent.childNodes.length; i++) { if (parent.childNodes[i] === lastNode) { extendNode = parent.childNodes[i + 1]; } } } } if (extendNode) { const extendSelection = Selection.getBoundingClientRect(extendNode); const extendRect = rect.union(extendSelection); if ( extendRect.width <= MAX_DETECT_WIDTH && extendRect.height <= MAX_DETECT_HEIGHT ) { rect = extendRect; } } } if ( rect && (rect.width < MIN_DETECT_ABSOLUTE_WIDTH || rect.height < MIN_DETECT_ABSOLUTE_HEIGHT) ) { rect = null; } if (!rect) { ui.HoverBox.hide(); } else { ui.HoverBox.display(rect); } autoDetectRect = rect; }, /** When we find an element, maybe there's one that's just a little bit better... */ evenBetterElement(node, origRect) { let el = node.parentNode; const ELEMENT_NODE = document.ELEMENT_NODE; while (el && el.nodeType === ELEMENT_NODE) { if (!el.getAttribute) { return null; } const role = el.getAttribute("role"); if ( role === "article" || (el.className && typeof el.className === "string" && el.className.search("tweet ") !== -1) ) { const rect = Selection.getBoundingClientRect(el); if (!rect) { return null; } if ( rect.width <= MAX_DETECT_WIDTH && rect.height <= MAX_DETECT_HEIGHT ) { return el; } return null; } el = el.parentNode; } return null; }, mousedown(event) { // FIXME: this is happening but we don't know why, we'll track it now // but avoid popping up messages: if (typeof ui === "undefined") { const exc = new Error("Undefined ui in mousedown"); exc.unloadTime = unloadTime; exc.nowTime = Date.now(); exc.noPopup = true; exc.noReport = true; throw exc; } if (ui.isHeader(event.target)) { return undefined; } // If the pageX is greater than this, then probably it's an attempt to get // to the scrollbar, or an actual scroll, and not an attempt to start the // selection: const maxX = window.innerWidth - SCROLLBAR_WIDTH; if (event.pageX >= maxX) { event.stopPropagation(); event.preventDefault(); return false; } mousedownPos = new Pos( event.pageX + window.scrollX, event.pageY + window.scrollY ); setState("draggingReady"); event.stopPropagation(); event.preventDefault(); return false; }, end() { ui.HoverBox.remove(); ui.PixelDimensions.remove(); }, }; stateHandlers.draggingReady = { minMove: 40, // px minAutoImageWidth: 40, minAutoImageHeight: 40, maxAutoElementWidth: 800, maxAutoElementHeight: 600, start() { ui.iframe.usePreSelection(); ui.Box.remove(); }, mousemove(event) { if (mousedownPos.distanceTo(event.pageX, event.pageY) > this.minMove) { selectedPos = new Selection( mousedownPos.x, mousedownPos.y, event.pageX + window.scrollX, event.pageY + window.scrollY ); mousedownPos = null; setState("dragging"); } }, mouseup(event) { // If we don't get into "dragging" then we attempt an autoselect if (mouseupNoAutoselect) { sendEvent("cancel-selection", "selection-background-mousedown"); setState("crosshairs"); return false; } if (autoDetectRect) { selectedPos = autoDetectRect; selectedPos.x1 += window.scrollX; selectedPos.y1 += window.scrollY; selectedPos.x2 += window.scrollX; selectedPos.y2 += window.scrollY; autoDetectRect = null; mousedownPos = null; ui.iframe.useSelection(); ui.Box.display(selectedPos, standardDisplayCallbacks); sendEvent( "make-selection", "selection-click", eventOptionsForBox(selectedPos) ); setState("selected"); sendEvent("autoselect"); callBackground("captureTelemetry", "element"); } else { sendEvent("no-selection", "no-element-found"); setState("crosshairs"); } return undefined; }, click(event) { this.mouseup(event); }, findGoodEl() { let el = mousedownPos.elementFromPoint(); if (!el) { return null; } const isGoodEl = element => { if (element.nodeType !== document.ELEMENT_NODE) { return false; } if (element.tagName === "IMG") { const rect = element.getBoundingClientRect(); return ( rect.width >= this.minAutoImageWidth && rect.height >= this.minAutoImageHeight ); } const display = window.getComputedStyle(element).display; if (["block", "inline-block", "table"].includes(display)) { return true; // FIXME: not sure if this is useful: // let rect = el.getBoundingClientRect(); // return rect.width <= this.maxAutoElementWidth && rect.height <= this.maxAutoElementHeight; } return false; }; while (el) { if (isGoodEl(el)) { return el; } el = el.parentNode; } return null; }, end() { mouseupNoAutoselect = false; }, }; stateHandlers.dragging = { start() { ui.iframe.useSelection(); ui.Box.display(selectedPos); }, mousemove(event) { selectedPos.x2 = util.truncateX(event.pageX); selectedPos.y2 = util.truncateY(event.pageY); scrollIfByEdge(event.pageX, event.pageY); ui.Box.display(selectedPos); ui.PixelDimensions.display( event.pageX, event.pageY, selectedPos.width, selectedPos.height ); }, mouseup(event) { selectedPos.x2 = util.truncateX(event.pageX); selectedPos.y2 = util.truncateY(event.pageY); ui.Box.display(selectedPos, standardDisplayCallbacks); sendEvent( "make-selection", "selection-drag", eventOptionsForBox({ top: selectedPos.y1, bottom: selectedPos.y2, left: selectedPos.x1, right: selectedPos.x2, }) ); setState("selected"); callBackground("captureTelemetry", "custom"); }, end() { ui.PixelDimensions.remove(); }, }; stateHandlers.selected = { start() { ui.iframe.useSelection(); }, mousedown(event) { const target = event.target; if (target.tagName === "HTML") { // This happens when you click on the scrollbar return undefined; } const direction = ui.Box.draggerDirection(target); if (direction) { sendEvent("start-resize-selection", "handle"); stateHandlers.resizing.startResize(event, direction); } else if (ui.Box.isSelection(target)) { sendEvent("start-move-selection", "selection"); stateHandlers.resizing.startResize(event, "move"); } else if (!ui.Box.isControl(target)) { mousedownPos = new Pos(event.pageX, event.pageY); setState("crosshairs"); } event.preventDefault(); return false; }, }; stateHandlers.resizing = { start() { ui.iframe.useSelection(); selectedPos.sortCoords(); }, startResize(event, direction) { selectedPos.sortCoords(); resizeDirection = direction; resizeStartPos = new Pos(event.pageX, event.pageY); resizeStartSelected = selectedPos.clone(); resizeHasMoved = false; setState("resizing"); }, mousemove(event) { this._resize(event); if (resizeDirection !== "move") { ui.PixelDimensions.display( event.pageX, event.pageY, selectedPos.width, selectedPos.height ); } return false; }, mouseup(event) { this._resize(event); sendEvent("selection-resized"); ui.Box.display(selectedPos, standardDisplayCallbacks); if (resizeHasMoved) { if (resizeDirection === "move") { const startPos = new Pos( resizeStartSelected.left, resizeStartSelected.top ); const endPos = new Pos(selectedPos.left, selectedPos.top); sendEvent( "move-selection", "mouseup", eventOptionsForMove(startPos, endPos) ); } else { sendEvent( "resize-selection", "mouseup", eventOptionsForResize(resizeStartSelected, selectedPos) ); } } else if (resizeDirection === "move") { sendEvent("keep-resize-selection", "mouseup"); } else { sendEvent("keep-move-selection", "mouseup"); } setState("selected"); callBackground("captureTelemetry", "custom"); }, _resize(event) { const diffX = event.pageX - resizeStartPos.x; const diffY = event.pageY - resizeStartPos.y; const movement = movements[resizeDirection]; if (movement[0]) { let moveX = movement[0]; moveX = moveX === "*" ? ["x1", "x2"] : [moveX]; for (const moveDir of moveX) { selectedPos[moveDir] = util.truncateX( resizeStartSelected[moveDir] + diffX ); } } if (movement[1]) { let moveY = movement[1]; moveY = moveY === "*" ? ["y1", "y2"] : [moveY]; for (const moveDir of moveY) { selectedPos[moveDir] = util.truncateY( resizeStartSelected[moveDir] + diffY ); } } if (diffX || diffY) { resizeHasMoved = true; } scrollIfByEdge(event.pageX, event.pageY); ui.Box.display(selectedPos); }, end() { resizeDirection = resizeStartPos = resizeStartSelected = null; selectedPos.sortCoords(); ui.PixelDimensions.remove(); }, }; stateHandlers.cancel = { start() { ui.iframe.hide(); ui.Box.remove(); }, }; function getDocumentWidth() { return Math.max( document.body && document.body.clientWidth, document.documentElement.clientWidth, document.body && document.body.scrollWidth, document.documentElement.scrollWidth ); } function getDocumentHeight() { return Math.max( document.body && document.body.clientHeight, document.documentElement.clientHeight, document.body && document.body.scrollHeight, document.documentElement.scrollHeight ); } function scrollIfByEdge(pageX, pageY) { const top = window.scrollY; const bottom = top + window.innerHeight; const left = window.scrollX; const right = left + window.innerWidth; if (pageY + SCROLL_BY_EDGE >= bottom && bottom < getDocumentHeight()) { window.scrollBy(0, SCROLL_BY_EDGE); } else if (pageY - SCROLL_BY_EDGE <= top) { window.scrollBy(0, -SCROLL_BY_EDGE); } if (pageX + SCROLL_BY_EDGE >= right && right < getDocumentWidth()) { window.scrollBy(SCROLL_BY_EDGE, 0); } else if (pageX - SCROLL_BY_EDGE <= left) { window.scrollBy(-SCROLL_BY_EDGE, 0); } } /** ********************************************* * Selection communication */ exports.activate = function () { if (!document.body) { callBackground("abortStartShot"); const tagName = String(document.documentElement.tagName || "").replace( /[^a-z0-9]/gi, "" ); sendEvent("abort-start-shot", `document-is-${tagName}`); selectorLoader.unloadModules(); return; } if (isFrameset()) { callBackground("abortStartShot"); sendEvent("abort-start-shot", "frame-page"); selectorLoader.unloadModules(); return; } addHandlers(); setState("crosshairs"); }; function isFrameset() { return document.body.tagName === "FRAMESET"; } exports.deactivate = function () { try { sendEvent("internal", "deactivate"); setState("cancel"); selectorLoader.unloadModules(); } catch (e) { log.error("Error in deactivate", e); // Sometimes this fires so late that the document isn't available // We don't care about the exception, so we swallow it here } }; let unloadTime = 0; exports.unload = function () { // Note that ui.unload() will be called on its own unloadTime = Date.now(); removeHandlers(); }; /** ********************************************* * Event handlers */ const primedDocumentHandlers = new Map(); let registeredDocumentHandlers = []; function addHandlers() { ["mouseup", "mousedown", "mousemove", "click"].forEach(eventName => { const fn = watchFunction( assertIsTrusted(function (event) { if (typeof event.button === "number" && event.button !== 0) { // Not a left click return undefined; } if ( event.ctrlKey || event.shiftKey || event.altKey || event.metaKey ) { // Modified click of key return undefined; } const state = getState(); const handler = stateHandlers[state]; if (handler[event.type]) { return handler[event.type](event); } return undefined; }) ); primedDocumentHandlers.set(eventName, fn); }); primedDocumentHandlers.set( "keyup", watchFunction(assertIsTrusted(keyupHandler)) ); primedDocumentHandlers.set( "keydown", watchFunction(assertIsTrusted(keydownHandler)) ); window.document.addEventListener( "visibilitychange", visibilityChangeHandler ); window.addEventListener("beforeunload", beforeunloadHandler); } let mousedownSetOnDocument = false; function installHandlersOnDocument(docObj) { for (const [eventName, handler] of primedDocumentHandlers) { const watchHandler = watchFunction(handler); const useCapture = eventName !== "keyup"; docObj.addEventListener(eventName, watchHandler, useCapture); registeredDocumentHandlers.push({ name: eventName, doc: docObj, handler: watchHandler, useCapture, }); } if (!mousedownSetOnDocument) { const mousedownHandler = primedDocumentHandlers.get("mousedown"); document.addEventListener("mousedown", mousedownHandler, true); registeredDocumentHandlers.push({ name: "mousedown", doc: document, handler: mousedownHandler, useCapture: true, }); mousedownSetOnDocument = true; } } function beforeunloadHandler() { sendEvent("cancel-shot", "tab-load"); exports.deactivate(); } function keydownHandler(event) { // In MacOS, the keyup event for 'c' is not fired when performing cmd+c. if ( event.code === "KeyC" && (event.ctrlKey || event.metaKey) && ["previewing", "selected"].includes(getState.state) ) { catcher.watchPromise( callBackground("getPlatformOs").then(os => { if ( (event.ctrlKey && os !== "mac") || (event.metaKey && os === "mac") ) { sendEvent("copy-shot", "keyboard-copy"); copyShot(); } }) ); } } function keyupHandler(event) { if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) { // unused modifier keys return; } if ((event.key || event.code) === "Escape") { sendEvent("cancel-shot", "keyboard-escape"); exports.deactivate(); } // Enter to trigger Download by default. But if the user tabbed to // select another button, then we do not want this. if ( (event.key || event.code) === "Enter" && getState.state === "selected" && ui.iframe.document().activeElement.tagName === "BODY" ) { sendEvent("download-shot", "keyboard-enter"); downloadShot(); } } function visibilityChangeHandler(event) { // The document is the event target if (event.target.hidden) { sendEvent("internal", "document-hidden"); } } function removeHandlers() { window.removeEventListener("beforeunload", beforeunloadHandler); window.document.removeEventListener( "visibilitychange", visibilityChangeHandler ); for (const { name, doc, handler, useCapture, } of registeredDocumentHandlers) { doc.removeEventListener(name, handler, !!useCapture); } registeredDocumentHandlers = []; } catcher.watchFunction(exports.activate)(); return exports; })(); null; PK !<|ÒµÒç ç selector/util.js/* 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/. */ "use strict"; this.util = (function () { // eslint-disable-line no-unused-vars const exports = {}; /** Removes a node from its document, if it's a node and the node is attached to a parent */ exports.removeNode = function (el) { if (el && el.parentNode) { el.remove(); } }; /** Truncates the X coordinate to the document size */ exports.truncateX = function (x) { const max = Math.max( document.documentElement.clientWidth, document.body.clientWidth, document.documentElement.scrollWidth, document.body.scrollWidth ); if (x < 0) { return 0; } else if (x > max) { return max; } return x; }; /** Truncates the Y coordinate to the document size */ exports.truncateY = function (y) { const max = Math.max( document.documentElement.clientHeight, document.body.clientHeight, document.documentElement.scrollHeight, document.body.scrollHeight ); if (y < 0) { return 0; } else if (y > max) { return max; } return y; }; // Pixels of wiggle the captured region gets in captureSelectedText: const CAPTURE_WIGGLE = 10; const ELEMENT_NODE = document.ELEMENT_NODE; exports.captureEnclosedText = function (box) { const scrollX = window.scrollX; const scrollY = window.scrollY; const text = []; function traverse(el) { let elBox = el.getBoundingClientRect(); elBox = { top: elBox.top + scrollY, bottom: elBox.bottom + scrollY, left: elBox.left + scrollX, right: elBox.right + scrollX, }; if ( elBox.bottom < box.top || elBox.top > box.bottom || elBox.right < box.left || elBox.left > box.right ) { // Totally outside of the box return; } if ( elBox.bottom > box.bottom + CAPTURE_WIGGLE || elBox.top < box.top - CAPTURE_WIGGLE || elBox.right > box.right + CAPTURE_WIGGLE || elBox.left < box.left - CAPTURE_WIGGLE ) { // Partially outside the box for (let i = 0; i < el.childNodes.length; i++) { const child = el.childNodes[i]; if (child.nodeType === ELEMENT_NODE) { traverse(child); } } return; } addText(el); } function addText(el) { let t; if (el.tagName === "IMG") { t = el.getAttribute("alt") || el.getAttribute("title"); } else if (el.tagName === "A") { t = el.innerText; if ( el.getAttribute("href") && !el.getAttribute("href").startsWith("#") ) { t += " (" + el.href + ")"; } } else { t = el.innerText; } if (t) { text.push(t); } } traverse(document.body); if (text.length) { let result = text.join("\n"); result = result.replace(/^\s+/, ""); result = result.replace(/\s+$/, ""); result = result.replace(/[ \t]+\n/g, "\n"); return result; } return null; }; return exports; })(); null; PK !<1?¿ùCC sitehelper.js/* 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, callBackground, content */ /** This is a content script added to all screenshots.firefox.com pages, and allows the site to communicate with the add-on */ "use strict"; this.sitehelper = (function () { catcher.registerHandler(errorObj => { callBackground("reportError", errorObj); }); const capabilities = {}; function registerListener(name, func) { capabilities[name] = name; document.addEventListener(name, func); } function sendCustomEvent(name, detail) { if (typeof detail === "object") { // Note sending an object can lead to security problems, while a string // is safe to transfer: detail = JSON.stringify(detail); } document.dispatchEvent(new CustomEvent(name, { detail })); } registerListener( "delete-everything", catcher.watchFunction(event => { // FIXME: reset some data in the add-on }, false) ); registerListener( "copy-to-clipboard", catcher.watchFunction(event => { catcher.watchPromise(callBackground("copyShotToClipboard", event.detail)); }) ); registerListener( "show-notification", catcher.watchFunction(event => { catcher.watchPromise(callBackground("showNotification", event.detail)); }) ); // Depending on the script loading order, the site might get the addon-present event, // but probably won't - instead the site will ask for that event after it has loaded registerListener( "request-addon-present", catcher.watchFunction(() => { sendCustomEvent("addon-present", capabilities); }) ); sendCustomEvent("addon-present", capabilities); })(); null; PK !<+g¨¹¤assertIsBlankDocument.jsPK !<«õ|jhh¤ÃassertIsTrusted.jsPK !