PK !<¡VÔxxassertIsBlankDocument.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 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.extension.getURL("blank.html")) { const exc = new Error("iframe URL does not match expected blank.html"); exc.foundURL = doc.documentURI; throw exc; } }; null; PK ! 0 && myGaSegment < GA_PORTION; } function flushEvents() { if (pendingEvents.length === 0) { return; } const eventsUrl = `${main.getBackend()}/event`; const deviceId = auth.getDeviceId(); const sendTime = Date.now(); pendingEvents.forEach(event => { event.queueTime = sendTime - event.eventTime; log.info(`sendEvent ${event.event}/${event.action}/${event.label || "none"} ${JSON.stringify(event.options)}`); }); const body = JSON.stringify({deviceId, events: pendingEvents}); const fetchRequest = fetch(eventsUrl, Object.assign({body}, fetchOptions)); fetchWatcher(fetchRequest); pendingEvents = []; } function flushTimings() { if (pendingTimings.length === 0) { return; } const timingsUrl = `${main.getBackend()}/timing`; const deviceId = auth.getDeviceId(); const body = JSON.stringify({deviceId, timings: pendingTimings}); const fetchRequest = fetch(timingsUrl, Object.assign({body}, fetchOptions)); fetchWatcher(fetchRequest); pendingTimings.forEach(t => { log.info(`sendTiming ${t.timingCategory}/${t.timingLabel}/${t.timingVar}: ${t.timingValue}`); }); pendingTimings = []; } function sendTiming(timingLabel, timingVar, timingValue) { // sendTiming is only called in response to sendEvent, so no need to check // the telemetry pref again here. if (!shouldSendEvents()) { return; } const timingCategory = "addon"; pendingTimings.push({ timingCategory, timingLabel, timingVar, timingValue, }); if (!timingsTimeoutHandle) { timingsTimeoutHandle = setTimeout(() => { timingsTimeoutHandle = null; flushTimings(); }, EVENT_BATCH_DURATION); } } exports.sendEvent = function(action, label, options) { const eventCategory = "addon"; if (!telemetryPrefKnown) { log.warn("sendEvent called before we were able to refresh"); return Promise.resolve(); } if (!telemetryEnabled) { log.info(`Cancelled sendEvent ${eventCategory}/${action}/${label || "none"} ${JSON.stringify(options)}`); return Promise.resolve(); } measureTiming(action, label); // Internal-only events are used for measuring time between events, // but aren't submitted to GA. if (action === "internal") { return Promise.resolve(); } if (typeof label === "object" && (!options)) { options = label; label = undefined; } options = options || {}; // Don't send events if in private browsing. if (options.incognito) { return Promise.resolve(); } // Don't include in event data. delete options.incognito; const di = deviceInfo(); options.applicationName = di.appName; options.applicationVersion = di.addonVersion; const abTests = auth.getAbTests(); for (const [gaField, value] of Object.entries(abTests)) { options[gaField] = value; } if (!shouldSendEvents()) { // We don't want to save or send the events anymore return Promise.resolve(); } pendingEvents.push({ eventTime: Date.now(), event: eventCategory, action, label, options, }); if (!eventsTimeoutHandle) { eventsTimeoutHandle = setTimeout(() => { eventsTimeoutHandle = null; flushEvents(); }, EVENT_BATCH_DURATION); } // This function used to return a Promise that was not used at any of the // call sites; doing this simply maintains that interface. return Promise.resolve(); }; exports.incrementCount = function(scalar) { const allowedScalars = ["download", "upload", "copy"]; if (!allowedScalars.includes(scalar)) { const err = `incrementCount passed an unrecognized scalar ${scalar}`; log.warn(err); return Promise.resolve(); } return browser.telemetry.scalarAdd(`screenshots.${scalar}`, 1).catch(err => { log.warn(`incrementCount failed with error: ${err}`); }); }; exports.refreshTelemetryPref = function() { return browser.telemetry.canUpload().then((result) => { telemetryPrefKnown = true; telemetryEnabled = result; }, (error) => { // If there's an error reading the pref, we should assume that we shouldn't send data telemetryPrefKnown = true; telemetryEnabled = false; throw error; }); }; exports.isTelemetryEnabled = function() { catcher.watchPromise(exports.refreshTelemetryPref()); return telemetryEnabled; }; const timingData = new Map(); // Configuration for filtering the sendEvent stream on start/end events. // When start or end events occur, the time is recorded. // When end events occur, the elapsed time is calculated and submitted // via `sendEvent`, where action = "perf-response-time", label = name of rule, // and cd1 value is the elapsed time in milliseconds. // If a cancel event happens between the start and end events, the start time // is deleted. const rules = [{ name: "page-action", start: { action: "start-shot", label: "toolbar-button" }, end: { action: "internal", label: "unhide-preselection-frame" }, cancel: [ { action: "cancel-shot" }, { action: "internal", label: "document-hidden" }, { action: "internal", label: "unhide-onboarding-frame" }, ], }, { name: "context-menu", start: { action: "start-shot", label: "context-menu" }, end: { action: "internal", label: "unhide-preselection-frame" }, cancel: [ { action: "cancel-shot" }, { action: "internal", label: "document-hidden" }, { action: "internal", label: "unhide-onboarding-frame" }, ], }, { name: "page-action-onboarding", start: { action: "start-shot", label: "toolbar-button" }, end: { action: "internal", label: "unhide-onboarding-frame" }, cancel: [ { action: "cancel-shot" }, { action: "internal", label: "document-hidden" }, { action: "internal", label: "unhide-preselection-frame" }, ], }, { name: "context-menu-onboarding", start: { action: "start-shot", label: "context-menu" }, end: { action: "internal", label: "unhide-onboarding-frame" }, cancel: [ { action: "cancel-shot" }, { action: "internal", label: "document-hidden" }, { action: "internal", label: "unhide-preselection-frame" }, ], }, { name: "capture-full-page", start: { action: "capture-full-page" }, end: { action: "internal", label: "unhide-preview-frame" }, cancel: [ { action: "cancel-shot" }, { action: "internal", label: "document-hidden" }, ], }, { name: "capture-visible", start: { action: "capture-visible" }, end: { action: "internal", label: "unhide-preview-frame" }, cancel: [ { action: "cancel-shot" }, { action: "internal", label: "document-hidden" }, ], }, { name: "make-selection", start: { action: "make-selection" }, end: { action: "internal", label: "unhide-selection-frame" }, cancel: [ { action: "cancel-shot" }, { action: "internal", label: "document-hidden" }, ], }, { name: "save-shot", start: { action: "save-shot" }, end: { action: "internal", label: "open-shot-tab" }, cancel: [{ action: "cancel-shot" }, { action: "upload-failed" }], }, { name: "save-visible", start: { action: "save-visible" }, end: { action: "internal", label: "open-shot-tab" }, cancel: [{ action: "cancel-shot" }, { action: "upload-failed" }], }, { name: "save-full-page", start: { action: "save-full-page" }, end: { action: "internal", label: "open-shot-tab" }, cancel: [{ action: "cancel-shot" }, { action: "upload-failed" }], }, { name: "save-full-page-truncated", start: { action: "save-full-page-truncated" }, end: { action: "internal", label: "open-shot-tab" }, cancel: [{ action: "cancel-shot" }, { action: "upload-failed" }], }, { name: "download-shot", start: { action: "download-shot" }, end: { action: "internal", label: "deactivate" }, cancel: [ { action: "cancel-shot" }, { action: "internal", label: "document-hidden" }, ], }, { name: "download-full-page", start: { action: "download-full-page" }, end: { action: "internal", label: "deactivate" }, cancel: [ { action: "cancel-shot" }, { action: "internal", label: "document-hidden" }, ], }, { name: "download-full-page-truncated", start: { action: "download-full-page-truncated" }, end: { action: "internal", label: "deactivate" }, cancel: [ { action: "cancel-shot" }, { action: "internal", label: "document-hidden" }, ], }, { name: "download-visible", start: { action: "download-visible" }, end: { action: "internal", label: "deactivate" }, cancel: [ { action: "cancel-shot" }, { action: "internal", label: "document-hidden" }, ], }]; // Match a filter (action and optional label) against an action and label. function match(filter, action, label) { return filter.label ? filter.action === action && filter.label === label : filter.action === action; } function anyMatches(filters, action, label) { return filters.some(filter => match(filter, action, label)); } function measureTiming(action, label) { rules.forEach(r => { if (anyMatches(r.cancel, action, label)) { delete timingData[r.name]; } else if (match(r.start, action, label)) { timingData[r.name] = Math.round(performance.now()); } else if (timingData[r.name] && match(r.end, action, label)) { const endTime = Math.round(performance.now()); const elapsed = endTime - timingData[r.name]; sendTiming("perf-response-time", r.name, elapsed); delete timingData[r.name]; } }); } function fetchWatcher(request) { request.then(response => { if (response.status === 410 || response.status === 404) { // Gone hasReturnedGone = true; pendingEvents = []; pendingTimings = []; } if (!response.ok) { log.debug(`Error code in event response: ${response.status} ${response.statusText}`); } }).catch(error => { serverFailedResponses--; if (serverFailedResponses <= 0) { log.info(`Server is not responding, no more events will be sent`); pendingEvents = []; pendingTimings = []; } log.debug(`Error event in response: ${error}`); }); } async function init() { const result = await browser.storage.local.get(["myGaSegment"]); if (!result.myGaSegment) { myGaSegment = Math.random(); await browser.storage.local.set({myGaSegment}); } else { myGaSegment = result.myGaSegment; } } init(); return exports; })(); PK !<‘§Ån¯¯background/auth.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 */ /* globals main, makeUuid, deviceInfo, analytics, catcher, buildSettings, communication */ "use strict"; this.auth = (function() { const exports = {}; let registrationInfo; let initialized = false; let authHeader = null; let sentryPublicDSN = null; let abTests = {}; let accountId = null; const fetchStoredInfo = catcher.watchPromise( browser.storage.local.get(["registrationInfo", "abTests"]).then((result) => { if (result.abTests) { abTests = result.abTests; } if (result.registrationInfo) { registrationInfo = result.registrationInfo; } })); function getRegistrationInfo() { if (!registrationInfo) { registrationInfo = generateRegistrationInfo(); log.info("Generating new device authentication ID", registrationInfo); browser.storage.local.set({registrationInfo}); } return registrationInfo; } exports.getDeviceId = function() { return registrationInfo && registrationInfo.deviceId; }; function generateRegistrationInfo() { const info = { deviceId: `anon${makeUuid()}`, secret: makeUuid(), registered: false, }; return info; } function register() { return new Promise((resolve, reject) => { const registerUrl = main.getBackend() + "/api/register"; // TODO: replace xhr with Fetch #2261 const req = new XMLHttpRequest(); req.open("POST", registerUrl); req.setRequestHeader("content-type", "application/json"); req.onload = catcher.watchFunction(() => { if (req.status === 200) { log.info("Registered login"); initialized = true; saveAuthInfo(JSON.parse(req.responseText)); resolve(true); analytics.sendEvent("registered"); } else { analytics.sendEvent("register-failed", `bad-response-${req.status}`); log.warn("Error in response:", req.responseText); const exc = new Error("Bad response: " + req.status); exc.popupMessage = "LOGIN_ERROR"; reject(exc); } }); req.onerror = catcher.watchFunction(() => { analytics.sendEvent("register-failed", "connection-error"); const exc = new Error("Error contacting server"); exc.popupMessage = "LOGIN_CONNECTION_ERROR"; reject(exc); }); req.send(JSON.stringify({ deviceId: registrationInfo.deviceId, secret: registrationInfo.secret, deviceInfo: JSON.stringify(deviceInfo()), })); }); } function login(options) { const { ownershipCheck, noRegister } = options || {}; return new Promise((resolve, reject) => { return fetchStoredInfo.then(() => { const registrationInfo = getRegistrationInfo(); const loginUrl = main.getBackend() + "/api/login"; // TODO: replace xhr with Fetch #2261 const req = new XMLHttpRequest(); req.open("POST", loginUrl); req.onload = catcher.watchFunction(() => { if (req.status === 404) { if (noRegister) { resolve(false); } else { resolve(register()); } } else if (req.status >= 300) { log.warn("Error in response:", req.responseText); const exc = new Error("Could not log in: " + req.status); exc.popupMessage = "LOGIN_ERROR"; analytics.sendEvent("login-failed", `bad-response-${req.status}`); reject(exc); } else if (req.status === 0) { const error = new Error("Could not log in, server unavailable"); error.popupMessage = "LOGIN_CONNECTION_ERROR"; analytics.sendEvent("login-failed", "connection-error"); reject(error); } else { initialized = true; const jsonResponse = JSON.parse(req.responseText); log.info("Screenshots logged in"); analytics.sendEvent("login"); saveAuthInfo(jsonResponse); if (ownershipCheck) { resolve({isOwner: jsonResponse.isOwner}); } else { resolve(true); } } }); req.onerror = catcher.watchFunction(() => { analytics.sendEvent("login-failed", "connection-error"); const exc = new Error("Connection failed"); exc.url = loginUrl; exc.popupMessage = "CONNECTION_ERROR"; reject(exc); }); req.setRequestHeader("content-type", "application/json"); req.send(JSON.stringify({ deviceId: registrationInfo.deviceId, secret: registrationInfo.secret, deviceInfo: JSON.stringify(deviceInfo()), ownershipCheck, })); }); }); } function saveAuthInfo(responseJson) { accountId = responseJson.accountId; if (responseJson.sentryPublicDSN) { sentryPublicDSN = responseJson.sentryPublicDSN; } if (responseJson.authHeader) { authHeader = responseJson.authHeader; if (!registrationInfo.registered) { registrationInfo.registered = true; catcher.watchPromise(browser.storage.local.set({registrationInfo})); } } if (responseJson.abTests) { abTests = responseJson.abTests; catcher.watchPromise(browser.storage.local.set({abTests})); } } exports.maybeLogin = function() { if (!registrationInfo) { return Promise.resolve(); } return exports.authHeaders(); }; exports.authHeaders = function() { let initPromise = Promise.resolve(); if (!initialized) { initPromise = login(); } return initPromise.then(() => { if (authHeader) { return {"x-screenshots-auth": authHeader}; } log.warn("No auth header available"); return {}; }); }; exports.getSentryPublicDSN = function() { return sentryPublicDSN || buildSettings.defaultSentryDsn; }; exports.getAbTests = function() { return abTests; }; exports.isRegistered = function() { return registrationInfo && registrationInfo.registered; }; communication.register("getAuthInfo", (sender, ownershipCheck) => { return fetchStoredInfo.then(() => { // If a device id was never generated, report back accordingly. if (!registrationInfo) { return null; } return exports.authHeaders().then((authHeaders) => { let info = registrationInfo; if (info.registered) { return login({ownershipCheck}).then((result) => { return { isOwner: result && result.isOwner, deviceId: registrationInfo.deviceId, accountId, authHeaders, }; }); } info = Object.assign({authHeaders}, info); return info; }); }); }); 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 !<©ÎT ¬¬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 */ "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 !<Û€Ï%%background/main.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 selectorLoader, analytics, communication, catcher, log, makeUuid, auth, senderror, startBackground, blobConverters buildSettings */ "use strict"; this.main = (function() { const exports = {}; const pasteSymbol = (window.navigator.platform.match(/Mac/i)) ? "\u2318" : "Ctrl"; const { sendEvent, incrementCount } = analytics; const manifest = browser.runtime.getManifest(); let backend; exports.hasAnyShots = function() { return false; }; exports.setBackend = function(newBackend) { backend = newBackend; backend = backend.replace(/\/*$/, ""); }; exports.getBackend = function() { return backend; }; communication.register("getBackend", () => { return backend; }); for (const permission of manifest.permissions) { if (/^https?:\/\//.test(permission)) { exports.setBackend(permission); break; } } function setIconActive(active, tabId) { const path = active ? "icons/icon-highlight-32-v2.svg" : "icons/icon-v2.svg"; browser.pageAction.setIcon({tabId, path}); } function toggleSelector(tab) { return analytics.refreshTelemetryPref() .then(() => selectorLoader.toggle(tab.id)) .then(active => { setIconActive(active, tab.id); return active; }) .catch((error) => { if (error.message && /Missing host permission for the tab/.test(error.message)) { error.noReport = true; } error.popupMessage = "UNSHOOTABLE_PAGE"; throw error; }); } function shouldOpenMyShots(url) { return /^about:(?:newtab|blank|home)/i.test(url) || /^resource:\/\/activity-streams\//i.test(url); } // This is called by startBackground.js, where is registered as a click // handler for the webextension page action. exports.onClicked = catcher.watchFunction((tab) => { _startShotFlow(tab, "toolbar-button"); }); exports.onClickedContextMenu = catcher.watchFunction((info, tab) => { _startShotFlow(tab, "context-menu"); }); exports.onCommand = catcher.watchFunction((tab) => { _startShotFlow(tab, "keyboard-shortcut"); }); const _openMyShots = (tab, inputType) => { catcher.watchPromise(analytics.refreshTelemetryPref().then(() => { sendEvent("goto-myshots", inputType, {incognito: tab.incognito}); })); catcher.watchPromise( auth.maybeLogin() .then(() => browser.tabs.update({url: backend + "/shots"}))); }; const _startShotFlow = (tab, inputType) => { if (!tab) { // Not in a page/tab context, ignore return; } if (!urlEnabled(tab.url)) { senderror.showError({ popupMessage: "UNSHOOTABLE_PAGE", }); return; } else if (shouldOpenMyShots(tab.url)) { _openMyShots(tab, inputType); return; } catcher.watchPromise(toggleSelector(tab) .then(active => { let event = "start-shot"; if (inputType !== "context-menu") { event = active ? "start-shot" : "cancel-shot"; } sendEvent(event, inputType, {incognito: tab.incognito}); }).catch((error) => { throw error; })); }; function urlEnabled(url) { if (shouldOpenMyShots(url)) { return true; } // Allow screenshots on urls related to web pages in reader mode. if (url && url.startsWith("about:reader?url=")) { return true; } if (isShotOrMyShotPage(url) || /^(?:about|data|moz-extension):/i.test(url) || isBlacklistedUrl(url)) { return false; } return true; } function isShotOrMyShotPage(url) { // It's okay to take a shot of any pages except shot pages and My Shots if (!url.startsWith(backend)) { return false; } const path = url.substr(backend.length).replace(/^\/*/, "").replace(/[?#].*/, ""); if (path === "shots") { return true; } if (/^[^/]{1,4000}\/[^/]{1,4000}$/.test(path)) { // Blocks {:id}/{:domain}, but not /, /privacy, etc return true; } return false; } function isBlacklistedUrl(url) { // These specific domains are not allowed for general WebExtension permission reasons // Discussion: https://bugzilla.mozilla.org/show_bug.cgi?id=1310082 // List of domains copied from: https://searchfox.org/mozilla-central/source/browser/app/permissions#18-19 // Note we disable it here to be informative, the security check is done in WebExtension code const badDomains = ["testpilot.firefox.com"]; let domain = url.replace(/^https?:\/\//i, ""); domain = domain.replace(/\/.*/, "").replace(/:.*/, ""); domain = domain.toLowerCase(); return badDomains.includes(domain); } communication.register("getStrings", (sender, ids) => { return getStrings(ids.map(id => ({id}))); }); communication.register("sendEvent", (sender, ...args) => { catcher.watchPromise(sendEvent(...args)); // We don't wait for it to complete: return null; }); communication.register("openMyShots", (sender) => { return catcher.watchPromise( auth.maybeLogin() .then(() => browser.tabs.create({url: backend + "/shots"}))); }); communication.register("openShot", async (sender, {url, copied}) => { if (copied) { const id = makeUuid(); const [ title, message ] = await getStrings([ { id: "screenshots-notification-link-copied-title" }, { id: "screenshots-notification-link-copied-details" }, ]); return browser.notifications.create(id, { type: "basic", iconUrl: "../icons/copied-notification.svg", title, message, }); } return null; }); // This is used for truncated full page downloads and copy to clipboards. // Those longer operations need to display an animated spinner/loader, so // it's preferable to perform toDataURL() in the background. communication.register("canvasToDataURL", (sender, imageData) => { const canvas = document.createElement("canvas"); canvas.width = imageData.width; canvas.height = imageData.height; canvas.getContext("2d").putImageData(imageData, 0, 0); let dataUrl = canvas.toDataURL(); if (buildSettings.pngToJpegCutoff && dataUrl.length > buildSettings.pngToJpegCutoff) { const jpegDataUrl = canvas.toDataURL("image/jpeg"); if (jpegDataUrl.length < dataUrl.length) { // Only use the JPEG if it is actually smaller dataUrl = jpegDataUrl; } } return dataUrl; }); communication.register("copyShotToClipboard", async (sender, blob) => { let buffer = await blobConverters.blobToArray(blob); await browser.clipboard.setImageData(buffer, blob.type.split("/", 2)[1]); const [title, message] = await getStrings([ { id: "screenshots-notification-image-copied-title" }, { id: "screenshots-notification-image-copied-details" }, ]); catcher.watchPromise(incrementCount("copy")); return browser.notifications.create({ type: "basic", iconUrl: "../icons/copied-notification.svg", title, message, }); }); communication.register("downloadShot", (sender, info) => { // 'data:' urls don't work directly, let's use a Blob // see http://stackoverflow.com/questions/40269862/save-data-uri-as-file-using-downloads-download-api const blob = blobConverters.dataUrlToBlob(info.url); const url = URL.createObjectURL(blob); let downloadId; const onChangedCallback = catcher.watchFunction(function(change) { if (!downloadId || downloadId !== change.id) { return; } if (change.state && change.state.current !== "in_progress") { URL.revokeObjectURL(url); browser.downloads.onChanged.removeListener(onChangedCallback); } }); browser.downloads.onChanged.addListener(onChangedCallback); catcher.watchPromise(incrementCount("download")); return browser.windows.getLastFocused().then(windowInfo => { return browser.downloads.download({ url, incognito: windowInfo.incognito, filename: info.filename, }).catch((error) => { // We are not logging error message when user cancels download if (error && error.message && !error.message.includes("canceled")) { log.error(error.message); } }).then((id) => { downloadId = id; }); }); }); communication.register("closeSelector", (sender) => { setIconActive(false, sender.tab.id); }); communication.register("abortStartShot", () => { // Note, we only show the error but don't report it, as we know that we can't // take shots of these pages: senderror.showError({ popupMessage: "UNSHOOTABLE_PAGE", }); }); // A Screenshots page wants us to start/force onboarding communication.register("requestOnboarding", (sender) => { return startSelectionWithOnboarding(sender.tab); }); communication.register("getPlatformOs", () => { return catcher.watchPromise(browser.runtime.getPlatformInfo().then(platformInfo => { return platformInfo.os; })); }); // This allows the web site show notifications through sitehelper.js communication.register("showNotification", (sender, notification) => { return browser.notifications.create(notification); }); return exports; })(); PK !<¬2½)77background/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 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 = [ "build/buildSettings.js", "log.js", "catcher.js", "assertIsTrusted.js", "assertIsBlankDocument.js", "blobConverters.js", "background/selectorLoader.js", "selector/callBackground.js", "selector/util.js", ]; const selectorScripts = [ "clipboard.js", "makeUuid.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(browser.tabs.executeScript(tabId, { code: `window.hasAnyShots = ${!!main.hasAnyShots()};`, runAt: "document_start", }).then(() => { return executeModules(tabId, standardScripts.concat(selectorScripts)); }).finally(() => { 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 !<þZHâ••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, makeUuid, Raven, catcher, auth, log */ "use strict"; this.senderror = (function() { const exports = {}; const manifest = browser.runtime.getManifest(); // 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", }, SHOT_PAGE: { titleKey: "screenshots-self-screenshot-error-title", }, MY_SHOTS: { titleKey: "screenshots-self-screenshot-error-title", }, 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 = makeUuid(); 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 dsn = auth.getSentryPublicDSN(); if (!dsn) { return; } if (!Raven.isSetup()) { Raven.config(dsn, {allowSecretKey: true}).install(); } 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; Raven.captureException(exception, { logger: "addon", tags: {category: e.popupMessage}, release: manifest.version, message: exception.message, extra: rest, }); }; catcher.registerHandler((errorObj) => { if (!errorObj.noPopup) { exports.showError(errorObj); } if (!errorObj.noReport) { exports.reportError(errorObj); } }); return exports; })(); PK !<ŠÀZ_ÐÐbackground/startBackground.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, main, communication, manifest */ /* This file handles: clicks on the WebExtension page action browser.contextMenus.onClicked browser.runtime.onMessage and loads the rest of the background page in response to those events, forwarding the events to main.onClicked, main.onClickedContextMenu, or communication.onMessage */ const startTime = Date.now(); // Set up to be able to use fluent: (function() { let link = document.createElement("link"); link.setAttribute("rel", "localization"); link.setAttribute("href", "browser/screenshots.ftl"); document.head.appendChild(link); link = document.createElement("link"); link.setAttribute("rel", "localization"); link.setAttribute("href", "browser/branding/brandings.ftl"); document.head.appendChild(link); })(); this.getStrings = async function(ids) { if (document.readyState != "complete") { await new Promise(resolve => window.addEventListener("load", resolve, {once: true})); } await document.l10n.ready; return document.l10n.formatValues(ids); } this.startBackground = (function() { const exports = {startTime}; const backgroundScripts = [ "log.js", "makeUuid.js", "catcher.js", "blobConverters.js", "background/selectorLoader.js", "background/communication.js", "background/auth.js", "background/senderror.js", "build/raven.js", "build/shot.js", "build/thumbnailGenerator.js", "background/analytics.js", "background/deviceInfo.js", "background/takeshot.js", "background/main.js", ]; browser.pageAction.onClicked.addListener(tab => { loadIfNecessary().then(() => { main.onClicked(tab); }).catch(error => { console.error("Error loading Screenshots:", error); }); }); this.getStrings([{id: "screenshots-context-menu"}]).then(msgs => { browser.contextMenus.create({ id: "create-screenshot", title: msgs[0], contexts: ["page", "selection"], documentUrlPatterns: ["", "about:reader*"], }); }); browser.contextMenus.onClicked.addListener((info, tab) => { loadIfNecessary().then(() => { main.onClickedContextMenu(info, tab); }).catch((error) => { console.error("Error loading Screenshots:", error); }); }); browser.commands.onCommand.addListener((cmd) => { if (cmd !== "take-screenshot") { return; } loadIfNecessary().then(() => { browser.tabs.query({currentWindow: true, active: true}).then((tabs) => { const activeTab = tabs[0]; main.onCommand(activeTab); }).catch((error) => { throw error; }); }).catch((error) => { console.error("Error toggling Screenshots via keyboard shortcut: ", 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.extension.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 !<ÙZ.¬ìì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 communication, shot, main, auth, catcher, analytics, buildSettings, blobConverters, thumbnailGenerator */ "use strict"; this.takeshot = (function() { const exports = {}; const Shot = shot.AbstractShot; const { sendEvent, incrementCount } = analytics; const MAX_CANVAS_DIMENSION = 32767; communication.register("screenshotPage", (sender, selectedPos, isFullPage, devicePixelRatio) => { return screenshotPage(selectedPos, isFullPage, devicePixelRatio); }); function screenshotPage(pos, isFullPage, 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 (isFullPage) { let rectangle = { x: 0, y: 0, width: pos.width, height: pos.height, } options.rect = rectangle; // To avoid creating extremely large images (which causes // performance problems), we set the scale to 1. devicePixelRatio = options.scale = 1; } else { 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); }); }); })); } /** Combines two buffers or Uint8Array's */ function concatBuffers(buffer1, buffer2) { const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); tmp.set(new Uint8Array(buffer1), 0); tmp.set(new Uint8Array(buffer2), buffer1.byteLength); return tmp.buffer; } /** Creates a multipart TypedArray, given {name: value} fields and a files array in the format of [{fieldName: "NAME", filename: "NAME.png", blob: fileBlob}, {...}, ...] Returns {body, "content-type"} */ function createMultipart(fields, files) { const boundary = "---------------------------ScreenshotBoundary" + Date.now(); let body = []; for (const name in fields) { body.push("--" + boundary); body.push(`Content-Disposition: form-data; name="${name}"`); body.push(""); body.push(fields[name]); } body.push(""); body = body.join("\r\n"); const enc = new TextEncoder("utf-8"); body = enc.encode(body).buffer; const blobToArrayPromises = files.map(f => { return blobConverters.blobToArray(f.blob); }); return Promise.all(blobToArrayPromises).then(buffers => { for (let i = 0; i < buffers.length; i++) { let filePart = []; filePart.push("--" + boundary); filePart.push(`Content-Disposition: form-data; name="${files[i].fieldName}"; filename="${files[i].filename}"`); filePart.push(`Content-Type: ${files[i].blob.type}`); filePart.push(""); filePart.push(""); filePart = filePart.join("\r\n"); filePart = concatBuffers(enc.encode(filePart).buffer, buffers[i]); body = concatBuffers(body, filePart); body = concatBuffers(body, enc.encode("\r\n").buffer); } let tail = `\r\n--${boundary}--`; tail = enc.encode(tail); body = concatBuffers(body, tail.buffer); return { "content-type": `multipart/form-data; boundary=${boundary}`, body, }; }); } function uploadShot(shot, blob, thumbnail) { let headers; return auth.authHeaders().then((_headers) => { headers = _headers; if (blob) { const files = [ {fieldName: "blob", filename: "screenshot.png", blob} ]; if (thumbnail) { files.push({fieldName: "thumbnail", filename: "thumbnail.png", blob: thumbnail}); } return createMultipart( {shot: JSON.stringify(shot)}, files ); } return { "content-type": "application/json", body: JSON.stringify(shot), }; }).then((submission) => { headers["content-type"] = submission["content-type"]; sendEvent("upload", "started", {eventValue: Math.floor(submission.body.length / 1000)}); return fetch(shot.jsonUrl, { method: "PUT", mode: "cors", headers, body: submission.body, }); }).then((resp) => { if (!resp.ok) { sendEvent("upload-failed", `status-${resp.status}`); const exc = new Error(`Response failed with status ${resp.status}`); exc.popupMessage = "REQUEST_ERROR"; throw exc; } else { sendEvent("upload", "success"); } }, (error) => { // FIXME: I'm not sure what exceptions we can expect sendEvent("upload-failed", "connection"); error.popupMessage = "CONNECTION_ERROR"; throw error; }); } return exports; })(); PK ! PK ! 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 ! 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("MOZ_EXTENSION/icons/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; } .myshots-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); } .myshots-all-buttons-container .spacer { background-color: #c9c9c9; flex: 0 0 1px; height: 80px; margin: 0 10px; position: relative; top: -8px; } .myshots-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; } .myshots-all-buttons-container button:hover { background-color: #ebebeb; border: 1px solid #c7c7c7; } .myshots-all-buttons-container button:active { background-color: #dedede; border: 1px solid #989898; } .myshots-all-buttons-container .myshots-button { background-image: url("MOZ_EXTENSION/icons/menu-myshot.svg"); } .myshots-all-buttons-container .full-page { background-image: url("MOZ_EXTENSION/icons/menu-fullpage.svg"); } .myshots-all-buttons-container .visible { background-image: url("MOZ_EXTENSION/icons/menu-visible.svg"); } .myshots-button-container { display: flex; align-items: center; justify-content: center; } @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 !<;µ<œTäTäbuild/raven.js/*! Raven.js 3.27.0 (200cffcc) | github.com/getsentry/raven-js */ /* * Includes TraceKit * https://github.com/getsentry/TraceKit * * Copyright (c) 2018 Sentry (https://sentry.io) and individual contributors. * All rights reserved. * https://github.com/getsentry/sentry-javascript/blob/master/packages/raven-js/LICENSE * */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Raven = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o captureException(string) => captureMessage(string) if (initialCall && initialCall.func === 'Raven.captureException') { initialCall = stack.stack[2]; } var fileurl = (initialCall && initialCall.url) || ''; if ( !!this._globalOptions.ignoreUrls.test && this._globalOptions.ignoreUrls.test(fileurl) ) { return; } if ( !!this._globalOptions.whitelistUrls.test && !this._globalOptions.whitelistUrls.test(fileurl) ) { return; } // Always attempt to get stacktrace if message is empty. // It's the only way to provide any helpful information to the user. if (this._globalOptions.stacktrace || options.stacktrace || data.message === '') { // fingerprint on msg, not stack trace (legacy behavior, could be revisited) data.fingerprint = data.fingerprint == null ? msg : data.fingerprint; options = objectMerge( { trimHeadFrames: 0 }, options ); // Since we know this is a synthetic trace, the top frame (this function call) // MUST be from Raven.js, so mark it for trimming // We add to the trim counter so that callers can choose to trim extra frames, such // as utility functions. options.trimHeadFrames += 1; var frames = this._prepareFrames(stack, options); data.stacktrace = { // Sentry expects frames oldest to newest frames: frames.reverse() }; } // Make sure that fingerprint is always wrapped in an array if (data.fingerprint) { data.fingerprint = isArray(data.fingerprint) ? data.fingerprint : [data.fingerprint]; } // Fire away! this._send(data); return this; }, captureBreadcrumb: function(obj) { var crumb = objectMerge( { timestamp: now() / 1000 }, obj ); if (isFunction(this._globalOptions.breadcrumbCallback)) { var result = this._globalOptions.breadcrumbCallback(crumb); if (isObject(result) && !isEmptyObject(result)) { crumb = result; } else if (result === false) { return this; } } this._breadcrumbs.push(crumb); if (this._breadcrumbs.length > this._globalOptions.maxBreadcrumbs) { this._breadcrumbs.shift(); } return this; }, addPlugin: function(plugin /*arg1, arg2, ... argN*/) { var pluginArgs = [].slice.call(arguments, 1); this._plugins.push([plugin, pluginArgs]); if (this._isRavenInstalled) { this._drainPlugins(); } return this; }, /* * Set/clear a user to be sent along with the payload. * * @param {object} user An object representing user data [optional] * @return {Raven} */ setUserContext: function(user) { // Intentionally do not merge here since that's an unexpected behavior. this._globalContext.user = user; return this; }, /* * Merge extra attributes to be sent along with the payload. * * @param {object} extra An object representing extra data [optional] * @return {Raven} */ setExtraContext: function(extra) { this._mergeContext('extra', extra); return this; }, /* * Merge tags to be sent along with the payload. * * @param {object} tags An object representing tags [optional] * @return {Raven} */ setTagsContext: function(tags) { this._mergeContext('tags', tags); return this; }, /* * Clear all of the context. * * @return {Raven} */ clearContext: function() { this._globalContext = {}; return this; }, /* * Get a copy of the current context. This cannot be mutated. * * @return {object} copy of context */ getContext: function() { // lol javascript return JSON.parse(stringify(this._globalContext)); }, /* * Set environment of application * * @param {string} environment Typically something like 'production'. * @return {Raven} */ setEnvironment: function(environment) { this._globalOptions.environment = environment; return this; }, /* * Set release version of application * * @param {string} release Typically something like a git SHA to identify version * @return {Raven} */ setRelease: function(release) { this._globalOptions.release = release; return this; }, /* * Set the dataCallback option * * @param {function} callback The callback to run which allows the * data blob to be mutated before sending * @return {Raven} */ setDataCallback: function(callback) { var original = this._globalOptions.dataCallback; this._globalOptions.dataCallback = keepOriginalCallback(original, callback); return this; }, /* * Set the breadcrumbCallback option * * @param {function} callback The callback to run which allows filtering * or mutating breadcrumbs * @return {Raven} */ setBreadcrumbCallback: function(callback) { var original = this._globalOptions.breadcrumbCallback; this._globalOptions.breadcrumbCallback = keepOriginalCallback(original, callback); return this; }, /* * Set the shouldSendCallback option * * @param {function} callback The callback to run which allows * introspecting the blob before sending * @return {Raven} */ setShouldSendCallback: function(callback) { var original = this._globalOptions.shouldSendCallback; this._globalOptions.shouldSendCallback = keepOriginalCallback(original, callback); return this; }, /** * Override the default HTTP transport mechanism that transmits data * to the Sentry server. * * @param {function} transport Function invoked instead of the default * `makeRequest` handler. * * @return {Raven} */ setTransport: function(transport) { this._globalOptions.transport = transport; return this; }, /* * Get the latest raw exception that was captured by Raven. * * @return {error} */ lastException: function() { return this._lastCapturedException; }, /* * Get the last event id * * @return {string} */ lastEventId: function() { return this._lastEventId; }, /* * Determine if Raven is setup and ready to go. * * @return {boolean} */ isSetup: function() { if (!this._hasJSON) return false; // needs JSON support if (!this._globalServer) { if (!this.ravenNotConfiguredError) { this.ravenNotConfiguredError = true; this._logDebug('error', 'Error: Raven has not been configured.'); } return false; } return true; }, afterLoad: function() { // TODO: remove window dependence? // Attempt to initialize Raven on load var RavenConfig = _window.RavenConfig; if (RavenConfig) { this.config(RavenConfig.dsn, RavenConfig.config).install(); } }, showReportDialog: function(options) { if ( !_document // doesn't work without a document (React native) ) return; options = objectMerge( { eventId: this.lastEventId(), dsn: this._dsn, user: this._globalContext.user || {} }, options ); if (!options.eventId) { throw new RavenConfigError('Missing eventId'); } if (!options.dsn) { throw new RavenConfigError('Missing DSN'); } var encode = encodeURIComponent; var encodedOptions = []; for (var key in options) { if (key === 'user') { var user = options.user; if (user.name) encodedOptions.push('name=' + encode(user.name)); if (user.email) encodedOptions.push('email=' + encode(user.email)); } else { encodedOptions.push(encode(key) + '=' + encode(options[key])); } } var globalServer = this._getGlobalServer(this._parseDSN(options.dsn)); var script = _document.createElement('script'); script.async = true; script.src = globalServer + '/api/embed/error-page/?' + encodedOptions.join('&'); (_document.head || _document.body).appendChild(script); }, /**** Private functions ****/ _ignoreNextOnError: function() { var self = this; this._ignoreOnError += 1; setTimeout(function() { // onerror should trigger before setTimeout self._ignoreOnError -= 1; }); }, _triggerEvent: function(eventType, options) { // NOTE: `event` is a native browser thing, so let's avoid conflicting wiht it var evt, key; if (!this._hasDocument) return; options = options || {}; eventType = 'raven' + eventType.substr(0, 1).toUpperCase() + eventType.substr(1); if (_document.createEvent) { evt = _document.createEvent('HTMLEvents'); evt.initEvent(eventType, true, true); } else { evt = _document.createEventObject(); evt.eventType = eventType; } for (key in options) if (hasKey(options, key)) { evt[key] = options[key]; } if (_document.createEvent) { // IE9 if standards _document.dispatchEvent(evt); } else { // IE8 regardless of Quirks or Standards // IE9 if quirks try { _document.fireEvent('on' + evt.eventType.toLowerCase(), evt); } catch (e) { // Do nothing } } }, /** * Wraps addEventListener to capture UI breadcrumbs * @param evtName the event name (e.g. "click") * @returns {Function} * @private */ _breadcrumbEventHandler: function(evtName) { var self = this; return function(evt) { // reset keypress timeout; e.g. triggering a 'click' after // a 'keypress' will reset the keypress debounce so that a new // set of keypresses can be recorded self._keypressTimeout = null; // It's possible this handler might trigger multiple times for the same // event (e.g. event propagation through node ancestors). Ignore if we've // already captured the event. if (self._lastCapturedEvent === evt) return; self._lastCapturedEvent = evt; // try/catch both: // - accessing evt.target (see getsentry/raven-js#838, #768) // - `htmlTreeAsString` because it's complex, and just accessing the DOM incorrectly // can throw an exception in some circumstances. var target; try { target = htmlTreeAsString(evt.target); } catch (e) { target = ''; } self.captureBreadcrumb({ category: 'ui.' + evtName, // e.g. ui.click, ui.input message: target }); }; }, /** * Wraps addEventListener to capture keypress UI events * @returns {Function} * @private */ _keypressEventHandler: function() { var self = this, debounceDuration = 1000; // milliseconds // TODO: if somehow user switches keypress target before // debounce timeout is triggered, we will only capture // a single breadcrumb from the FIRST target (acceptable?) return function(evt) { var target; try { target = evt.target; } catch (e) { // just accessing event properties can throw an exception in some rare circumstances // see: https://github.com/getsentry/raven-js/issues/838 return; } var tagName = target && target.tagName; // only consider keypress events on actual input elements // this will disregard keypresses targeting body (e.g. tabbing // through elements, hotkeys, etc) if ( !tagName || (tagName !== 'INPUT' && tagName !== 'TEXTAREA' && !target.isContentEditable) ) return; // record first keypress in a series, but ignore subsequent // keypresses until debounce clears var timeout = self._keypressTimeout; if (!timeout) { self._breadcrumbEventHandler('input')(evt); } clearTimeout(timeout); self._keypressTimeout = setTimeout(function() { self._keypressTimeout = null; }, debounceDuration); }; }, /** * Captures a breadcrumb of type "navigation", normalizing input URLs * @param to the originating URL * @param from the target URL * @private */ _captureUrlChange: function(from, to) { var parsedLoc = parseUrl(this._location.href); var parsedTo = parseUrl(to); var parsedFrom = parseUrl(from); // because onpopstate only tells you the "new" (to) value of location.href, and // not the previous (from) value, we need to track the value of the current URL // state ourselves this._lastHref = to; // Use only the path component of the URL if the URL matches the current // document (almost all the time when using pushState) if (parsedLoc.protocol === parsedTo.protocol && parsedLoc.host === parsedTo.host) to = parsedTo.relative; if (parsedLoc.protocol === parsedFrom.protocol && parsedLoc.host === parsedFrom.host) from = parsedFrom.relative; this.captureBreadcrumb({ category: 'navigation', data: { to: to, from: from } }); }, _patchFunctionToString: function() { var self = this; self._originalFunctionToString = Function.prototype.toString; // eslint-disable-next-line no-extend-native Function.prototype.toString = function() { if (typeof this === 'function' && this.__raven__) { return self._originalFunctionToString.apply(this.__orig__, arguments); } return self._originalFunctionToString.apply(this, arguments); }; }, _unpatchFunctionToString: function() { if (this._originalFunctionToString) { // eslint-disable-next-line no-extend-native Function.prototype.toString = this._originalFunctionToString; } }, /** * Wrap timer functions and event targets to catch errors and provide * better metadata. */ _instrumentTryCatch: function() { var self = this; var wrappedBuiltIns = self._wrappedBuiltIns; function wrapTimeFn(orig) { return function(fn, t) { // preserve arity // Make a copy of the arguments to prevent deoptimization // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments var args = new Array(arguments.length); for (var i = 0; i < args.length; ++i) { args[i] = arguments[i]; } var originalCallback = args[0]; if (isFunction(originalCallback)) { args[0] = self.wrap( { mechanism: { type: 'instrument', data: {function: orig.name || ''} } }, originalCallback ); } // IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it // also supports only two arguments and doesn't care what this is, so we // can just call the original function directly. if (orig.apply) { return orig.apply(this, args); } else { return orig(args[0], args[1]); } }; } var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs; function wrapEventTarget(global) { var proto = _window[global] && _window[global].prototype; if (proto && proto.hasOwnProperty && proto.hasOwnProperty('addEventListener')) { fill( proto, 'addEventListener', function(orig) { return function(evtName, fn, capture, secure) { // preserve arity try { if (fn && fn.handleEvent) { fn.handleEvent = self.wrap( { mechanism: { type: 'instrument', data: { target: global, function: 'handleEvent', handler: (fn && fn.name) || '' } } }, fn.handleEvent ); } } catch (err) { // can sometimes get 'Permission denied to access property "handle Event' } // More breadcrumb DOM capture ... done here and not in `_instrumentBreadcrumbs` // so that we don't have more than one wrapper function var before, clickHandler, keypressHandler; if ( autoBreadcrumbs && autoBreadcrumbs.dom && (global === 'EventTarget' || global === 'Node') ) { // NOTE: generating multiple handlers per addEventListener invocation, should // revisit and verify we can just use one (almost certainly) clickHandler = self._breadcrumbEventHandler('click'); keypressHandler = self._keypressEventHandler(); before = function(evt) { // need to intercept every DOM event in `before` argument, in case that // same wrapped method is re-used for different events (e.g. mousemove THEN click) // see #724 if (!evt) return; var eventType; try { eventType = evt.type; } catch (e) { // just accessing event properties can throw an exception in some rare circumstances // see: https://github.com/getsentry/raven-js/issues/838 return; } if (eventType === 'click') return clickHandler(evt); else if (eventType === 'keypress') return keypressHandler(evt); }; } return orig.call( this, evtName, self.wrap( { mechanism: { type: 'instrument', data: { target: global, function: 'addEventListener', handler: (fn && fn.name) || '' } } }, fn, before ), capture, secure ); }; }, wrappedBuiltIns ); fill( proto, 'removeEventListener', function(orig) { return function(evt, fn, capture, secure) { try { fn = fn && (fn.__raven_wrapper__ ? fn.__raven_wrapper__ : fn); } catch (e) { // ignore, accessing __raven_wrapper__ will throw in some Selenium environments } return orig.call(this, evt, fn, capture, secure); }; }, wrappedBuiltIns ); } } fill(_window, 'setTimeout', wrapTimeFn, wrappedBuiltIns); fill(_window, 'setInterval', wrapTimeFn, wrappedBuiltIns); if (_window.requestAnimationFrame) { fill( _window, 'requestAnimationFrame', function(orig) { return function(cb) { return orig( self.wrap( { mechanism: { type: 'instrument', data: { function: 'requestAnimationFrame', handler: (orig && orig.name) || '' } } }, cb ) ); }; }, wrappedBuiltIns ); } // event targets borrowed from bugsnag-js: // https://github.com/bugsnag/bugsnag-js/blob/master/src/bugsnag.js#L666 var eventTargets = [ 'EventTarget', 'Window', 'Node', 'ApplicationCache', 'AudioTrackList', 'ChannelMergerNode', 'CryptoOperation', 'EventSource', 'FileReader', 'HTMLUnknownElement', 'IDBDatabase', 'IDBRequest', 'IDBTransaction', 'KeyOperation', 'MediaController', 'MessagePort', 'ModalWindow', 'Notification', 'SVGElementInstance', 'Screen', 'TextTrack', 'TextTrackCue', 'TextTrackList', 'WebSocket', 'WebSocketWorker', 'Worker', 'XMLHttpRequest', 'XMLHttpRequestEventTarget', 'XMLHttpRequestUpload' ]; for (var i = 0; i < eventTargets.length; i++) { wrapEventTarget(eventTargets[i]); } }, /** * Instrument browser built-ins w/ breadcrumb capturing * - XMLHttpRequests * - DOM interactions (click/typing) * - window.location changes * - console * * Can be disabled or individually configured via the `autoBreadcrumbs` config option */ _instrumentBreadcrumbs: function() { var self = this; var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs; var wrappedBuiltIns = self._wrappedBuiltIns; function wrapProp(prop, xhr) { if (prop in xhr && isFunction(xhr[prop])) { fill(xhr, prop, function(orig) { return self.wrap( { mechanism: { type: 'instrument', data: {function: prop, handler: (orig && orig.name) || ''} } }, orig ); }); // intentionally don't track filled methods on XHR instances } } if (autoBreadcrumbs.xhr && 'XMLHttpRequest' in _window) { var xhrproto = _window.XMLHttpRequest && _window.XMLHttpRequest.prototype; fill( xhrproto, 'open', function(origOpen) { return function(method, url) { // preserve arity // if Sentry key appears in URL, don't capture if (isString(url) && url.indexOf(self._globalKey) === -1) { this.__raven_xhr = { method: method, url: url, status_code: null }; } return origOpen.apply(this, arguments); }; }, wrappedBuiltIns ); fill( xhrproto, 'send', function(origSend) { return function() { // preserve arity var xhr = this; function onreadystatechangeHandler() { if (xhr.__raven_xhr && xhr.readyState === 4) { try { // touching statusCode in some platforms throws // an exception xhr.__raven_xhr.status_code = xhr.status; } catch (e) { /* do nothing */ } self.captureBreadcrumb({ type: 'http', category: 'xhr', data: xhr.__raven_xhr }); } } var props = ['onload', 'onerror', 'onprogress']; for (var j = 0; j < props.length; j++) { wrapProp(props[j], xhr); } if ('onreadystatechange' in xhr && isFunction(xhr.onreadystatechange)) { fill( xhr, 'onreadystatechange', function(orig) { return self.wrap( { mechanism: { type: 'instrument', data: { function: 'onreadystatechange', handler: (orig && orig.name) || '' } } }, orig, onreadystatechangeHandler ); } /* intentionally don't track this instrumentation */ ); } else { // if onreadystatechange wasn't actually set by the page on this xhr, we // are free to set our own and capture the breadcrumb xhr.onreadystatechange = onreadystatechangeHandler; } return origSend.apply(this, arguments); }; }, wrappedBuiltIns ); } if (autoBreadcrumbs.xhr && supportsFetch()) { fill( _window, 'fetch', function(origFetch) { return function() { // preserve arity // Make a copy of the arguments to prevent deoptimization // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments var args = new Array(arguments.length); for (var i = 0; i < args.length; ++i) { args[i] = arguments[i]; } var fetchInput = args[0]; var method = 'GET'; var url; if (typeof fetchInput === 'string') { url = fetchInput; } else if ('Request' in _window && fetchInput instanceof _window.Request) { url = fetchInput.url; if (fetchInput.method) { method = fetchInput.method; } } else { url = '' + fetchInput; } // if Sentry key appears in URL, don't capture, as it's our own request if (url.indexOf(self._globalKey) !== -1) { return origFetch.apply(this, args); } if (args[1] && args[1].method) { method = args[1].method; } var fetchData = { method: method, url: url, status_code: null }; return origFetch .apply(this, args) .then(function(response) { fetchData.status_code = response.status; self.captureBreadcrumb({ type: 'http', category: 'fetch', data: fetchData }); return response; }) ['catch'](function(err) { // if there is an error performing the request self.captureBreadcrumb({ type: 'http', category: 'fetch', data: fetchData, level: 'error' }); throw err; }); }; }, wrappedBuiltIns ); } // Capture breadcrumbs from any click that is unhandled / bubbled up all the way // to the document. Do this before we instrument addEventListener. if (autoBreadcrumbs.dom && this._hasDocument) { if (_document.addEventListener) { _document.addEventListener('click', self._breadcrumbEventHandler('click'), false); _document.addEventListener('keypress', self._keypressEventHandler(), false); } else if (_document.attachEvent) { // IE8 Compatibility _document.attachEvent('onclick', self._breadcrumbEventHandler('click')); _document.attachEvent('onkeypress', self._keypressEventHandler()); } } // record navigation (URL) changes // NOTE: in Chrome App environment, touching history.pushState, *even inside // a try/catch block*, will cause Chrome to output an error to console.error // borrowed from: https://github.com/angular/angular.js/pull/13945/files var chrome = _window.chrome; var isChromePackagedApp = chrome && chrome.app && chrome.app.runtime; var hasPushAndReplaceState = !isChromePackagedApp && _window.history && _window.history.pushState && _window.history.replaceState; if (autoBreadcrumbs.location && hasPushAndReplaceState) { // TODO: remove onpopstate handler on uninstall() var oldOnPopState = _window.onpopstate; _window.onpopstate = function() { var currentHref = self._location.href; self._captureUrlChange(self._lastHref, currentHref); if (oldOnPopState) { return oldOnPopState.apply(this, arguments); } }; var historyReplacementFunction = function(origHistFunction) { // note history.pushState.length is 0; intentionally not declaring // params to preserve 0 arity return function(/* state, title, url */) { var url = arguments.length > 2 ? arguments[2] : undefined; // url argument is optional if (url) { // coerce to string (this is what pushState does) self._captureUrlChange(self._lastHref, url + ''); } return origHistFunction.apply(this, arguments); }; }; fill(_window.history, 'pushState', historyReplacementFunction, wrappedBuiltIns); fill(_window.history, 'replaceState', historyReplacementFunction, wrappedBuiltIns); } if (autoBreadcrumbs.console && 'console' in _window && console.log) { // console var consoleMethodCallback = function(msg, data) { self.captureBreadcrumb({ message: msg, level: data.level, category: 'console' }); }; each(['debug', 'info', 'warn', 'error', 'log'], function(_, level) { wrapConsoleMethod(console, level, consoleMethodCallback); }); } }, _restoreBuiltIns: function() { // restore any wrapped builtins var builtin; while (this._wrappedBuiltIns.length) { builtin = this._wrappedBuiltIns.shift(); var obj = builtin[0], name = builtin[1], orig = builtin[2]; obj[name] = orig; } }, _restoreConsole: function() { // eslint-disable-next-line guard-for-in for (var method in this._originalConsoleMethods) { this._originalConsole[method] = this._originalConsoleMethods[method]; } }, _drainPlugins: function() { var self = this; // FIX ME TODO each(this._plugins, function(_, plugin) { var installer = plugin[0]; var args = plugin[1]; installer.apply(self, [self].concat(args)); }); }, _parseDSN: function(str) { var m = dsnPattern.exec(str), dsn = {}, i = 7; try { while (i--) dsn[dsnKeys[i]] = m[i] || ''; } catch (e) { throw new RavenConfigError('Invalid DSN: ' + str); } if (dsn.pass && !this._globalOptions.allowSecretKey) { throw new RavenConfigError( 'Do not specify your secret key in the DSN. See: http://bit.ly/raven-secret-key' ); } return dsn; }, _getGlobalServer: function(uri) { // assemble the endpoint from the uri pieces var globalServer = '//' + uri.host + (uri.port ? ':' + uri.port : ''); if (uri.protocol) { globalServer = uri.protocol + ':' + globalServer; } return globalServer; }, _handleOnErrorStackInfo: function(stackInfo, options) { options = options || {}; options.mechanism = options.mechanism || { type: 'onerror', handled: false }; // if we are intentionally ignoring errors via onerror, bail out if (!this._ignoreOnError) { this._handleStackInfo(stackInfo, options); } }, _handleStackInfo: function(stackInfo, options) { var frames = this._prepareFrames(stackInfo, options); this._triggerEvent('handle', { stackInfo: stackInfo, options: options }); this._processException( stackInfo.name, stackInfo.message, stackInfo.url, stackInfo.lineno, frames, options ); }, _prepareFrames: function(stackInfo, options) { var self = this; var frames = []; if (stackInfo.stack && stackInfo.stack.length) { each(stackInfo.stack, function(i, stack) { var frame = self._normalizeFrame(stack, stackInfo.url); if (frame) { frames.push(frame); } }); // e.g. frames captured via captureMessage throw if (options && options.trimHeadFrames) { for (var j = 0; j < options.trimHeadFrames && j < frames.length; j++) { frames[j].in_app = false; } } } frames = frames.slice(0, this._globalOptions.stackTraceLimit); return frames; }, _normalizeFrame: function(frame, stackInfoUrl) { // normalize the frames data var normalized = { filename: frame.url, lineno: frame.line, colno: frame.column, function: frame.func || '?' }; // Case when we don't have any information about the error // E.g. throwing a string or raw object, instead of an `Error` in Firefox // Generating synthetic error doesn't add any value here // // We should probably somehow let a user know that they should fix their code if (!frame.url) { normalized.filename = stackInfoUrl; // fallback to whole stacks url from onerror handler } normalized.in_app = !// determine if an exception came from outside of our app // first we check the global includePaths list. ( (!!this._globalOptions.includePaths.test && !this._globalOptions.includePaths.test(normalized.filename)) || // Now we check for fun, if the function name is Raven or TraceKit /(Raven|TraceKit)\./.test(normalized['function']) || // finally, we do a last ditch effort and check for raven.min.js /raven\.(min\.)?js$/.test(normalized.filename) ); return normalized; }, _processException: function(type, message, fileurl, lineno, frames, options) { var prefixedMessage = (type ? type + ': ' : '') + (message || ''); if ( !!this._globalOptions.ignoreErrors.test && (this._globalOptions.ignoreErrors.test(message) || this._globalOptions.ignoreErrors.test(prefixedMessage)) ) { return; } var stacktrace; if (frames && frames.length) { fileurl = frames[0].filename || fileurl; // Sentry expects frames oldest to newest // and JS sends them as newest to oldest frames.reverse(); stacktrace = {frames: frames}; } else if (fileurl) { stacktrace = { frames: [ { filename: fileurl, lineno: lineno, in_app: true } ] }; } if ( !!this._globalOptions.ignoreUrls.test && this._globalOptions.ignoreUrls.test(fileurl) ) { return; } if ( !!this._globalOptions.whitelistUrls.test && !this._globalOptions.whitelistUrls.test(fileurl) ) { return; } var data = objectMerge( { // sentry.interfaces.Exception exception: { values: [ { type: type, value: message, stacktrace: stacktrace } ] }, transaction: fileurl }, options ); var ex = data.exception.values[0]; if (ex.type == null && ex.value === '') { ex.value = 'Unrecoverable error caught'; } // Move mechanism from options to exception interface // We do this, as requiring user to pass `{exception:{mechanism:{ ... }}}` would be // too much if (!data.exception.mechanism && data.mechanism) { data.exception.mechanism = data.mechanism; delete data.mechanism; } data.exception.mechanism = objectMerge( { type: 'generic', handled: true }, data.exception.mechanism || {} ); // Fire away! this._send(data); }, _trimPacket: function(data) { // For now, we only want to truncate the two different messages // but this could/should be expanded to just trim everything var max = this._globalOptions.maxMessageLength; if (data.message) { data.message = truncate(data.message, max); } if (data.exception) { var exception = data.exception.values[0]; exception.value = truncate(exception.value, max); } var request = data.request; if (request) { if (request.url) { request.url = truncate(request.url, this._globalOptions.maxUrlLength); } if (request.Referer) { request.Referer = truncate(request.Referer, this._globalOptions.maxUrlLength); } } if (data.breadcrumbs && data.breadcrumbs.values) this._trimBreadcrumbs(data.breadcrumbs); return data; }, /** * Truncate breadcrumb values (right now just URLs) */ _trimBreadcrumbs: function(breadcrumbs) { // known breadcrumb properties with urls // TODO: also consider arbitrary prop values that start with (https?)?:// var urlProps = ['to', 'from', 'url'], urlProp, crumb, data; for (var i = 0; i < breadcrumbs.values.length; ++i) { crumb = breadcrumbs.values[i]; if ( !crumb.hasOwnProperty('data') || !isObject(crumb.data) || objectFrozen(crumb.data) ) continue; data = objectMerge({}, crumb.data); for (var j = 0; j < urlProps.length; ++j) { urlProp = urlProps[j]; if (data.hasOwnProperty(urlProp) && data[urlProp]) { data[urlProp] = truncate(data[urlProp], this._globalOptions.maxUrlLength); } } breadcrumbs.values[i].data = data; } }, _getHttpData: function() { if (!this._hasNavigator && !this._hasDocument) return; var httpData = {}; if (this._hasNavigator && _navigator.userAgent) { httpData.headers = { 'User-Agent': _navigator.userAgent }; } // Check in `window` instead of `document`, as we may be in ServiceWorker environment if (_window.location && _window.location.href) { httpData.url = _window.location.href; } if (this._hasDocument && _document.referrer) { if (!httpData.headers) httpData.headers = {}; httpData.headers.Referer = _document.referrer; } return httpData; }, _resetBackoff: function() { this._backoffDuration = 0; this._backoffStart = null; }, _shouldBackoff: function() { return this._backoffDuration && now() - this._backoffStart < this._backoffDuration; }, /** * Returns true if the in-process data payload matches the signature * of the previously-sent data * * NOTE: This has to be done at this level because TraceKit can generate * data from window.onerror WITHOUT an exception object (IE8, IE9, * other old browsers). This can take the form of an "exception" * data object with a single frame (derived from the onerror args). */ _isRepeatData: function(current) { var last = this._lastData; if ( !last || current.message !== last.message || // defined for captureMessage current.transaction !== last.transaction // defined for captureException/onerror ) return false; // Stacktrace interface (i.e. from captureMessage) if (current.stacktrace || last.stacktrace) { return isSameStacktrace(current.stacktrace, last.stacktrace); } else if (current.exception || last.exception) { // Exception interface (i.e. from captureException/onerror) return isSameException(current.exception, last.exception); } return true; }, _setBackoffState: function(request) { // If we are already in a backoff state, don't change anything if (this._shouldBackoff()) { return; } var status = request.status; // 400 - project_id doesn't exist or some other fatal // 401 - invalid/revoked dsn // 429 - too many requests if (!(status === 400 || status === 401 || status === 429)) return; var retry; try { // If Retry-After is not in Access-Control-Expose-Headers, most // browsers will throw an exception trying to access it if (supportsFetch()) { retry = request.headers.get('Retry-After'); } else { retry = request.getResponseHeader('Retry-After'); } // Retry-After is returned in seconds retry = parseInt(retry, 10) * 1000; } catch (e) { /* eslint no-empty:0 */ } this._backoffDuration = retry ? // If Sentry server returned a Retry-After value, use it retry : // Otherwise, double the last backoff duration (starts at 1 sec) this._backoffDuration * 2 || 1000; this._backoffStart = now(); }, _send: function(data) { var globalOptions = this._globalOptions; var baseData = { project: this._globalProject, logger: globalOptions.logger, platform: 'javascript' }, httpData = this._getHttpData(); if (httpData) { baseData.request = httpData; } // HACK: delete `trimHeadFrames` to prevent from appearing in outbound payload if (data.trimHeadFrames) delete data.trimHeadFrames; data = objectMerge(baseData, data); // Merge in the tags and extra separately since objectMerge doesn't handle a deep merge data.tags = objectMerge(objectMerge({}, this._globalContext.tags), data.tags); data.extra = objectMerge(objectMerge({}, this._globalContext.extra), data.extra); // Send along our own collected metadata with extra data.extra['session:duration'] = now() - this._startTime; if (this._breadcrumbs && this._breadcrumbs.length > 0) { // intentionally make shallow copy so that additions // to breadcrumbs aren't accidentally sent in this request data.breadcrumbs = { values: [].slice.call(this._breadcrumbs, 0) }; } if (this._globalContext.user) { // sentry.interfaces.User data.user = this._globalContext.user; } // Include the environment if it's defined in globalOptions if (globalOptions.environment) data.environment = globalOptions.environment; // Include the release if it's defined in globalOptions if (globalOptions.release) data.release = globalOptions.release; // Include server_name if it's defined in globalOptions if (globalOptions.serverName) data.server_name = globalOptions.serverName; data = this._sanitizeData(data); // Cleanup empty properties before sending them to the server Object.keys(data).forEach(function(key) { if (data[key] == null || data[key] === '' || isEmptyObject(data[key])) { delete data[key]; } }); if (isFunction(globalOptions.dataCallback)) { data = globalOptions.dataCallback(data) || data; } // Why?????????? if (!data || isEmptyObject(data)) { return; } // Check if the request should be filtered or not if ( isFunction(globalOptions.shouldSendCallback) && !globalOptions.shouldSendCallback(data) ) { return; } // Backoff state: Sentry server previously responded w/ an error (e.g. 429 - too many requests), // so drop requests until "cool-off" period has elapsed. if (this._shouldBackoff()) { this._logDebug('warn', 'Raven dropped error due to backoff: ', data); return; } if (typeof globalOptions.sampleRate === 'number') { if (Math.random() < globalOptions.sampleRate) { this._sendProcessedPayload(data); } } else { this._sendProcessedPayload(data); } }, _sanitizeData: function(data) { return sanitize(data, this._globalOptions.sanitizeKeys); }, _getUuid: function() { return uuid4(); }, _sendProcessedPayload: function(data, callback) { var self = this; var globalOptions = this._globalOptions; if (!this.isSetup()) return; // Try and clean up the packet before sending by truncating long values data = this._trimPacket(data); // ideally duplicate error testing should occur *before* dataCallback/shouldSendCallback, // but this would require copying an un-truncated copy of the data packet, which can be // arbitrarily deep (extra_data) -- could be worthwhile? will revisit if (!this._globalOptions.allowDuplicates && this._isRepeatData(data)) { this._logDebug('warn', 'Raven dropped repeat event: ', data); return; } // Send along an event_id if not explicitly passed. // This event_id can be used to reference the error within Sentry itself. // Set lastEventId after we know the error should actually be sent this._lastEventId = data.event_id || (data.event_id = this._getUuid()); // Store outbound payload after trim this._lastData = data; this._logDebug('debug', 'Raven about to send:', data); var auth = { sentry_version: '7', sentry_client: 'raven-js/' + this.VERSION, sentry_key: this._globalKey }; if (this._globalSecret) { auth.sentry_secret = this._globalSecret; } var exception = data.exception && data.exception.values[0]; // only capture 'sentry' breadcrumb is autoBreadcrumbs is truthy if ( this._globalOptions.autoBreadcrumbs && this._globalOptions.autoBreadcrumbs.sentry ) { this.captureBreadcrumb({ category: 'sentry', message: exception ? (exception.type ? exception.type + ': ' : '') + exception.value : data.message, event_id: data.event_id, level: data.level || 'error' // presume error unless specified }); } var url = this._globalEndpoint; (globalOptions.transport || this._makeRequest).call(this, { url: url, auth: auth, data: data, options: globalOptions, onSuccess: function success() { self._resetBackoff(); self._triggerEvent('success', { data: data, src: url }); callback && callback(); }, onError: function failure(error) { self._logDebug('error', 'Raven transport failed to send: ', error); if (error.request) { self._setBackoffState(error.request); } self._triggerEvent('failure', { data: data, src: url }); error = error || new Error('Raven send failed (no additional details provided)'); callback && callback(error); } }); }, _makeRequest: function(opts) { // Auth is intentionally sent as part of query string (NOT as custom HTTP header) to avoid preflight CORS requests var url = opts.url + '?' + urlencode(opts.auth); var evaluatedHeaders = null; var evaluatedFetchParameters = {}; if (opts.options.headers) { evaluatedHeaders = this._evaluateHash(opts.options.headers); } if (opts.options.fetchParameters) { evaluatedFetchParameters = this._evaluateHash(opts.options.fetchParameters); } if (supportsFetch()) { evaluatedFetchParameters.body = stringify(opts.data); var defaultFetchOptions = objectMerge({}, this._fetchDefaults); var fetchOptions = objectMerge(defaultFetchOptions, evaluatedFetchParameters); if (evaluatedHeaders) { fetchOptions.headers = evaluatedHeaders; } return _window .fetch(url, fetchOptions) .then(function(response) { if (response.ok) { opts.onSuccess && opts.onSuccess(); } else { var error = new Error('Sentry error code: ' + response.status); // It's called request only to keep compatibility with XHR interface // and not add more redundant checks in setBackoffState method error.request = response; opts.onError && opts.onError(error); } }) ['catch'](function() { opts.onError && opts.onError(new Error('Sentry error code: network unavailable')); }); } var request = _window.XMLHttpRequest && new _window.XMLHttpRequest(); if (!request) return; // if browser doesn't support CORS (e.g. IE7), we are out of luck var hasCORS = 'withCredentials' in request || typeof XDomainRequest !== 'undefined'; if (!hasCORS) return; if ('withCredentials' in request) { request.onreadystatechange = function() { if (request.readyState !== 4) { return; } else if (request.status === 200) { opts.onSuccess && opts.onSuccess(); } else if (opts.onError) { var err = new Error('Sentry error code: ' + request.status); err.request = request; opts.onError(err); } }; } else { request = new XDomainRequest(); // xdomainrequest cannot go http -> https (or vice versa), // so always use protocol relative url = url.replace(/^https?:/, ''); // onreadystatechange not supported by XDomainRequest if (opts.onSuccess) { request.onload = opts.onSuccess; } if (opts.onError) { request.onerror = function() { var err = new Error('Sentry error code: XDomainRequest'); err.request = request; opts.onError(err); }; } } request.open('POST', url); if (evaluatedHeaders) { each(evaluatedHeaders, function(key, value) { request.setRequestHeader(key, value); }); } request.send(stringify(opts.data)); }, _evaluateHash: function(hash) { var evaluated = {}; for (var key in hash) { if (hash.hasOwnProperty(key)) { var value = hash[key]; evaluated[key] = typeof value === 'function' ? value() : value; } } return evaluated; }, _logDebug: function(level) { // We allow `Raven.debug` and `Raven.config(DSN, { debug: true })` to not make backward incompatible API change if ( this._originalConsoleMethods[level] && (this.debug || this._globalOptions.debug) ) { // In IE<10 console methods do not have their own 'apply' method Function.prototype.apply.call( this._originalConsoleMethods[level], this._originalConsole, [].slice.call(arguments, 1) ); } }, _mergeContext: function(key, context) { if (isUndefined(context)) { delete this._globalContext[key]; } else { this._globalContext[key] = objectMerge(this._globalContext[key] || {}, context); } } }; // Deprecations Raven.prototype.setUser = Raven.prototype.setUserContext; Raven.prototype.setReleaseContext = Raven.prototype.setRelease; module.exports = Raven; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"1":1,"2":2,"5":5,"6":6,"7":7,"8":8}],4:[function(_dereq_,module,exports){ (function (global){ /** * Enforces a single instance of the Raven client, and the * main entry point for Raven. If you are a consumer of the * Raven library, you SHOULD load this file (vs raven.js). **/ var RavenConstructor = _dereq_(3); // This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785) var _window = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; var _Raven = _window.Raven; var Raven = new RavenConstructor(); /* * Allow multiple versions of Raven to be installed. * Strip Raven from the global context and returns the instance. * * @return {Raven} */ Raven.noConflict = function() { _window.Raven = _Raven; return Raven; }; Raven.afterLoad(); module.exports = Raven; /** * DISCLAIMER: * * Expose `Client` constructor for cases where user want to track multiple "sub-applications" in one larger app. * It's not meant to be used by a wide audience, so pleaaase make sure that you know what you're doing before using it. * Accidentally calling `install` multiple times, may result in an unexpected behavior that's very hard to debug. * * It's called `Client' to be in-line with Raven Node implementation. * * HOWTO: * * import Raven from 'raven-js'; * * const someAppReporter = new Raven.Client(); * const someOtherAppReporter = new Raven.Client(); * * someAppReporter.config('__DSN__', { * ...config goes here * }); * * someOtherAppReporter.config('__OTHER_DSN__', { * ...config goes here * }); * * someAppReporter.captureMessage(...); * someAppReporter.captureException(...); * someAppReporter.captureBreadcrumb(...); * * someOtherAppReporter.captureMessage(...); * someOtherAppReporter.captureException(...); * someOtherAppReporter.captureBreadcrumb(...); * * It should "just work". */ module.exports.Client = RavenConstructor; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"3":3}],5:[function(_dereq_,module,exports){ (function (global){ var stringify = _dereq_(7); var _window = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function isObject(what) { return typeof what === 'object' && what !== null; } // Yanked from https://git.io/vS8DV re-used under CC0 // with some tiny modifications function isError(value) { switch (Object.prototype.toString.call(value)) { case '[object Error]': return true; case '[object Exception]': return true; case '[object DOMException]': return true; default: return value instanceof Error; } } function isErrorEvent(value) { return Object.prototype.toString.call(value) === '[object ErrorEvent]'; } function isDOMError(value) { return Object.prototype.toString.call(value) === '[object DOMError]'; } function isDOMException(value) { return Object.prototype.toString.call(value) === '[object DOMException]'; } function isUndefined(what) { return what === void 0; } function isFunction(what) { return typeof what === 'function'; } function isPlainObject(what) { return Object.prototype.toString.call(what) === '[object Object]'; } function isString(what) { return Object.prototype.toString.call(what) === '[object String]'; } function isArray(what) { return Object.prototype.toString.call(what) === '[object Array]'; } function isEmptyObject(what) { if (!isPlainObject(what)) return false; for (var _ in what) { if (what.hasOwnProperty(_)) { return false; } } return true; } function supportsErrorEvent() { try { new ErrorEvent(''); // eslint-disable-line no-new return true; } catch (e) { return false; } } function supportsDOMError() { try { new DOMError(''); // eslint-disable-line no-new return true; } catch (e) { return false; } } function supportsDOMException() { try { new DOMException(''); // eslint-disable-line no-new return true; } catch (e) { return false; } } function supportsFetch() { if (!('fetch' in _window)) return false; try { new Headers(); // eslint-disable-line no-new new Request(''); // eslint-disable-line no-new new Response(); // eslint-disable-line no-new return true; } catch (e) { return false; } } // Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default // https://caniuse.com/#feat=referrer-policy // It doesn't. And it throw exception instead of ignoring this parameter... // REF: https://github.com/getsentry/raven-js/issues/1233 function supportsReferrerPolicy() { if (!supportsFetch()) return false; try { // eslint-disable-next-line no-new new Request('pickleRick', { referrerPolicy: 'origin' }); return true; } catch (e) { return false; } } function supportsPromiseRejectionEvent() { return typeof PromiseRejectionEvent === 'function'; } function wrappedCallback(callback) { function dataCallback(data, original) { var normalizedData = callback(data) || data; if (original) { return original(normalizedData) || normalizedData; } return normalizedData; } return dataCallback; } function each(obj, callback) { var i, j; if (isUndefined(obj.length)) { for (i in obj) { if (hasKey(obj, i)) { callback.call(null, i, obj[i]); } } } else { j = obj.length; if (j) { for (i = 0; i < j; i++) { callback.call(null, i, obj[i]); } } } } function objectMerge(obj1, obj2) { if (!obj2) { return obj1; } each(obj2, function(key, value) { obj1[key] = value; }); return obj1; } /** * This function is only used for react-native. * react-native freezes object that have already been sent over the * js bridge. We need this function in order to check if the object is frozen. * So it's ok that objectFrozen returns false if Object.isFrozen is not * supported because it's not relevant for other "platforms". See related issue: * https://github.com/getsentry/react-native-sentry/issues/57 */ function objectFrozen(obj) { if (!Object.isFrozen) { return false; } return Object.isFrozen(obj); } function truncate(str, max) { if (typeof max !== 'number') { throw new Error('2nd argument to `truncate` function should be a number'); } if (typeof str !== 'string' || max === 0) { return str; } return str.length <= max ? str : str.substr(0, max) + '\u2026'; } /** * hasKey, a better form of hasOwnProperty * Example: hasKey(MainHostObject, property) === true/false * * @param {Object} host object to check property * @param {string} key to check */ function hasKey(object, key) { return Object.prototype.hasOwnProperty.call(object, key); } function joinRegExp(patterns) { // Combine an array of regular expressions and strings into one large regexp // Be mad. var sources = [], i = 0, len = patterns.length, pattern; for (; i < len; i++) { pattern = patterns[i]; if (isString(pattern)) { // If it's a string, we need to escape it // Taken from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions sources.push(pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1')); } else if (pattern && pattern.source) { // If it's a regexp already, we want to extract the source sources.push(pattern.source); } // Intentionally skip other cases } return new RegExp(sources.join('|'), 'i'); } function urlencode(o) { var pairs = []; each(o, function(key, value) { pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); }); return pairs.join('&'); } // borrowed from https://tools.ietf.org/html/rfc3986#appendix-B // intentionally using regex and not href parsing trick because React Native and other // environments where DOM might not be available function parseUrl(url) { if (typeof url !== 'string') return {}; var match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/); // coerce to undefined values to empty string so we don't get 'undefined' var query = match[6] || ''; var fragment = match[8] || ''; return { protocol: match[2], host: match[4], path: match[5], relative: match[5] + query + fragment // everything minus origin }; } function uuid4() { var crypto = _window.crypto || _window.msCrypto; if (!isUndefined(crypto) && crypto.getRandomValues) { // Use window.crypto API if available // eslint-disable-next-line no-undef var arr = new Uint16Array(8); crypto.getRandomValues(arr); // set 4 in byte 7 arr[3] = (arr[3] & 0xfff) | 0x4000; // set 2 most significant bits of byte 9 to '10' arr[4] = (arr[4] & 0x3fff) | 0x8000; var pad = function(num) { var v = num.toString(16); while (v.length < 4) { v = '0' + v; } return v; }; return ( pad(arr[0]) + pad(arr[1]) + pad(arr[2]) + pad(arr[3]) + pad(arr[4]) + pad(arr[5]) + pad(arr[6]) + pad(arr[7]) ); } else { // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } } /** * Given a child DOM element, returns a query-selector statement describing that * and its ancestors * e.g. [HTMLElement] => body > div > input#foo.btn[name=baz] * @param elem * @returns {string} */ function htmlTreeAsString(elem) { /* eslint no-extra-parens:0*/ var MAX_TRAVERSE_HEIGHT = 5, MAX_OUTPUT_LEN = 80, out = [], height = 0, len = 0, separator = ' > ', sepLength = separator.length, nextStr; while (elem && height++ < MAX_TRAVERSE_HEIGHT) { nextStr = htmlElementAsString(elem); // bail out if // - nextStr is the 'html' element // - the length of the string that would be created exceeds MAX_OUTPUT_LEN // (ignore this limit if we are on the first iteration) if ( nextStr === 'html' || (height > 1 && len + out.length * sepLength + nextStr.length >= MAX_OUTPUT_LEN) ) { break; } out.push(nextStr); len += nextStr.length; elem = elem.parentNode; } return out.reverse().join(separator); } /** * Returns a simple, query-selector representation of a DOM element * e.g. [HTMLElement] => input#foo.btn[name=baz] * @param HTMLElement * @returns {string} */ function htmlElementAsString(elem) { var out = [], className, classes, key, attr, i; if (!elem || !elem.tagName) { return ''; } out.push(elem.tagName.toLowerCase()); if (elem.id) { out.push('#' + elem.id); } className = elem.className; if (className && isString(className)) { classes = className.split(/\s+/); for (i = 0; i < classes.length; i++) { out.push('.' + classes[i]); } } var attrWhitelist = ['type', 'name', 'title', 'alt']; for (i = 0; i < attrWhitelist.length; i++) { key = attrWhitelist[i]; attr = elem.getAttribute(key); if (attr) { out.push('[' + key + '="' + attr + '"]'); } } return out.join(''); } /** * Returns true if either a OR b is truthy, but not both */ function isOnlyOneTruthy(a, b) { return !!(!!a ^ !!b); } /** * Returns true if both parameters are undefined */ function isBothUndefined(a, b) { return isUndefined(a) && isUndefined(b); } /** * Returns true if the two input exception interfaces have the same content */ function isSameException(ex1, ex2) { if (isOnlyOneTruthy(ex1, ex2)) return false; ex1 = ex1.values[0]; ex2 = ex2.values[0]; if (ex1.type !== ex2.type || ex1.value !== ex2.value) return false; // in case both stacktraces are undefined, we can't decide so default to false if (isBothUndefined(ex1.stacktrace, ex2.stacktrace)) return false; return isSameStacktrace(ex1.stacktrace, ex2.stacktrace); } /** * Returns true if the two input stack trace interfaces have the same content */ function isSameStacktrace(stack1, stack2) { if (isOnlyOneTruthy(stack1, stack2)) return false; var frames1 = stack1.frames; var frames2 = stack2.frames; // Exit early if stacktrace is malformed if (frames1 === undefined || frames2 === undefined) return false; // Exit early if frame count differs if (frames1.length !== frames2.length) return false; // Iterate through every frame; bail out if anything differs var a, b; for (var i = 0; i < frames1.length; i++) { a = frames1[i]; b = frames2[i]; if ( a.filename !== b.filename || a.lineno !== b.lineno || a.colno !== b.colno || a['function'] !== b['function'] ) return false; } return true; } /** * Polyfill a method * @param obj object e.g. `document` * @param name method name present on object e.g. `addEventListener` * @param replacement replacement function * @param track {optional} record instrumentation to an array */ function fill(obj, name, replacement, track) { if (obj == null) return; var orig = obj[name]; obj[name] = replacement(orig); obj[name].__raven__ = true; obj[name].__orig__ = orig; if (track) { track.push([obj, name, orig]); } } /** * Join values in array * @param input array of values to be joined together * @param delimiter string to be placed in-between values * @returns {string} */ function safeJoin(input, delimiter) { if (!isArray(input)) return ''; var output = []; for (var i = 0; i < input.length; i++) { try { output.push(String(input[i])); } catch (e) { output.push('[value cannot be serialized]'); } } return output.join(delimiter); } // Default Node.js REPL depth var MAX_SERIALIZE_EXCEPTION_DEPTH = 3; // 50kB, as 100kB is max payload size, so half sounds reasonable var MAX_SERIALIZE_EXCEPTION_SIZE = 50 * 1024; var MAX_SERIALIZE_KEYS_LENGTH = 40; function utf8Length(value) { return ~-encodeURI(value).split(/%..|./).length; } function jsonSize(value) { return utf8Length(JSON.stringify(value)); } function serializeValue(value) { if (typeof value === 'string') { var maxLength = 40; return truncate(value, maxLength); } else if ( typeof value === 'number' || typeof value === 'boolean' || typeof value === 'undefined' ) { return value; } var type = Object.prototype.toString.call(value); // Node.js REPL notation if (type === '[object Object]') return '[Object]'; if (type === '[object Array]') return '[Array]'; if (type === '[object Function]') return value.name ? '[Function: ' + value.name + ']' : '[Function]'; return value; } function serializeObject(value, depth) { if (depth === 0) return serializeValue(value); if (isPlainObject(value)) { return Object.keys(value).reduce(function(acc, key) { acc[key] = serializeObject(value[key], depth - 1); return acc; }, {}); } else if (Array.isArray(value)) { return value.map(function(val) { return serializeObject(val, depth - 1); }); } return serializeValue(value); } function serializeException(ex, depth, maxSize) { if (!isPlainObject(ex)) return ex; depth = typeof depth !== 'number' ? MAX_SERIALIZE_EXCEPTION_DEPTH : depth; maxSize = typeof depth !== 'number' ? MAX_SERIALIZE_EXCEPTION_SIZE : maxSize; var serialized = serializeObject(ex, depth); if (jsonSize(stringify(serialized)) > maxSize) { return serializeException(ex, depth - 1); } return serialized; } function serializeKeysForMessage(keys, maxLength) { if (typeof keys === 'number' || typeof keys === 'string') return keys.toString(); if (!Array.isArray(keys)) return ''; keys = keys.filter(function(key) { return typeof key === 'string'; }); if (keys.length === 0) return '[object has no keys]'; maxLength = typeof maxLength !== 'number' ? MAX_SERIALIZE_KEYS_LENGTH : maxLength; if (keys[0].length >= maxLength) return keys[0]; for (var usedKeys = keys.length; usedKeys > 0; usedKeys--) { var serialized = keys.slice(0, usedKeys).join(', '); if (serialized.length > maxLength) continue; if (usedKeys === keys.length) return serialized; return serialized + '\u2026'; } return ''; } function sanitize(input, sanitizeKeys) { if (!isArray(sanitizeKeys) || (isArray(sanitizeKeys) && sanitizeKeys.length === 0)) return input; var sanitizeRegExp = joinRegExp(sanitizeKeys); var sanitizeMask = '********'; var safeInput; try { safeInput = JSON.parse(stringify(input)); } catch (o_O) { return input; } function sanitizeWorker(workerInput) { if (isArray(workerInput)) { return workerInput.map(function(val) { return sanitizeWorker(val); }); } if (isPlainObject(workerInput)) { return Object.keys(workerInput).reduce(function(acc, k) { if (sanitizeRegExp.test(k)) { acc[k] = sanitizeMask; } else { acc[k] = sanitizeWorker(workerInput[k]); } return acc; }, {}); } return workerInput; } return sanitizeWorker(safeInput); } module.exports = { isObject: isObject, isError: isError, isErrorEvent: isErrorEvent, isDOMError: isDOMError, isDOMException: isDOMException, isUndefined: isUndefined, isFunction: isFunction, isPlainObject: isPlainObject, isString: isString, isArray: isArray, isEmptyObject: isEmptyObject, supportsErrorEvent: supportsErrorEvent, supportsDOMError: supportsDOMError, supportsDOMException: supportsDOMException, supportsFetch: supportsFetch, supportsReferrerPolicy: supportsReferrerPolicy, supportsPromiseRejectionEvent: supportsPromiseRejectionEvent, wrappedCallback: wrappedCallback, each: each, objectMerge: objectMerge, truncate: truncate, objectFrozen: objectFrozen, hasKey: hasKey, joinRegExp: joinRegExp, urlencode: urlencode, uuid4: uuid4, htmlTreeAsString: htmlTreeAsString, htmlElementAsString: htmlElementAsString, isSameException: isSameException, isSameStacktrace: isSameStacktrace, parseUrl: parseUrl, fill: fill, safeJoin: safeJoin, serializeException: serializeException, serializeKeysForMessage: serializeKeysForMessage, sanitize: sanitize }; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"7":7}],6:[function(_dereq_,module,exports){ (function (global){ var utils = _dereq_(5); /* TraceKit - Cross brower stack traces This was originally forked from github.com/occ/TraceKit, but has since been largely re-written and is now maintained as part of raven-js. Tests for this are in test/vendor. MIT license */ var TraceKit = { collectWindowErrors: true, debug: false }; // This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785) var _window = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; // global reference to slice var _slice = [].slice; var UNKNOWN_FUNCTION = '?'; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Error_types var ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/; function getLocationHref() { if (typeof document === 'undefined' || document.location == null) return ''; return document.location.href; } function getLocationOrigin() { if (typeof document === 'undefined' || document.location == null) return ''; // Oh dear IE10... if (!document.location.origin) { return ( document.location.protocol + '//' + document.location.hostname + (document.location.port ? ':' + document.location.port : '') ); } return document.location.origin; } /** * TraceKit.report: cross-browser processing of unhandled exceptions * * Syntax: * TraceKit.report.subscribe(function(stackInfo) { ... }) * TraceKit.report.unsubscribe(function(stackInfo) { ... }) * TraceKit.report(exception) * try { ...code... } catch(ex) { TraceKit.report(ex); } * * Supports: * - Firefox: full stack trace with line numbers, plus column number * on top frame; column number is not guaranteed * - Opera: full stack trace with line and column numbers * - Chrome: full stack trace with line and column numbers * - Safari: line and column number for the top frame only; some frames * may be missing, and column number is not guaranteed * - IE: line and column number for the top frame only; some frames * may be missing, and column number is not guaranteed * * In theory, TraceKit should work on all of the following versions: * - IE5.5+ (only 8.0 tested) * - Firefox 0.9+ (only 3.5+ tested) * - Opera 7+ (only 10.50 tested; versions 9 and earlier may require * Exceptions Have Stacktrace to be enabled in opera:config) * - Safari 3+ (only 4+ tested) * - Chrome 1+ (only 5+ tested) * - Konqueror 3.5+ (untested) * * Requires TraceKit.computeStackTrace. * * Tries to catch all unhandled exceptions and report them to the * subscribed handlers. Please note that TraceKit.report will rethrow the * exception. This is REQUIRED in order to get a useful stack trace in IE. * If the exception does not reach the top of the browser, you will only * get a stack trace from the point where TraceKit.report was called. * * Handlers receive a stackInfo object as described in the * TraceKit.computeStackTrace docs. */ TraceKit.report = (function reportModuleWrapper() { var handlers = [], lastArgs = null, lastException = null, lastExceptionStack = null; /** * Add a crash handler. * @param {Function} handler */ function subscribe(handler) { installGlobalHandler(); handlers.push(handler); } /** * Remove a crash handler. * @param {Function} handler */ function unsubscribe(handler) { for (var i = handlers.length - 1; i >= 0; --i) { if (handlers[i] === handler) { handlers.splice(i, 1); } } } /** * Remove all crash handlers. */ function unsubscribeAll() { uninstallGlobalHandler(); handlers = []; } /** * Dispatch stack information to all handlers. * @param {Object.} stack */ function notifyHandlers(stack, isWindowError) { var exception = null; if (isWindowError && !TraceKit.collectWindowErrors) { return; } for (var i in handlers) { if (handlers.hasOwnProperty(i)) { try { handlers[i].apply(null, [stack].concat(_slice.call(arguments, 2))); } catch (inner) { exception = inner; } } } if (exception) { throw exception; } } var _oldOnerrorHandler, _onErrorHandlerInstalled; /** * Ensures all global unhandled exceptions are recorded. * Supported by Gecko and IE. * @param {string} msg Error message. * @param {string} url URL of script that generated the exception. * @param {(number|string)} lineNo The line number at which the error * occurred. * @param {?(number|string)} colNo The column number at which the error * occurred. * @param {?Error} ex The actual Error object. */ function traceKitWindowOnError(msg, url, lineNo, colNo, ex) { var stack = null; // If 'ex' is ErrorEvent, get real Error from inside var exception = utils.isErrorEvent(ex) ? ex.error : ex; // If 'msg' is ErrorEvent, get real message from inside var message = utils.isErrorEvent(msg) ? msg.message : msg; if (lastExceptionStack) { TraceKit.computeStackTrace.augmentStackTraceWithInitialElement( lastExceptionStack, url, lineNo, message ); processLastException(); } else if (exception && utils.isError(exception)) { // non-string `exception` arg; attempt to extract stack trace // New chrome and blink send along a real error object // Let's just report that like a normal error. // See: https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror stack = TraceKit.computeStackTrace(exception); notifyHandlers(stack, true); } else { var location = { url: url, line: lineNo, column: colNo }; var name = undefined; var groups; if ({}.toString.call(message) === '[object String]') { var groups = message.match(ERROR_TYPES_RE); if (groups) { name = groups[1]; message = groups[2]; } } location.func = UNKNOWN_FUNCTION; stack = { name: name, message: message, url: getLocationHref(), stack: [location] }; notifyHandlers(stack, true); } if (_oldOnerrorHandler) { return _oldOnerrorHandler.apply(this, arguments); } return false; } function installGlobalHandler() { if (_onErrorHandlerInstalled) { return; } _oldOnerrorHandler = _window.onerror; _window.onerror = traceKitWindowOnError; _onErrorHandlerInstalled = true; } function uninstallGlobalHandler() { if (!_onErrorHandlerInstalled) { return; } _window.onerror = _oldOnerrorHandler; _onErrorHandlerInstalled = false; _oldOnerrorHandler = undefined; } function processLastException() { var _lastExceptionStack = lastExceptionStack, _lastArgs = lastArgs; lastArgs = null; lastExceptionStack = null; lastException = null; notifyHandlers.apply(null, [_lastExceptionStack, false].concat(_lastArgs)); } /** * Reports an unhandled Error to TraceKit. * @param {Error} ex * @param {?boolean} rethrow If false, do not re-throw the exception. * Only used for window.onerror to not cause an infinite loop of * rethrowing. */ function report(ex, rethrow) { var args = _slice.call(arguments, 1); if (lastExceptionStack) { if (lastException === ex) { return; // already caught by an inner catch block, ignore } else { processLastException(); } } var stack = TraceKit.computeStackTrace(ex); lastExceptionStack = stack; lastException = ex; lastArgs = args; // If the stack trace is incomplete, wait for 2 seconds for // slow slow IE to see if onerror occurs or not before reporting // this exception; otherwise, we will end up with an incomplete // stack trace setTimeout(function() { if (lastException === ex) { processLastException(); } }, stack.incomplete ? 2000 : 0); if (rethrow !== false) { throw ex; // re-throw to propagate to the top level (and cause window.onerror) } } report.subscribe = subscribe; report.unsubscribe = unsubscribe; report.uninstall = unsubscribeAll; return report; })(); /** * TraceKit.computeStackTrace: cross-browser stack traces in JavaScript * * Syntax: * s = TraceKit.computeStackTrace(exception) // consider using TraceKit.report instead (see below) * Returns: * s.name - exception name * s.message - exception message * s.stack[i].url - JavaScript or HTML file URL * s.stack[i].func - function name, or empty for anonymous functions (if guessing did not work) * s.stack[i].args - arguments passed to the function, if known * s.stack[i].line - line number, if known * s.stack[i].column - column number, if known * * Supports: * - Firefox: full stack trace with line numbers and unreliable column * number on top frame * - Opera 10: full stack trace with line and column numbers * - Opera 9-: full stack trace with line numbers * - Chrome: full stack trace with line and column numbers * - Safari: line and column number for the topmost stacktrace element * only * - IE: no line numbers whatsoever * * Tries to guess names of anonymous functions by looking for assignments * in the source code. In IE and Safari, we have to guess source file names * by searching for function bodies inside all page scripts. This will not * work for scripts that are loaded cross-domain. * Here be dragons: some function names may be guessed incorrectly, and * duplicate functions may be mismatched. * * TraceKit.computeStackTrace should only be used for tracing purposes. * Logging of unhandled exceptions should be done with TraceKit.report, * which builds on top of TraceKit.computeStackTrace and provides better * IE support by utilizing the window.onerror event to retrieve information * about the top of the stack. * * Note: In IE and Safari, no stack trace is recorded on the Error object, * so computeStackTrace instead walks its *own* chain of callers. * This means that: * * in Safari, some methods may be missing from the stack trace; * * in IE, the topmost function in the stack trace will always be the * caller of computeStackTrace. * * This is okay for tracing (because you are likely to be calling * computeStackTrace from the function you want to be the topmost element * of the stack trace anyway), but not okay for logging unhandled * exceptions (because your catch block will likely be far away from the * inner function that actually caused the exception). * */ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { // Contents of Exception in various browsers. // // SAFARI: // ex.message = Can't find variable: qq // ex.line = 59 // ex.sourceId = 580238192 // ex.sourceURL = http://... // ex.expressionBeginOffset = 96 // ex.expressionCaretOffset = 98 // ex.expressionEndOffset = 98 // ex.name = ReferenceError // // FIREFOX: // ex.message = qq is not defined // ex.fileName = http://... // ex.lineNumber = 59 // ex.columnNumber = 69 // ex.stack = ...stack trace... (see the example below) // ex.name = ReferenceError // // CHROME: // ex.message = qq is not defined // ex.name = ReferenceError // ex.type = not_defined // ex.arguments = ['aa'] // ex.stack = ...stack trace... // // INTERNET EXPLORER: // ex.message = ... // ex.name = ReferenceError // // OPERA: // ex.message = ...message... (see the example below) // ex.name = ReferenceError // ex.opera#sourceloc = 11 (pretty much useless, duplicates the info in ex.message) // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace' /** * Computes stack trace information from the stack property. * Chrome and Gecko use this property. * @param {Error} ex * @return {?Object.} Stack trace information. */ function computeStackTraceFromStackProp(ex) { if (typeof ex.stack === 'undefined' || !ex.stack) return; var chrome = /^\s*at (?:(.*?) ?\()?((?:file|https?|blob|chrome-extension|native|eval|webpack||[a-z]:|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i; var winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx(?:-web)|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i; // NOTE: blob urls are now supposed to always have an origin, therefore it's format // which is `blob:http://url/path/with-some-uuid`, is matched by `blob.*?:\/` as well var gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|moz-extension).*?:\/.*?|\[native code\]|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i; // Used to additionally parse URL/line/column from eval frames var geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i; var chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/; var lines = ex.stack.split('\n'); var stack = []; var submatch; var parts; var element; var reference = /^(.*) is undefined$/.exec(ex.message); for (var i = 0, j = lines.length; i < j; ++i) { if ((parts = chrome.exec(lines[i]))) { var isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line var isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line if (isEval && (submatch = chromeEval.exec(parts[2]))) { // throw out eval line/column and use top-most line/column number parts[2] = submatch[1]; // url parts[3] = submatch[2]; // line parts[4] = submatch[3]; // column } element = { url: !isNative ? parts[2] : null, func: parts[1] || UNKNOWN_FUNCTION, args: isNative ? [parts[2]] : [], line: parts[3] ? +parts[3] : null, column: parts[4] ? +parts[4] : null }; } else if ((parts = winjs.exec(lines[i]))) { element = { url: parts[2], func: parts[1] || UNKNOWN_FUNCTION, args: [], line: +parts[3], column: parts[4] ? +parts[4] : null }; } else if ((parts = gecko.exec(lines[i]))) { var isEval = parts[3] && parts[3].indexOf(' > eval') > -1; if (isEval && (submatch = geckoEval.exec(parts[3]))) { // throw out eval line/column and use top-most line number parts[3] = submatch[1]; parts[4] = submatch[2]; parts[5] = null; // no column when eval } else if (i === 0 && !parts[5] && typeof ex.columnNumber !== 'undefined') { // FireFox uses this awesome columnNumber property for its top frame // Also note, Firefox's column number is 0-based and everything else expects 1-based, // so adding 1 // NOTE: this hack doesn't work if top-most frame is eval stack[0].column = ex.columnNumber + 1; } element = { url: parts[3], func: parts[1] || UNKNOWN_FUNCTION, args: parts[2] ? parts[2].split(',') : [], line: parts[4] ? +parts[4] : null, column: parts[5] ? +parts[5] : null }; } else { continue; } if (!element.func && element.line) { element.func = UNKNOWN_FUNCTION; } if (element.url && element.url.substr(0, 5) === 'blob:') { // Special case for handling JavaScript loaded into a blob. // We use a synchronous AJAX request here as a blob is already in // memory - it's not making a network request. This will generate a warning // in the browser console, but there has already been an error so that's not // that much of an issue. var xhr = new XMLHttpRequest(); xhr.open('GET', element.url, false); xhr.send(null); // If we failed to download the source, skip this patch if (xhr.status === 200) { var source = xhr.responseText || ''; // We trim the source down to the last 300 characters as sourceMappingURL is always at the end of the file. // Why 300? To be in line with: https://github.com/getsentry/sentry/blob/4af29e8f2350e20c28a6933354e4f42437b4ba42/src/sentry/lang/javascript/processor.py#L164-L175 source = source.slice(-300); // Now we dig out the source map URL var sourceMaps = source.match(/\/\/# sourceMappingURL=(.*)$/); // If we don't find a source map comment or we find more than one, continue on to the next element. if (sourceMaps) { var sourceMapAddress = sourceMaps[1]; // Now we check to see if it's a relative URL. // If it is, convert it to an absolute one. if (sourceMapAddress.charAt(0) === '~') { sourceMapAddress = getLocationOrigin() + sourceMapAddress.slice(1); } // Now we strip the '.map' off of the end of the URL and update the // element so that Sentry can match the map to the blob. element.url = sourceMapAddress.slice(0, -4); } } } stack.push(element); } if (!stack.length) { return null; } return { name: ex.name, message: ex.message, url: getLocationHref(), stack: stack }; } /** * Adds information about the first frame to incomplete stack traces. * Safari and IE require this to get complete data on the first frame. * @param {Object.} stackInfo Stack trace information from * one of the compute* methods. * @param {string} url The URL of the script that caused an error. * @param {(number|string)} lineNo The line number of the script that * caused an error. * @param {string=} message The error generated by the browser, which * hopefully contains the name of the object that caused the error. * @return {boolean} Whether or not the stack information was * augmented. */ function augmentStackTraceWithInitialElement(stackInfo, url, lineNo, message) { var initial = { url: url, line: lineNo }; if (initial.url && initial.line) { stackInfo.incomplete = false; if (!initial.func) { initial.func = UNKNOWN_FUNCTION; } if (stackInfo.stack.length > 0) { if (stackInfo.stack[0].url === initial.url) { if (stackInfo.stack[0].line === initial.line) { return false; // already in stack trace } else if ( !stackInfo.stack[0].line && stackInfo.stack[0].func === initial.func ) { stackInfo.stack[0].line = initial.line; return false; } } } stackInfo.stack.unshift(initial); stackInfo.partial = true; return true; } else { stackInfo.incomplete = true; } return false; } /** * Computes stack trace information by walking the arguments.caller * chain at the time the exception occurred. This will cause earlier * frames to be missed but is the only way to get any stack trace in * Safari and IE. The top frame is restored by * {@link augmentStackTraceWithInitialElement}. * @param {Error} ex * @return {?Object.} Stack trace information. */ function computeStackTraceByWalkingCallerChain(ex, depth) { var functionName = /function\s+([_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)?\s*\(/i, stack = [], funcs = {}, recursion = false, parts, item, source; for ( var curr = computeStackTraceByWalkingCallerChain.caller; curr && !recursion; curr = curr.caller ) { if (curr === computeStackTrace || curr === TraceKit.report) { // console.log('skipping internal function'); continue; } item = { url: null, func: UNKNOWN_FUNCTION, line: null, column: null }; if (curr.name) { item.func = curr.name; } else if ((parts = functionName.exec(curr.toString()))) { item.func = parts[1]; } if (typeof item.func === 'undefined') { try { item.func = parts.input.substring(0, parts.input.indexOf('{')); } catch (e) {} } if (funcs['' + curr]) { recursion = true; } else { funcs['' + curr] = true; } stack.push(item); } if (depth) { // console.log('depth is ' + depth); // console.log('stack is ' + stack.length); stack.splice(0, depth); } var result = { name: ex.name, message: ex.message, url: getLocationHref(), stack: stack }; augmentStackTraceWithInitialElement( result, ex.sourceURL || ex.fileName, ex.line || ex.lineNumber, ex.message || ex.description ); return result; } /** * Computes a stack trace for an exception. * @param {Error} ex * @param {(string|number)=} depth */ function computeStackTrace(ex, depth) { var stack = null; depth = depth == null ? 0 : +depth; try { stack = computeStackTraceFromStackProp(ex); if (stack) { return stack; } } catch (e) { if (TraceKit.debug) { throw e; } } try { stack = computeStackTraceByWalkingCallerChain(ex, depth + 1); if (stack) { return stack; } } catch (e) { if (TraceKit.debug) { throw e; } } return { name: ex.name, message: ex.message, url: getLocationHref() }; } computeStackTrace.augmentStackTraceWithInitialElement = augmentStackTraceWithInitialElement; computeStackTrace.computeStackTraceFromStackProp = computeStackTraceFromStackProp; return computeStackTrace; })(); module.exports = TraceKit; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"5":5}],7:[function(_dereq_,module,exports){ /* json-stringify-safe Like JSON.stringify, but doesn't throw on circular references. Originally forked from https://github.com/isaacs/json-stringify-safe version 5.0.1 on 3/8/2017 and modified to handle Errors serialization and IE8 compatibility. Tests for this are in test/vendor. ISC license: https://github.com/isaacs/json-stringify-safe/blob/master/LICENSE */ exports = module.exports = stringify; exports.getSerialize = serializer; function indexOf(haystack, needle) { for (var i = 0; i < haystack.length; ++i) { if (haystack[i] === needle) return i; } return -1; } function stringify(obj, replacer, spaces, cycleReplacer) { return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces); } // https://github.com/ftlabs/js-abbreviate/blob/fa709e5f139e7770a71827b1893f22418097fbda/index.js#L95-L106 function stringifyError(value) { var err = { // These properties are implemented as magical getters and don't show up in for in stack: value.stack, message: value.message, name: value.name }; for (var i in value) { if (Object.prototype.hasOwnProperty.call(value, i)) { err[i] = value[i]; } } return err; } function serializer(replacer, cycleReplacer) { var stack = []; var keys = []; if (cycleReplacer == null) { cycleReplacer = function(key, value) { if (stack[0] === value) { return '[Circular ~]'; } return '[Circular ~.' + keys.slice(0, indexOf(stack, value)).join('.') + ']'; }; } return function(key, value) { if (stack.length > 0) { var thisPos = indexOf(stack, this); ~thisPos ? stack.splice(thisPos + 1) : stack.push(this); ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key); if (~indexOf(stack, value)) { value = cycleReplacer.call(this, key, value); } } else { stack.push(value); } return replacer == null ? value instanceof Error ? stringifyError(value) : value : replacer.call(this, key, value); }; } },{}],8:[function(_dereq_,module,exports){ /* * JavaScript MD5 * https://github.com/blueimp/JavaScript-MD5 * * Copyright 2011, Sebastian Tschan * https://blueimp.net * * Licensed under the MIT license: * https://opensource.org/licenses/MIT * * Based on * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * Digest Algorithm, as defined in RFC 1321. * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet * Distributed under the BSD License * See http://pajhome.org.uk/crypt/md5 for more info. */ /* * Add integers, wrapping at 2^32. This uses 16-bit operations internally * to work around bugs in some JS interpreters. */ function safeAdd(x, y) { var lsw = (x & 0xffff) + (y & 0xffff); var msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xffff); } /* * Bitwise rotate a 32-bit number to the left. */ function bitRotateLeft(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)); } /* * These functions implement the four basic operations the algorithm uses. */ function md5cmn(q, a, b, x, s, t) { return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b); } function md5ff(a, b, c, d, x, s, t) { return md5cmn((b & c) | (~b & d), a, b, x, s, t); } function md5gg(a, b, c, d, x, s, t) { return md5cmn((b & d) | (c & ~d), a, b, x, s, t); } function md5hh(a, b, c, d, x, s, t) { return md5cmn(b ^ c ^ d, a, b, x, s, t); } function md5ii(a, b, c, d, x, s, t) { return md5cmn(c ^ (b | ~d), a, b, x, s, t); } /* * Calculate the MD5 of an array of little-endian words, and a bit length. */ function binlMD5(x, len) { /* append padding */ x[len >> 5] |= 0x80 << (len % 32); x[(((len + 64) >>> 9) << 4) + 14] = len; var i; var olda; var oldb; var oldc; var oldd; var a = 1732584193; var b = -271733879; var c = -1732584194; var d = 271733878; for (i = 0; i < x.length; i += 16) { olda = a; oldb = b; oldc = c; oldd = d; a = md5ff(a, b, c, d, x[i], 7, -680876936); d = md5ff(d, a, b, c, x[i + 1], 12, -389564586); c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330); a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426); c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); b = md5ff(b, c, d, a, x[i + 7], 22, -45705983); a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417); c = md5ff(c, d, a, b, x[i + 10], 17, -42063); b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162); a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); d = md5ff(d, a, b, c, x[i + 13], 12, -40341101); c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329); a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632); c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); b = md5gg(b, c, d, a, x[i], 20, -373897302); a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); d = md5gg(d, a, b, c, x[i + 10], 9, 38016083); c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); b = md5gg(b, c, d, a, x[i + 4], 20, -405537848); a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690); c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501); a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); d = md5gg(d, a, b, c, x[i + 2], 9, -51403784); c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734); a = md5hh(a, b, c, d, x[i + 5], 4, -378558); d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463); c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); b = md5hh(b, c, d, a, x[i + 14], 23, -35309556); a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353); c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640); a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); d = md5hh(d, a, b, c, x[i], 11, -358537222); c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); b = md5hh(b, c, d, a, x[i + 6], 23, 76029189); a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); d = md5hh(d, a, b, c, x[i + 12], 11, -421815835); c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); b = md5hh(b, c, d, a, x[i + 2], 23, -995338651); a = md5ii(a, b, c, d, x[i], 6, -198630844); d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415); c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); b = md5ii(b, c, d, a, x[i + 5], 21, -57434055); a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606); c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799); a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); d = md5ii(d, a, b, c, x[i + 15], 10, -30611744); c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649); a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379); c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); b = md5ii(b, c, d, a, x[i + 9], 21, -343485551); a = safeAdd(a, olda); b = safeAdd(b, oldb); c = safeAdd(c, oldc); d = safeAdd(d, oldd); } return [a, b, c, d]; } /* * Convert an array of little-endian words to a string */ function binl2rstr(input) { var i; var output = ''; var length32 = input.length * 32; for (i = 0; i < length32; i += 8) { output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xff); } return output; } /* * Convert a raw string to an array of little-endian words * Characters >255 have their high-byte silently ignored. */ function rstr2binl(input) { var i; var output = []; output[(input.length >> 2) - 1] = undefined; for (i = 0; i < output.length; i += 1) { output[i] = 0; } var length8 = input.length * 8; for (i = 0; i < length8; i += 8) { output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (i % 32); } return output; } /* * Calculate the MD5 of a raw string */ function rstrMD5(s) { return binl2rstr(binlMD5(rstr2binl(s), s.length * 8)); } /* * Calculate the HMAC-MD5, of a key and some data (raw strings) */ function rstrHMACMD5(key, data) { var i; var bkey = rstr2binl(key); var ipad = []; var opad = []; var hash; ipad[15] = opad[15] = undefined; if (bkey.length > 16) { bkey = binlMD5(bkey, key.length * 8); } for (i = 0; i < 16; i += 1) { ipad[i] = bkey[i] ^ 0x36363636; opad[i] = bkey[i] ^ 0x5c5c5c5c; } hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); return binl2rstr(binlMD5(opad.concat(hash), 512 + 128)); } /* * Convert a raw string to a hex string */ function rstr2hex(input) { var hexTab = '0123456789abcdef'; var output = ''; var x; var i; for (i = 0; i < input.length; i += 1) { x = input.charCodeAt(i); output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f); } return output; } /* * Encode a string as utf-8 */ function str2rstrUTF8(input) { return unescape(encodeURIComponent(input)); } /* * Take string arguments and return either raw or hex encoded strings */ function rawMD5(s) { return rstrMD5(str2rstrUTF8(s)); } function hexMD5(s) { return rstr2hex(rawMD5(s)); } function rawHMACMD5(k, d) { return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d)); } function hexHMACMD5(k, d) { return rstr2hex(rawHMACMD5(k, d)); } function md5(string, key, raw) { if (!key) { if (!raw) { return hexMD5(string); } return rawMD5(string); } if (!raw) { return hexHMACMD5(key, string); } return rawHMACMD5(key, string); } module.exports = md5; },{}]},{},[4])(4) }); PK !< Síà 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 !<÷,rUrU 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/. */ 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) { const match = (/^https?:\/\/[^/:]{1,4000}\/?$/i).exec(url); if (!match) { 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; } const match = (/^https?:\/\/[^/:]{1,4000}/i).exec(url); if (match) { return match[0]; } 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-next-line no-control-regex filenameTitle = filenameTitle.replace(/[:\\<>/!@&?"*.|\x00-\x1F]/g, " "); filenameTitle = filenameTitle.replace(/\s{1,4000}/g, " "); const filenameDate = new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000).toISOString().substring(0, 10); let clipFilename = `Screenshot_${filenameDate} ${filenameTitle}`; const clipFilenameBytesSize = clipFilename.length * 2; // JS STrings are UTF-16 if (clipFilenameBytesSize > 251) { // 255 bytes (Usual filesystems max) - 4 for the ".png" file extension string const excedingchars = (clipFilenameBytesSize - 246) / 2; // 251 - 5 for ellipsis "[...]" clipFilename = clipFilename.substring(0, clipFilename.length - excedingchars); clipFilename = clipFilename + "[...]"; } 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 deviceId 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 !<Â'Ä/kkbuild/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.20; 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 !<;B™[ÈÈ 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 !<+ é‘££ 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 */ "use strict"; this.clipboard = (function() { const exports = {}; exports.copy = function(text) { return new Promise((resolve, reject) => { const element = document.createElement("iframe"); element.src = browser.extension.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 !<[ô¥ÝþþdomainFromUrl.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._-]/ig, ""); if (!domain) { domain = "site"; } } return domain; }; })(); null; PK !<=ÜEuexperiments/screenshots/api.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, AppConstants, Services, ExtensionAPI */ "use strict"; ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm"); ChromeUtils.defineModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); this.screenshots = class extends ExtensionAPI { getAPI(context) { const {extension} = context; return { experiments: { screenshots: { // If you are checking for 'nightly', also check for 'nightly-try'. // // Otherwise, just use the standard builds, but be aware of the many // non-standard options that also exist (as of August 2018). // // Standard builds: // 'esr' - ESR channel // 'release' - release channel // 'beta' - beta channel // 'nightly' - nightly channel // Non-standard / deprecated builds: // 'aurora' - deprecated aurora channel (still observed in dxr) // 'default' - local builds from source // 'nightly-try' - nightly Try builds (QA may occasionally need to test with these) getUpdateChannel() { return AppConstants.MOZ_UPDATE_CHANNEL; }, isHistoryEnabled() { return Services.prefs.getBoolPref("places.history.enabled", true); }, isUploadDisabled() { return Services.prefs.getBoolPref("extensions.screenshots.upload-disabled", false); }, }, }, }; } }; PK !<5mÛ  #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 }, { "name": "isUploadDisabled", "type": "function", "description": "Returns the value of the 'extensions.screenshots.upload-disabled' preference", "parameters": [], "async": true } ] } ] PK ! PK !< þÂ?--icons/cloud.svg PK !<¦Tí~ssicons/copied-notification.svg PK !<¸ f³NNicons/copy.svg PK ! PK !< khd..icons/download.svg PK !<»ÆŒ›ÞÞicons/help-16.svg PK !<ðÎó@''icons/icon-highlight-32-v2.svg PK !<æúleeicons/icon-v2.svg PK !<8Å,½½(icons/icon-welcome-face-without-eyes.svg PK !<:‰ì$$icons/menu-fullpage.svg PK !<dtW--icons/menu-myshot-white.svg PK !<õZœicons/menu-myshot.svg PK !<:(ðÓÓicons/menu-visible.svg PK !<ÐjÎxxlog.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 buildSettings */ /* eslint-disable no-console */ "use strict"; this.log = (function() { const exports = {}; const levels = ["debug", "info", "warn", "error"]; if (!levels.includes(buildSettings.logLevel)) { console.warn("Invalid buildSettings.logLevel:", buildSettings.logLevel); } const shouldLog = {}; { let startLogging = false; for (const level of levels) { if (buildSettings.logLevel === level) { startLogging = true; } if (startLogging) { shouldLog[level] = true; } } } function stub() {} exports.debug = exports.info = exports.warn = exports.error = stub; if (shouldLog.debug) { exports.debug = console.debug; } if (shouldLog.info) { exports.info = console.info; } if (shouldLog.warn) { exports.warn = console.warn; } if (shouldLog.error) { exports.error = console.error; } return exports; })(); null; PK !", "homepage_url": "https://github.com/mozilla-services/screenshots", "incognito": "spanning", "applications": { "gecko": { "id": "screenshots@mozilla.org", "strict_min_version": "57.0a1" } }, "l10n_resources": [ "browser/screenshots.ftl" ], "background": { "scripts": [ "build/buildSettings.js", "background/startBackground.js" ] }, "commands": { "take-screenshot": { "suggested_key": { "default": "Ctrl+Shift+S" }, "description": "Open the Firefox Screenshots UI" } }, "content_scripts": [ { "matches": ["https://screenshots.firefox.com/*"], "js": [ "build/buildSettings.js", "log.js", "catcher.js", "selector/callBackground.js", "sitehelper.js" ] } ], "page_action": { "browser_style": true, "default_icon" : { "32": "icons/icon-v2.svg" }, "default_title": "__MSG_screenshots-context-menu__", "show_matches": ["", "about:reader*"], "pinned": false }, "icons": { "32": "icons/icon-v2.svg" }, "web_accessible_resources": [ "blank.html", "icons/cancel.svg", "icons/download.svg", "icons/copy.svg", "icons/icon-256.png", "icons/help-16.svg", "icons/menu-fullpage.svg", "icons/menu-visible.svg", "icons/menu-myshot.svg", "icons/icon-welcome-face-without-eyes.svg" ], "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 !<Éx{_ _ 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", "makeUuid.js", "manifest.json", "moz.build", "randomString.js", "sitehelper.js", ] FINAL_TARGET_FILES.features["screenshots@mozilla.org"]["background"] += [ "background/analytics.js", "background/auth.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/buildSettings.js", "build/inlineSelectionCss.js", "build/raven.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"]["icons"] += [ "icons/cancel.svg", "icons/cloud.svg", "icons/copied-notification.svg", "icons/copy.svg", "icons/download-white.svg", "icons/download.svg", "icons/help-16.svg", "icons/icon-highlight-32-v2.svg", "icons/icon-v2.svg", "icons/icon-welcome-face-without-eyes.svg", "icons/menu-fullpage.svg", "icons/menu-myshot-white.svg", "icons/menu-myshot.svg", "icons/menu-visible.svg", ] 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 ! { 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 !<ô\6 6 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 !<ôZûQÕÕselector/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, documentMetadata, util, uicontrol, ui, catcher */ /* globals buildSettings, domainFromUrl, randomString, shot, blobConverters */ "use strict"; this.shooter = (function() { // eslint-disable-line no-unused-vars const exports = {}; const { AbstractShot } = shot; const RANDOM_STRING_LENGTH = 16; const MAX_CANVAS_DIMENSION = 32767; let backend; let shotObject; const callBackground = global.callBackground; const clipboard = global.clipboard; function regexpEscape(str) { // http://stackoverflow.com/questions/3115150/how-to-escape-regular-expression-special-characters-using-javascript return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); } function sanitizeError(data) { const href = new RegExp(regexpEscape(window.location.href), "g"); const origin = new RegExp(`${regexpEscape(window.location.origin)}[^ \t\n\r",>]*`, "g"); const json = JSON.stringify(data) .replace(href, "REDACTED_HREF") .replace(origin, "REDACTED_URL"); const result = JSON.parse(json); return result; } catcher.registerHandler((errorObj) => { callBackground("reportError", sanitizeError(errorObj)); }); function hideUIFrame() { ui.iframe.hide(); return Promise.resolve(null); } function screenshotPage(dataUrl, selectedPos, type, screenshotTaskFn) { let promise = Promise.resolve(dataUrl); if (!dataUrl) { let isFullPage = type === "fullPage" || type == "fullPageTruncated"; promise = callBackground( "screenshotPage", selectedPos.toJSON(), isFullPage, window.devicePixelRatio); } catcher.watchPromise(promise.then((dataUrl) => { screenshotTaskFn(dataUrl); })); } exports.downloadShot = function(selectedPos, previewDataUrl, type) { const shotPromise = previewDataUrl ? Promise.resolve(previewDataUrl) : hideUIFrame(); catcher.watchPromise(shotPromise.then(dataUrl => { screenshotPage(dataUrl, selectedPos, type, url => { let type = blobConverters.getTypeFromDataUrl(url); type = type ? type.split("/", 2)[1] : null; shotObject.delAllClips(); shotObject.addClip({ createdDate: Date.now(), image: { url, type, 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}); } callBackground("sendEvent", ...args); }; catcher.watchFunction(() => { shotObject = new AbstractShot( backend, randomString(RANDOM_STRING_LENGTH) + "/" + domainFromUrl(location), { origin: shot.originFromUrl(location.href), } ); shotObject.update(documentMetadata()); })(); return exports; })(); null; PK !< ™c)?s?sselector/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 log, util, catcher, inlineSelectionCss, callBackground, assertIsTrusted, assertIsBlankDocument, buildSettings 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("myshots-button") || 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.extension.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 highContrastCheck(win) { const doc = win.document; const el = doc.createElement("div"); el.style.backgroundImage = "url('#')"; el.style.display = "none"; doc.body.appendChild(el); const computed = win.getComputedStyle(el); doc.body.removeChild(el); // When Windows is in High Contrast mode, Firefox replaces background // image URLs with the string "none". return (computed && computed.backgroundImage === "none"); } const showMyShots = exports.showMyShots = function() { return window.hasAnyShots; }; function initializeIframe() { const el = document.createElement("iframe"); el.src = browser.extension.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"; 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("position", "absolute", "important"); this.element.style.setProperty("background-color", "transparent"); 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"; catcher.watchPromise(callBackground("sendEvent", "internal", "unhide-selection-frame")); if (highContrastCheck(this.element.contentWindow)) { this.element.contentDocument.body.classList.add("hcm"); } 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.setProperty("background-color", "transparent"); this.element.style.width = "100%"; this.element.style.height = "100%"; 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 = `
${showMyShots() ? `
` : ""}
`; 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"); if (showMyShots()) { overlay.querySelector(".myshots-button").addEventListener( "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onOpenMyShots))); } 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"; catcher.watchPromise(callBackground("sendEvent", "internal", "unhide-preselection-frame")); if (highContrastCheck(this.element.contentWindow)) { this.element.contentDocument.body.classList.add("hcm"); } 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.setProperty("background-color", "transparent"); this.element.style.height = "100%"; this.element.style.width = "100%"; 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"; catcher.watchPromise(callBackground("sendEvent", "internal", "unhide-preview-frame")); 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.save && this.save) { // We use onclick here because we don't want addEventListener // to add multiple event handlers to the same button this.save.removeAttribute("disabled"); this.save.onclick = watchFunction(assertIsTrusted((e) => { this.save.setAttribute("disabled", "true"); callbacks.save(e); })); this.save.style.display = ""; } else if (this.save) { this.save.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 = browser.extension.getURL("icons/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 = browser.extension.getURL("icons/copy.svg"); copy.appendChild(copyImg); copy.appendChild(copyString); buttons.appendChild(copy); const download = makeEl("button", "highlight-button-download"); const downloadImg = makeEl("img"); downloadImg.src = browser.extension.getURL("icons/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; }, clearSaveDisabled() { if (!this.save) { // Happens if we try to remove the disabled status after the worker // has been shut down return; } this.save.removeAttribute("disabled"); }, 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 !<Ì Ù8mmselector/uicontrol.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, catcher, util, ui, slides */ /* globals shooter, callBackground, selectorLoader, assertIsTrusted, buildSettings, selection */ "use strict"; this.uicontrol = (function() { const exports = {}; /** ******************************************************** * selection */ /* States: "crosshairs": Nothing has happened, and the crosshairs will follow the movement of the mouse "draggingReady": The user has pressed the mouse button, but hasn't moved enough to create a selection "dragging": The user has pressed down a mouse button, and is dragging out an area far enough to show a selection "selected": The user has selected an area "resizing": The user is resizing the selection "cancelled": Everything has been cancelled "previewing": The user is previewing the full-screen/visible image A mousedown goes from crosshairs to dragging. A mouseup goes from dragging to selected A click outside of the selection goes from selected to crosshairs A click on one of the draggers goes from selected to resizing State variables: state (string, one of the above) mousedownPos (object with x/y during draggingReady, shows where the selection started) selectedPos (object with x/y/h/w during selected or dragging, gives the entire selection) resizeDirection (string: top, topLeft, etc, during resizing) resizeStartPos (x/y position where resizing started) mouseupNoAutoselect (true if a mouseup in draggingReady should not trigger autoselect) */ const { watchFunction, watchPromise } = catcher; const MAX_PAGE_HEIGHT = buildSettings.maxImageHeight; const MAX_PAGE_WIDTH = buildSettings.maxImageWidth; // An autoselection smaller than these will be ignored entirely: const MIN_DETECT_ABSOLUTE_HEIGHT = 10; const MIN_DETECT_ABSOLUTE_WIDTH = 30; // An autoselection smaller than these will not be preferred: const MIN_DETECT_HEIGHT = 30; const MIN_DETECT_WIDTH = 100; // An autoselection bigger than either of these will be ignored: const MAX_DETECT_HEIGHT = Math.max(window.innerHeight + 100, 700); const MAX_DETECT_WIDTH = Math.max(window.innerWidth + 100, 1000); // This is how close (in pixels) you can get to the edge of the window and then // it will scroll: const SCROLL_BY_EDGE = 20; // This is how wide the inboard scrollbars are, generally 0 except on Mac const SCROLLBAR_WIDTH = (window.navigator.platform.match(/Mac/i)) ? 17 : 0; const { Selection } = selection; const { sendEvent } = shooter; const log = global.log; function round10(n) { return Math.floor(n / 10) * 10; } function eventOptionsForBox(box) { return { cd1: round10(Math.abs(box.bottom - box.top)), cd2: round10(Math.abs(box.right - box.left)), }; } function eventOptionsForResize(boxStart, boxEnd) { return { cd1: round10( (boxEnd.bottom - boxEnd.top) - (boxStart.bottom - boxStart.top)), cd2: round10( (boxEnd.right - boxEnd.left) - (boxStart.right - boxStart.left)), }; } function eventOptionsForMove(posStart, posEnd) { return { cd1: round10(posEnd.y - posStart.y), cd2: round10(posEnd.x - posStart.x), }; } function downloadShot() { const previewDataUrl = (captureType === "fullPageTruncated") ? null : dataUrl; // Downloaded shots don't have dimension limits removeDimensionLimitsOnFullPageShot(); shooter.downloadShot(selectedPos, previewDataUrl, captureType); } function copyShot() { const previewDataUrl = (captureType === "fullPageTruncated") ? null : dataUrl; // Copied shots don't have dimension limits removeDimensionLimitsOnFullPageShot(); shooter.copyShot(selectedPos, previewDataUrl, captureType); } /** ********************************************* * State and stateHandlers infrastructure */ // This enumerates all the anchors on the selection, and what part of the // selection they move: const movements = { topLeft: ["x1", "y1"], top: [null, "y1"], topRight: ["x2", "y1"], left: ["x1", null], right: ["x2", null], bottomLeft: ["x1", "y2"], bottom: [null, "y2"], bottomRight: ["x2", "y2"], move: ["*", "*"], }; const doNotAutoselectTags = { H1: true, H2: true, H3: true, H4: true, H5: true, H6: true, }; let captureType; function removeDimensionLimitsOnFullPageShot() { if (captureType === "fullPageTruncated") { captureType = "fullPage"; selectedPos = new Selection( 0, 0, getDocumentWidth(), getDocumentHeight()); } } const standardDisplayCallbacks = { cancel: () => { 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(); }, onOpenMyShots: () => { sendEvent("goto-myshots", "selection-button"); callBackground("openMyShots") .then(() => exports.deactivate()) .catch(() => { // Handled in communication.js }); }, onClickVisible: () => { 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: () => { 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"); } 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 = (el) => { if (el.nodeType !== document.ELEMENT_NODE) { return false; } if (el.tagName === "IMG") { const rect = el.getBoundingClientRect(); return rect.width >= this.minAutoImageWidth && rect.height >= this.minAutoImageHeight; } const display = window.getComputedStyle(el).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"); }, 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"); }, _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]/ig, ""); 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"); callBackground("closeSelector"); 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(eventName, 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[eventName]) { return handler[eventName](event); } return undefined; }).bind(null, eventName))); 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 Save or 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 !<ÍÁÖ1s s 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 !<ÚZþ þ 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() { // This gives us the content's copy of XMLHttpRequest, instead of the wrapped // copy that this content script gets: const ContentXMLHttpRequest = content.XMLHttpRequest; 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})); } /** Set the cookie, even if third-party cookies are disabled in this browser (when they are disabled, login from the background page won't set cookies) */ function sendBackupCookieRequest(authHeaders) { // See https://bugzilla.mozilla.org/show_bug.cgi?id=1295660 // This bug would allow us to access window.content.XMLHttpRequest, and get // a safer (not overridable by content) version of the object. // This is a very minimal attempt to verify that the XMLHttpRequest object we got // is legitimate. It is not a good test. if (Object.toString.apply(ContentXMLHttpRequest) !== "function XMLHttpRequest() {\n [native code]\n}") { console.warn("Insecure copy of XMLHttpRequest"); return; } const req = new ContentXMLHttpRequest(); req.open("POST", "/api/set-login-cookie"); for (const name in authHeaders) { req.setRequestHeader(name, authHeaders[name]); } req.send(""); req.onload = () => { if (req.status !== 200) { console.warn("Attempt to set Screenshots cookie via /api/set-login-cookie failed:", req.status, req.statusText, req.responseText); } }; } registerListener("delete-everything", catcher.watchFunction((event) => { // FIXME: reset some data in the add-on }, false)); registerListener("request-login", catcher.watchFunction((event) => { const shotId = event.detail; catcher.watchPromise(callBackground("getAuthInfo", shotId || null).then((info) => { if (info) { sendBackupCookieRequest(info.authHeaders); sendCustomEvent("login-successful", {deviceId: info.deviceId, accountId: info.accountId, isOwner: info.isOwner, backupCookieRequest: true}); } })); })); 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 !<¡VÔxxassertIsBlankDocument.jsPK !