Edit C:\Program Files\Mozilla Firefox\browser\features\screenshots@mozilla.org.xpi
PK !<?V?x x assertIsBlankDocument.js/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /** 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 !<JV??g g assertIsTrusted.js/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /** For use with addEventListener, assures that any events have event.isTrusted set to true https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted Should be applied *inside* catcher.watchFunction */ this.assertIsTrusted = function assertIsTrusted(handlerFunction) { return function(event) { if (!event) { const exc = new Error("assertIsTrusted did not get an event"); exc.noPopup = true; throw exc; } if (!event.isTrusted) { const exc = new Error(`Received untrusted event (type: ${event.type})`); exc.noPopup = true; throw exc; } return handlerFunction.call(this, event); }; }; null; PK !<"?r?/ ?/ background/analytics.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 main, auth, browser, catcher, deviceInfo, communication, log */ "use strict"; this.analytics = (function() { const exports = {}; const GA_PORTION = 0.1; // 10% of users will send to the server/GA // This is set from storage, or randomly; if it is less that GA_PORTION then we send analytics: let myGaSegment = 1; let telemetryPrefKnown = false; let telemetryEnabled; // If we ever get a 410 Gone response (or 404) from the server, we'll stop trying to send events for the rest // of the session let hasReturnedGone = false; // If there's this many entirely failed responses (e.g., server can't be contacted), then stop sending events // for the rest of the session: let serverFailedResponses = 3; const EVENT_BATCH_DURATION = 1000; // ms for setTimeout let pendingEvents = []; let pendingTimings = []; let eventsTimeoutHandle, timingsTimeoutHandle; const fetchOptions = { method: "POST", mode: "cors", headers: { "content-type": "application/json" }, credentials: "include", }; function shouldSendEvents() { return !hasReturnedGone && serverFailedResponses > 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?)7 7 background/selectorLoader.js/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* globals 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: ["<all_urls>", "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 !<t[?9? ? blank.html<!-- 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/. --> <html></html> PK !<iD?r? ? blobConverters.js/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ this.blobConverters = (function() { const exports = {}; exports.dataUrlToBlob = function(url) { const binary = atob(url.split(",", 2)[1]); let contentType = exports.getTypeFromDataUrl(url); if (contentType !== "image/png" && contentType !== "image/jpeg") { contentType = "image/png"; } const data = Uint8Array.from(binary, char => char.charCodeAt(0)); const blob = new Blob([data], {type: contentType}); return blob; }; exports.getTypeFromDataUrl = function(url) { let contentType = url.split(",", 1)[0]; contentType = contentType.split(";", 1)[0]; contentType = contentType.split(":", 2)[1]; return contentType; }; exports.blobToArray = function(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.addEventListener("loadend", function() { resolve(reader.result); }); reader.readAsArrayBuffer(blob); }); }; exports.blobToDataUrl = function(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.addEventListener("loadend", function() { resolve(reader.result); }); reader.readAsDataURL(blob); }); }; return exports; })(); null; PK !<f?WQ? ? build/buildSettings.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/. */ window.buildSettings = { defaultSentryDsn: "", logLevel: "" || "warn", captureText: ("" === "true"), uploadBinary: ("" === "true"), pngToJpegCutoff: parseInt("" || 2500000, 10), maxImageHeight: parseInt("" || 10000, 10), maxImageWidth: parseInt("" || 10000, 10) }; null; PK !<[C??O ?O build/inlineSelectionCss.js/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* Created from build/server/static/css/inline-selection.css */ window.inlineSelectionCss = ` .button, .highlight-button-cancel, .highlight-button-download, .highlight-button-copy { display: flex; align-items: center; justify-content: center; border: 0; border-radius: 3px; cursor: pointer; font-size: 16px; font-weight: 400; height: 40px; min-width: 40px; outline: none; padding: 0 10px; position: relative; text-align: center; text-decoration: none; transition: background 150ms cubic-bezier(0.07, 0.95, 0, 1), border 150ms cubic-bezier(0.07, 0.95, 0, 1); user-select: none; white-space: nowrap; } .button.hidden, .hidden.highlight-button-cancel, .hidden.highlight-button-download, .hidden.highlight-button-copy { display: none; } .button.small, .small.highlight-button-cancel, .small.highlight-button-download, .small.highlight-button-copy { height: 32px; line-height: 32px; padding: 0 8px; } .button.active, .active.highlight-button-cancel, .active.highlight-button-download, .active.highlight-button-copy { background-color: #dedede; } .button.tiny, .tiny.highlight-button-cancel, .tiny.highlight-button-download, .tiny.highlight-button-copy { font-size: 14px; height: 26px; border: 1px solid #c7c7c7; } .button.tiny:hover, .tiny.highlight-button-cancel:hover, .tiny.highlight-button-download:hover, .tiny.highlight-button-copy:hover, .button.tiny:focus, .tiny.highlight-button-cancel:focus, .tiny.highlight-button-download:focus, .tiny.highlight-button-copy:focus { background: #ededf0; border-color: #989898; } .button.tiny:active, .tiny.highlight-button-cancel:active, .tiny.highlight-button-download:active, .tiny.highlight-button-copy:active { background: #dedede; border-color: #989898; } .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-download, .block-button.highlight-button-copy { display: flex; align-items: center; justify-content: center; box-sizing: border-box; border: 0; border-inline-end: 1px solid #c7c7c7; box-shadow: 0; border-radius: 0; flex-shrink: 0; font-size: 20px; height: 100px; line-height: 100%; overflow: hidden; } @media (max-width: 719px) { .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-download, .block-button.highlight-button-copy { justify-content: flex-start; font-size: 16px; height: 72px; margin-inline-end: 10px; padding: 0 5px; } } .button.block-button:hover, .block-button.highlight-button-cancel:hover, .block-button.highlight-button-download:hover, .block-button.highlight-button-copy:hover { background: #ededf0; } .button.block-button:active, .block-button.highlight-button-cancel:active, .block-button.highlight-button-download:active, .block-button.highlight-button-copy:active { background: #dedede; } .button.download, .download.highlight-button-cancel, .download.highlight-button-download, .download.highlight-button-copy, .button.edit, .edit.highlight-button-cancel, .edit.highlight-button-download, .edit.highlight-button-copy, .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-download, .trash.highlight-button-copy, .button.share, .share.highlight-button-cancel, .share.highlight-button-download, .share.highlight-button-copy, .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-download, .flag.highlight-button-copy { background-repeat: no-repeat; background-size: 50%; background-position: center; margin-inline-end: 10px; transition: background-color 150ms cubic-bezier(0.07, 0.95, 0, 1); } .button.download, .download.highlight-button-cancel, .download.highlight-button-download, .download.highlight-button-copy { background-image: url("../img/icon-download.svg"); } .button.download:hover, .download.highlight-button-cancel:hover, .download.highlight-button-download:hover, .download.highlight-button-copy:hover { background-color: #ededf0; } .button.download:active, .download.highlight-button-cancel:active, .download.highlight-button-download:active, .download.highlight-button-copy:active { background-color: #dedede; } .button.share, .share.highlight-button-cancel, .share.highlight-button-download, .share.highlight-button-copy { background-image: url("../img/icon-share.svg"); } .button.share:hover, .share.highlight-button-cancel:hover, .share.highlight-button-download:hover, .share.highlight-button-copy:hover { background-color: #ededf0; } .button.share.active, .share.active.highlight-button-cancel, .share.active.highlight-button-download, .share.active.highlight-button-copy, .button.share:active, .share.highlight-button-cancel:active, .share.highlight-button-download:active, .share.highlight-button-copy:active { background-color: #dedede; } .button.share.newicon, .share.newicon.highlight-button-cancel, .share.newicon.highlight-button-download, .share.newicon.highlight-button-copy { background-image: url("../img/icon-share-alternate.svg"); } .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-download, .trash.highlight-button-copy { background-image: url("../img/icon-trash.svg"); } .button.trash:hover, .trash.highlight-button-cancel:hover, .trash.highlight-button-download:hover, .trash.highlight-button-copy:hover { background-color: #ededf0; } .button.trash:active, .trash.highlight-button-cancel:active, .trash.highlight-button-download:active, .trash.highlight-button-copy:active { background-color: #dedede; } .button.edit, .edit.highlight-button-cancel, .edit.highlight-button-download, .edit.highlight-button-copy { background-image: url("../img/icon-edit.svg"); } .button.edit:hover, .edit.highlight-button-cancel:hover, .edit.highlight-button-download:hover, .edit.highlight-button-copy:hover { background-color: #ededf0; } .button.edit:active, .edit.highlight-button-cancel:active, .edit.highlight-button-download:active, .edit.highlight-button-copy:active { background-color: #dedede; } .app-body { background: #f9f9fa; color: #38383d; } .app-body a { color: #0a84ff; } .highlight-color-scheme { background: #0a84ff; color: #fff; } .highlight-color-scheme a { color: #fff; text-decoration: underline; } .alt-color-scheme { background: #38383d; color: #f9f9fa; } .alt-color-scheme h1 { color: #6f7fb6; } .alt-color-scheme a { color: #e1e1e6; text-decoration: underline; } .button.primary, .primary.highlight-button-cancel, .highlight-button-download, .primary.highlight-button-copy { background-color: #0a84ff; color: #fff; } .button.primary:hover, .primary.highlight-button-cancel:hover, .highlight-button-download:hover, .primary.highlight-button-copy:hover, .button.primary:focus, .primary.highlight-button-cancel:focus, .highlight-button-download:focus, .primary.highlight-button-copy:focus { background-color: #0072e5; } .button.primary:active, .primary.highlight-button-cancel:active, .highlight-button-download:active, .primary.highlight-button-copy:active { background-color: #0065cc; } .button.secondary, .highlight-button-cancel, .secondary.highlight-button-download, .highlight-button-copy { background-color: #f9f9fa; color: #38383d; } .button.secondary:hover, .highlight-button-cancel:hover, .secondary.highlight-button-download:hover, .highlight-button-copy:hover { background-color: #ededf0; } .button.secondary:active, .highlight-button-cancel:active, .secondary.highlight-button-download:active, .highlight-button-copy:active { background-color: #dedede; } .button.transparent, .transparent.highlight-button-cancel, .transparent.highlight-button-download, .transparent.highlight-button-copy { background-color: transparent; color: #38383d; } .button.transparent:hover, .transparent.highlight-button-cancel:hover, .transparent.highlight-button-download:hover, .transparent.highlight-button-copy:hover { background-color: #ededf0; } .button.transparent:focus, .transparent.highlight-button-cancel:focus, .transparent.highlight-button-download:focus, .transparent.highlight-button-copy:focus, .button.transparent:active, .transparent.highlight-button-cancel:active, .transparent.highlight-button-download:active, .transparent.highlight-button-copy:active { background-color: #dedede; } .button.warning, .warning.highlight-button-cancel, .warning.highlight-button-download, .warning.highlight-button-copy { color: #fff; background: #d92215; } .button.warning:hover, .warning.highlight-button-cancel:hover, .warning.highlight-button-download:hover, .warning.highlight-button-copy:hover, .button.warning:focus, .warning.highlight-button-cancel:focus, .warning.highlight-button-download:focus, .warning.highlight-button-copy:focus { background: #b81d12; } .button.warning:active, .warning.highlight-button-cancel:active, .warning.highlight-button-download:active, .warning.highlight-button-copy:active { background: #a11910; } .subtitle-link { color: #0a84ff; } .loader { background: rgba(12, 12, 13, 0.2); border-radius: 2px; height: 4px; overflow: hidden; position: relative; width: 200px; } .loader-inner { animation: bounce infinite alternate 1250ms cubic-bezier(0.7, 0, 0.3, 1); background: #45a1ff; border-radius: 2px; height: 4px; transform: translateX(-40px); width: 50px; } @keyframes bounce { 0% { transform: translateX(-40px); } 100% { transform: translate(190px); } } @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes pop { 0% { transform: scale(1); } 97% { transform: scale(1.04); } 100% { transform: scale(1); } } @keyframes pulse { 0% { opacity: 0.3; transform: scale(1); } 70% { opacity: 0.25; transform: scale(1.04); } 100% { opacity: 0.3; transform: scale(1); } } @keyframes slide-left { 0% { opacity: 0; transform: translate3d(160px, 0, 0); } 100% { opacity: 1; transform: translate3d(0, 0, 0); } } @keyframes bounce-in { 0% { opacity: 0; transform: scale(1); } 60% { opacity: 1; transform: scale(1.02); } 100% { transform: scale(1); } } .mover-target { display: flex; align-items: center; justify-content: center; pointer-events: auto; position: absolute; z-index: 5; } .highlight, .mover-target { background-color: transparent; background-image: none; } .mover-target, .bghighlight { border: 0; } .hover-highlight { animation: fade-in 125ms forwards cubic-bezier(0.07, 0.95, 0, 1); background: rgba(255, 255, 255, 0.2); border-radius: 1px; pointer-events: none; position: absolute; z-index: 10000000000; } .hover-highlight::before { border: 2px dashed rgba(255, 255, 255, 0.4); bottom: 0; content: ""; inset-inline-start: 0; position: absolute; inset-inline-end: 0; top: 0; } body.hcm .hover-highlight { background-color: white; opacity: 0.2; } .mover-target.direction-topLeft { cursor: nwse-resize; height: 60px; left: -30px; top: -30px; width: 60px; } .mover-target.direction-top { cursor: ns-resize; height: 60px; inset-inline-start: 0; top: -30px; width: 100%; z-index: 4; } .mover-target.direction-topRight { cursor: nesw-resize; height: 60px; right: -30px; top: -30px; width: 60px; } .mover-target.direction-left { cursor: ew-resize; height: 100%; left: -30px; top: 0; width: 60px; z-index: 4; } .mover-target.direction-right { cursor: ew-resize; height: 100%; right: -30px; top: 0; width: 60px; z-index: 4; } .mover-target.direction-bottomLeft { bottom: -30px; cursor: nesw-resize; height: 60px; left: -30px; width: 60px; } .mover-target.direction-bottom { bottom: -30px; cursor: ns-resize; height: 60px; inset-inline-start: 0; width: 100%; z-index: 4; } .mover-target.direction-bottomRight { bottom: -30px; cursor: nwse-resize; height: 60px; right: -30px; width: 60px; } .mover-target:hover .mover { transform: scale(1.05); } .mover { background-color: #fff; border-radius: 50%; box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); height: 16px; opacity: 1; position: relative; transition: transform 125ms cubic-bezier(0.07, 0.95, 0, 1); width: 16px; } .small-selection .mover { height: 10px; width: 10px; } .direction-topLeft .mover, .direction-left .mover, .direction-bottomLeft .mover { left: -1px; } .direction-topLeft .mover, .direction-top .mover, .direction-topRight .mover { top: -1px; } .direction-topRight .mover, .direction-right .mover, .direction-bottomRight .mover { right: -1px; } .direction-bottomRight .mover, .direction-bottom .mover, .direction-bottomLeft .mover { bottom: -1px; } .bghighlight { background-color: rgba(0, 0, 0, 0.7); position: absolute; z-index: 9999999999; } body.hcm .bghighlight { background-color: black; opacity: 0.7; } .preview-overlay { align-items: center; background-color: rgba(0, 0, 0, 0.7); display: flex; height: 100%; justify-content: center; inset-inline-start: 0; margin: 0; padding: 0; position: fixed; top: 0; width: 100%; z-index: 9999999999; } body.hcm .preview-overlay { background-color: black; opacity: 0.7; } .precision-cursor { cursor: crosshair; } .highlight { border-radius: 1px; border: 2px dashed rgba(255, 255, 255, 0.8); box-sizing: border-box; cursor: move; position: absolute; z-index: 9999999999; } body.hcm .highlight { border: 2px dashed white; opacity: 1.0; } .highlight-buttons { display: flex; align-items: center; justify-content: center; bottom: -58px; position: absolute; inset-inline-end: 5px; z-index: 6; } .bottom-selection .highlight-buttons { bottom: 5px; } .left-selection .highlight-buttons { inset-inline-end: auto; inset-inline-start: 5px; } .highlight-buttons > button { box-shadow: 0 0 0 1px rgba(12, 12, 13, 0.1), 0 2px 8px rgba(12, 12, 13, 0.1); } .highlight-button-cancel { margin: 5px; width: 40px; } .highlight-button-download { margin: 5px; width: auto; font-size: 18px; } .highlight-button-download img { height: 16px; width: 16px; } .highlight-button-download:-moz-locale-dir(rtl) { flex-direction: reverse; } .highlight-button-download img:-moz-locale-dir(ltr) { padding-inline-end: 8px; } .highlight-button-download img:-moz-locale-dir(rtl) { padding-inline-start: 8px; } .highlight-button-copy { margin: 5px; width: auto; } .highlight-button-copy img { height: 16px; width: 16px; } .highlight-button-copy:-moz-locale-dir(rtl) { flex-direction: reverse; } .highlight-button-copy img:-moz-locale-dir(ltr) { padding-inline-end: 8px; } .highlight-button-copy img:-moz-locale-dir(rtl) { padding-inline-start: 8px; } .pixel-dimensions { position: absolute; pointer-events: none; font-weight: bold; font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif; font-size: 70%; color: #000; text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; } .preview-buttons { display: flex; align-items: center; justify-content: flex-end; padding-inline-end: 4px; inset-inline-end: 0; width: 100%; position: absolute; height: 60px; border-radius: 4px 4px 0 0; background: rgba(249, 249, 250, 0.8); top: 0; border: 1px solid rgba(249, 249, 250, 0.2); border-bottom: 0; box-sizing: border-box; } .preview-image { display: flex; align-items: center; flex-direction: column; justify-content: center; margin: 24px auto; position: relative; max-width: 80%; max-height: 95%; text-align: center; animation-delay: 50ms; display: flex; } .preview-image-wrapper { background: rgba(249, 249, 250, 0.8); border-radius: 0 0 4px 4px; display: block; height: auto; max-width: 100%; min-width: 320px; overflow-y: scroll; padding: 0 60px; margin-top: 60px; border: 1px solid rgba(249, 249, 250, 0.2); border-top: 0; } .preview-image-wrapper > img { box-shadow: 0 0 0 1px rgba(12, 12, 13, 0.1), 0 2px 8px rgba(12, 12, 13, 0.1); height: auto; margin-bottom: 60px; max-width: 100%; width: 100%; } .fixed-container { align-items: center; display: flex; flex-direction: column; height: 100vh; justify-content: center; inset-inline-start: 0; margin: 0; padding: 0; pointer-events: none; position: fixed; top: 0; width: 100%; } .face-container { position: relative; width: 64px; height: 64px; } .face { width: 62.4px; height: 62.4px; display: block; background-image: url("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<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){ function RavenConfigError(message) { this.name = 'RavenConfigError'; this.message = message; } RavenConfigError.prototype = new Error(); RavenConfigError.prototype.constructor = RavenConfigError; module.exports = RavenConfigError; },{}],2:[function(_dereq_,module,exports){ var utils = _dereq_(5); var wrapMethod = function(console, level, callback) { var originalConsoleLevel = console[level]; var originalConsole = console; if (!(level in console)) { return; } var sentryLevel = level === 'warn' ? 'warning' : level; console[level] = function() { var args = [].slice.call(arguments); var msg = utils.safeJoin(args, ' '); var data = {level: sentryLevel, logger: 'console', extra: {arguments: args}}; if (level === 'assert') { if (args[0] === false) { // Default browsers message msg = 'Assertion failed: ' + (utils.safeJoin(args.slice(1), ' ') || 'console.assert'); data.extra.arguments = args.slice(1); callback && callback(msg, data); } } else { callback && callback(msg, data); } // this fails for some browsers. :( if (originalConsoleLevel) { // IE9 doesn't allow calling apply on console functions directly // See: https://stackoverflow.com/questions/5472938/does-ie9-support-console-log-and-is-it-a-real-function#answer-5473193 Function.prototype.apply.call(originalConsoleLevel, originalConsole, args); } }; }; module.exports = { wrapMethod: wrapMethod }; },{"5":5}],3:[function(_dereq_,module,exports){ (function (global){ /*global XDomainRequest:false */ var TraceKit = _dereq_(6); var stringify = _dereq_(7); var md5 = _dereq_(8); var RavenConfigError = _dereq_(1); var utils = _dereq_(5); var isErrorEvent = utils.isErrorEvent; var isDOMError = utils.isDOMError; var isDOMException = utils.isDOMException; var isError = utils.isError; var isObject = utils.isObject; var isPlainObject = utils.isPlainObject; var isUndefined = utils.isUndefined; var isFunction = utils.isFunction; var isString = utils.isString; var isArray = utils.isArray; var isEmptyObject = utils.isEmptyObject; var each = utils.each; var objectMerge = utils.objectMerge; var truncate = utils.truncate; var objectFrozen = utils.objectFrozen; var hasKey = utils.hasKey; var joinRegExp = utils.joinRegExp; var urlencode = utils.urlencode; var uuid4 = utils.uuid4; var htmlTreeAsString = utils.htmlTreeAsString; var isSameException = utils.isSameException; var isSameStacktrace = utils.isSameStacktrace; var parseUrl = utils.parseUrl; var fill = utils.fill; var supportsFetch = utils.supportsFetch; var supportsReferrerPolicy = utils.supportsReferrerPolicy; var serializeKeysForMessage = utils.serializeKeysForMessage; var serializeException = utils.serializeException; var sanitize = utils.sanitize; var wrapConsoleMethod = _dereq_(2).wrapMethod; var dsnKeys = 'source protocol user pass host port path'.split(' '), dsnPattern = /^(?:(\w+):)?\/\/(?:(\w+)(:\w+)?@)?([\w\.-]+)(?::(\d+))?(\/.*)/; function now() { return +new Date(); } // 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 _document = _window.document; var _navigator = _window.navigator; function keepOriginalCallback(original, callback) { return isFunction(callback) ? function(data) { return callback(data, original); } : callback; } // First, check for JSON support // If there is no JSON, we no-op the core features of Raven // since JSON is required to encode the payload function Raven() { this._hasJSON = !!(typeof JSON === 'object' && JSON.stringify); // Raven can run in contexts where there's no document (react-native) this._hasDocument = !isUndefined(_document); this._hasNavigator = !isUndefined(_navigator); this._lastCapturedException = null; this._lastData = null; this._lastEventId = null; this._globalServer = null; this._globalKey = null; this._globalProject = null; this._globalContext = {}; this._globalOptions = { // SENTRY_RELEASE can be injected by https://github.com/getsentry/sentry-webpack-plugin release: _window.SENTRY_RELEASE && _window.SENTRY_RELEASE.id, logger: 'javascript', ignoreErrors: [], ignoreUrls: [], whitelistUrls: [], includePaths: [], headers: null, collectWindowErrors: true, captureUnhandledRejections: true, maxMessageLength: 0, // By default, truncates URL values to 250 chars maxUrlLength: 250, stackTraceLimit: 50, autoBreadcrumbs: true, instrument: true, sampleRate: 1, sanitizeKeys: [] }; this._fetchDefaults = { method: 'POST', // 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 referrerPolicy: supportsReferrerPolicy() ? 'origin' : '' }; this._ignoreOnError = 0; this._isRavenInstalled = false; this._originalErrorStackTraceLimit = Error.stackTraceLimit; // capture references to window.console *and* all its methods first // before the console plugin has a chance to monkey patch this._originalConsole = _window.console || {}; this._originalConsoleMethods = {}; this._plugins = []; this._startTime = now(); this._wrappedBuiltIns = []; this._breadcrumbs = []; this._lastCapturedEvent = null; this._keypressTimeout; this._location = _window.location; this._lastHref = this._location && this._location.href; this._resetBackoff(); // eslint-disable-next-line guard-for-in for (var method in this._originalConsole) { this._originalConsoleMethods[method] = this._originalConsole[method]; } } /* * The core Raven singleton * * @this {Raven} */ Raven.prototype = { // Hardcode version string so that raven source can be loaded directly via // webpack (using a build step causes webpack #1617). Grunt verifies that // this value matches package.json during build. // See: https://github.com/getsentry/raven-js/issues/465 VERSION: '3.27.0', debug: false, TraceKit: TraceKit, // alias to TraceKit /* * Configure Raven with a DSN and extra options * * @param {string} dsn The public Sentry DSN * @param {object} options Set of global options [optional] * @return {Raven} */ config: function(dsn, options) { var self = this; if (self._globalServer) { this._logDebug('error', 'Error: Raven has already been configured'); return self; } if (!dsn) return self; var globalOptions = self._globalOptions; // merge in options if (options) { each(options, function(key, value) { // tags and extra are special and need to be put into context if (key === 'tags' || key === 'extra' || key === 'user') { self._globalContext[key] = value; } else { globalOptions[key] = value; } }); } self.setDSN(dsn); // "Script error." is hard coded into browsers for errors that it can't read. // this is the result of a script being pulled in from an external domain and CORS. globalOptions.ignoreErrors.push(/^Script error\.?$/); globalOptions.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/); // join regexp rules into one big rule globalOptions.ignoreErrors = joinRegExp(globalOptions.ignoreErrors); globalOptions.ignoreUrls = globalOptions.ignoreUrls.length ? joinRegExp(globalOptions.ignoreUrls) : false; globalOptions.whitelistUrls = globalOptions.whitelistUrls.length ? joinRegExp(globalOptions.whitelistUrls) : false; globalOptions.includePaths = joinRegExp(globalOptions.includePaths); globalOptions.maxBreadcrumbs = Math.max( 0, Math.min(globalOptions.maxBreadcrumbs || 100, 100) ); // default and hard limit is 100 var autoBreadcrumbDefaults = { xhr: true, console: true, dom: true, location: true, sentry: true }; var autoBreadcrumbs = globalOptions.autoBreadcrumbs; if ({}.toString.call(autoBreadcrumbs) === '[object Object]') { autoBreadcrumbs = objectMerge(autoBreadcrumbDefaults, autoBreadcrumbs); } else if (autoBreadcrumbs !== false) { autoBreadcrumbs = autoBreadcrumbDefaults; } globalOptions.autoBreadcrumbs = autoBreadcrumbs; var instrumentDefaults = { tryCatch: true }; var instrument = globalOptions.instrument; if ({}.toString.call(instrument) === '[object Object]') { instrument = objectMerge(instrumentDefaults, instrument); } else if (instrument !== false) { instrument = instrumentDefaults; } globalOptions.instrument = instrument; TraceKit.collectWindowErrors = !!globalOptions.collectWindowErrors; // return for chaining return self; }, /* * Installs a global window.onerror error handler * to capture and report uncaught exceptions. * At this point, install() is required to be called due * to the way TraceKit is set up. * * @return {Raven} */ install: function() { var self = this; if (self.isSetup() && !self._isRavenInstalled) { TraceKit.report.subscribe(function() { self._handleOnErrorStackInfo.apply(self, arguments); }); if (self._globalOptions.captureUnhandledRejections) { self._attachPromiseRejectionHandler(); } self._patchFunctionToString(); if (self._globalOptions.instrument && self._globalOptions.instrument.tryCatch) { self._instrumentTryCatch(); } if (self._globalOptions.autoBreadcrumbs) self._instrumentBreadcrumbs(); // Install all of the plugins self._drainPlugins(); self._isRavenInstalled = true; } Error.stackTraceLimit = self._globalOptions.stackTraceLimit; return this; }, /* * Set the DSN (can be called multiple time unlike config) * * @param {string} dsn The public Sentry DSN */ setDSN: function(dsn) { var self = this, uri = self._parseDSN(dsn), lastSlash = uri.path.lastIndexOf('/'), path = uri.path.substr(1, lastSlash); self._dsn = dsn; self._globalKey = uri.user; self._globalSecret = uri.pass && uri.pass.substr(1); self._globalProject = uri.path.substr(lastSlash + 1); self._globalServer = self._getGlobalServer(uri); self._globalEndpoint = self._globalServer + '/' + path + 'api/' + self._globalProject + '/store/'; // Reset backoff state since we may be pointing at a // new project/server this._resetBackoff(); }, /* * Wrap code within a context so Raven can capture errors * reliably across domains that is executed immediately. * * @param {object} options A specific set of options for this context [optional] * @param {function} func The callback to be immediately executed within the context * @param {array} args An array of arguments to be called with the callback [optional] */ context: function(options, func, args) { if (isFunction(options)) { args = func || []; func = options; options = {}; } return this.wrap(options, func).apply(this, args); }, /* * Wrap code within a context and returns back a new function to be executed * * @param {object} options A specific set of options for this context [optional] * @param {function} func The function to be wrapped in a new context * @param {function} _before A function to call before the try/catch wrapper [optional, private] * @return {function} The newly wrapped functions with a context */ wrap: function(options, func, _before) { var self = this; // 1 argument has been passed, and it's not a function // so just return it if (isUndefined(func) && !isFunction(options)) { return options; } // options is optional if (isFunction(options)) { func = options; options = undefined; } // At this point, we've passed along 2 arguments, and the second one // is not a function either, so we'll just return the second argument. if (!isFunction(func)) { return func; } // We don't wanna wrap it twice! try { if (func.__raven__) { return func; } // If this has already been wrapped in the past, return that if (func.__raven_wrapper__) { return func.__raven_wrapper__; } } catch (e) { // Just accessing custom props in some Selenium environments // can cause a "Permission denied" exception (see raven-js#495). // Bail on wrapping and return the function as-is (defers to window.onerror). return func; } function wrapped() { var args = [], i = arguments.length, deep = !options || (options && options.deep !== false); if (_before && isFunction(_before)) { _before.apply(this, arguments); } // Recursively wrap all of a function's arguments that are // functions themselves. while (i--) args[i] = deep ? self.wrap(options, arguments[i]) : arguments[i]; try { // Attempt to invoke user-land function // NOTE: If you are a Sentry user, and you are seeing this stack frame, it // means Raven caught an error invoking your application code. This is // expected behavior and NOT indicative of a bug with Raven.js. return func.apply(this, args); } catch (e) { self._ignoreNextOnError(); self.captureException(e, options); throw e; } } // copy over properties of the old function for (var property in func) { if (hasKey(func, property)) { wrapped[property] = func[property]; } } wrapped.prototype = func.prototype; func.__raven_wrapper__ = wrapped; // Signal that this function has been wrapped/filled already // for both debugging and to prevent it to being wrapped/filled twice wrapped.__raven__ = true; wrapped.__orig__ = func; return wrapped; }, /** * Uninstalls the global error handler. * * @return {Raven} */ uninstall: function() { TraceKit.report.uninstall(); this._detachPromiseRejectionHandler(); this._unpatchFunctionToString(); this._restoreBuiltIns(); this._restoreConsole(); Error.stackTraceLimit = this._originalErrorStackTraceLimit; this._isRavenInstalled = false; return this; }, /** * Callback used for `unhandledrejection` event * * @param {PromiseRejectionEvent} event An object containing * promise: the Promise that was rejected * reason: the value with which the Promise was rejected * @return void */ _promiseRejectionHandler: function(event) { this._logDebug('debug', 'Raven caught unhandled promise rejection:', event); this.captureException(event.reason, { mechanism: { type: 'onunhandledrejection', handled: false } }); }, /** * Installs the global promise rejection handler. * * @return {raven} */ _attachPromiseRejectionHandler: function() { this._promiseRejectionHandler = this._promiseRejectionHandler.bind(this); _window.addEventListener && _window.addEventListener('unhandledrejection', this._promiseRejectionHandler); return this; }, /** * Uninstalls the global promise rejection handler. * * @return {raven} */ _detachPromiseRejectionHandler: function() { _window.removeEventListener && _window.removeEventListener('unhandledrejection', this._promiseRejectionHandler); return this; }, /** * Manually capture an exception and send it over to Sentry * * @param {error} ex An exception to be logged * @param {object} options A specific set of options for this error [optional] * @return {Raven} */ captureException: function(ex, options) { options = objectMerge({trimHeadFrames: 0}, options ? options : {}); if (isErrorEvent(ex) && ex.error) { // If it is an ErrorEvent with `error` property, extract it to get actual Error ex = ex.error; } else if (isDOMError(ex) || isDOMException(ex)) { // If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers) // then we just extract the name and message, as they don't provide anything else // https://developer.mozilla.org/en-US/docs/Web/API/DOMError // https://developer.mozilla.org/en-US/docs/Web/API/DOMException var name = ex.name || (isDOMError(ex) ? 'DOMError' : 'DOMException'); var message = ex.message ? name + ': ' + ex.message : name; return this.captureMessage( message, objectMerge(options, { // neither DOMError or DOMException provide stack trace and we most likely wont get it this way as well // but it's barely any overhead so we may at least try stacktrace: true, trimHeadFrames: options.trimHeadFrames + 1 }) ); } else if (isError(ex)) { // we have a real Error object ex = ex; } else if (isPlainObject(ex)) { // If it is plain Object, serialize it manually and extract options // This will allow us to group events based on top-level keys // which is much better than creating new group when any key/value change options = this._getCaptureExceptionOptionsFromPlainObject(options, ex); ex = new Error(options.message); } else { // If none of previous checks were valid, then it means that // it's not a DOMError/DOMException // it's not a plain Object // it's not a valid ErrorEvent (one with an error property) // it's not an Error // So bail out and capture it as a simple message: return this.captureMessage( ex, objectMerge(options, { stacktrace: true, // if we fall back to captureMessage, default to attempting a new trace trimHeadFrames: options.trimHeadFrames + 1 }) ); } // Store the raw exception object for potential debugging and introspection this._lastCapturedException = ex; // TraceKit.report will re-raise any exception passed to it, // which means you have to wrap it in try/catch. Instead, we // can wrap it here and only re-raise if TraceKit.report // raises an exception different from the one we asked to // report on. try { var stack = TraceKit.computeStackTrace(ex); this._handleStackInfo(stack, options); } catch (ex1) { if (ex !== ex1) { throw ex1; } } return this; }, _getCaptureExceptionOptionsFromPlainObject: function(currentOptions, ex) { var exKeys = Object.keys(ex).sort(); var options = objectMerge(currentOptions, { message: 'Non-Error exception captured with keys: ' + serializeKeysForMessage(exKeys), fingerprint: [md5(exKeys)], extra: currentOptions.extra || {} }); options.extra.__serialized__ = serializeException(ex); return options; }, /* * Manually send a message to Sentry * * @param {string} msg A plain message to be captured in Sentry * @param {object} options A specific set of options for this message [optional] * @return {Raven} */ captureMessage: function(msg, options) { // config() automagically converts ignoreErrors from a list to a RegExp so we need to test for an // early call; we'll error on the side of logging anything called before configuration since it's // probably something you should see: if ( !!this._globalOptions.ignoreErrors.test && this._globalOptions.ignoreErrors.test(msg) ) { return; } options = options || {}; msg = msg + ''; // Make sure it's actually a string var data = objectMerge( { message: msg }, options ); var ex; // Generate a "synthetic" stack trace from this point. // NOTE: If you are a Sentry user, and you are seeing this stack frame, it is NOT indicative // of a bug with Raven.js. Sentry generates synthetic traces either by configuration, // or if it catches a thrown object without a "stack" property. try { throw new Error(msg); } catch (ex1) { ex = ex1; } // null exception name so `Error` isn't prefixed to msg ex.name = null; var stack = TraceKit.computeStackTrace(ex); // stack[0] is `throw new Error(msg)` call itself, we are interested in the frame that was just before that, stack[1] var initialCall = isArray(stack.stack) && stack.stack[1]; // if stack[1] is `Raven.captureException`, it means that someone passed a string to it and we redirected that call // to be handled by `captureMessage`, thus `initialCall` is the 3rd one, not 2nd // initialCall => 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 = '<unknown>'; } 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 || '<anonymous>'} } }, 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) || '<anonymous>' } } }, 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) || '<anonymous>' } } }, 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) || '<anonymous>' } } }, 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) || '<anonymous>'} } }, 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) || '<anonymous>' } } }, 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 <a/> 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.<string, *>} 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.<string, *>} 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|<anonymous>|[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.<string, *>} 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.<string, *>} 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<x2, y1<y2 */ sortCoords() { if (this.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 <html> 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 !<?,?rU rU 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 !<?'?/k k build/thumbnailGenerator.js/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ this.thumbnailGenerator = (function () {let exports={}; // This is used in webextension/background/takeshot.js, // server/src/pages/shot/controller.js, and // server/scr/pages/shotindex/view.js. It is used in a browser // environment. // Resize down 1/2 at a time produces better image quality. // Not quite as good as using a third-party filter (which will be // slower), but good enough. const maxResizeScaleFactor = 0.5; // The shot will be scaled or cropped down to 210px on x, and cropped or // scaled down to a maximum of 280px on y. // x: 210 // y: <= 280 const maxThumbnailWidth = 210; const maxThumbnailHeight = 280; /** * @param {int} imageHeight Height in pixels of the original image. * @param {int} imageWidth Width in pixels of the original image. * @returns {width, height, scaledX, scaledY} */ function getThumbnailDimensions(imageWidth, imageHeight) { const displayAspectRatio = 3 / 4; const imageAspectRatio = imageWidth / imageHeight; let thumbnailImageWidth, thumbnailImageHeight; let scaledX, scaledY; if (imageAspectRatio > displayAspectRatio) { // "Landscape" mode // Scale on y, crop on x const yScaleFactor = (imageHeight > maxThumbnailHeight) ? (maxThumbnailHeight / imageHeight) : 1.0; thumbnailImageHeight = scaledY = Math.round(imageHeight * yScaleFactor); scaledX = Math.round(imageWidth * yScaleFactor); thumbnailImageWidth = Math.min(scaledX, maxThumbnailWidth); } else { // "Portrait" mode // Scale on x, crop on y const xScaleFactor = (imageWidth > maxThumbnailWidth) ? (maxThumbnailWidth / imageWidth) : 1.0; thumbnailImageWidth = scaledX = Math.round(imageWidth * xScaleFactor); scaledY = Math.round(imageHeight * xScaleFactor); // The CSS could widen the image, in which case we crop more off of y. thumbnailImageHeight = Math.min(scaledY, maxThumbnailHeight, maxThumbnailHeight / (maxThumbnailWidth / imageWidth)); } return { width: thumbnailImageWidth, height: thumbnailImageHeight, scaledX, scaledY, }; } /** * @param {dataUrl} String Data URL of the original image. * @param {int} imageHeight Height in pixels of the original image. * @param {int} imageWidth Width in pixels of the original image. * @param {String} urlOrBlob 'blob' for a blob, otherwise data url. * @returns A promise that resolves to the data URL or blob of the thumbnail image, or null. */ function createThumbnail(dataUrl, imageWidth, imageHeight, urlOrBlob) { // There's cost associated with generating, transmitting, and storing // thumbnails, so we'll opt out if the image size is below a certain threshold const thumbnailThresholdFactor = 1.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 !<=?Eu experiments/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 !<ao*?? ? icons/cancel.svg<!-- 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/. --> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10.5 8.7L5.2 3.3c-.5-.5-1.3-.5-1.8 0s-.5 1.3 0 1.8l5.3 5.3-5.3 5.3c-.5.5-.5 1.3 0 1.8s1.3.5 1.8 0l5.3-5.3 5.3 5.3c.5.5 1.3.5 1.8 0s.5-1.3 0-1.8l-5.3-5.3 5.3-5.3c.5-.5.5-1.3 0-1.8s-1.3-.5-1.8 0l-5.3 5.4z" fill="#3e3d40"/></svg>PK !<???- - icons/cloud.svg<!-- 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/. --> <svg width="20" height="18" xmlns="http://www.w3.org/2000/svg"><g fill="#FFF" fill-rule="evenodd"><path d="M15 5.6h-.3C14.5 2.7 12 .5 9.2.5c-3 0-5.4 2.4-5.5 5.3C1.5 6.4 0 8.3 0 10.6c0 2.8 2.2 5 5 5a1 1 0 0 0 1-1v-.1a1 1 0 0 0-1-1c-1.7 0-3-1.3-3-3 0-1.3.8-2.5 2.2-2.9l1.4-.4.1-1.4c.1-1.9 1.6-3.3 3.5-3.3 1.8 0 3.4 1.4 3.5 3.2l.1 1.8h2.1c1.7 0 3 1.3 3 3s-1.3 3-3 3h-1.85a1.05 1.05 0 1 0 0 2.1H15c2.8 0 5-2.2 5-5s-2.2-5-5-5z" fill-rule="nonzero"/><path d="M10 11.414V17c0 .667-.333 1-1 1s-1-.333-1-1v-5.586l-.293.293a1 1 0 1 1-1.414-1.414L9 7.586l2.707 2.707a1 1 0 0 1-1.414 1.414L10 11.414z"/></g></svg>PK !<?T?~s s icons/copied-notification.svg<!-- 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/. --> <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><path fill="context-fill" d="M44.121 24.879l-9-9A3 3 0 0 0 33 15h-3v-3a3 3 0 0 0-.879-2.121l-9-9A3 3 0 0 0 18 0H9a6 6 0 0 0-6 6v21a6 6 0 0 0 6 6h9v9a6 6 0 0 0 6 6h15a6 6 0 0 0 6-6V27a3 3 0 0 0-.879-2.121zM37.758 27H33v-4.758zm-15-15H18V7.242zM18 21v6H9V6h6v7.5a1.5 1.5 0 0 0 1.5 1.5H24a6 6 0 0 0-6 6zm6 21V21h6v7.5a1.5 1.5 0 0 0 1.5 1.5H39v12z"/></svg>PK !<? f?N N icons/copy.svg<!-- 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/. --> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#3e3d40" d="M14.707 8.293l-3-3A1 1 0 0 0 11 5h-1V4a1 1 0 0 0-.293-.707l-3-3A1 1 0 0 0 6 0H3a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3v3a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V9a1 1 0 0 0-.293-.707zM12.586 9H11V7.414zm-5-5H6V2.414zM6 7v2H3V2h2v2.5a.5.5 0 0 0 .5.5H8a2 2 0 0 0-2 2zm2 7V7h2v2.5a.5.5 0 0 0 .5.5H13v4z"/></svg>PK !<K?Z?+ + icons/download-white.svg<!-- 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/. --> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.1 12L4.9 7.9c-.5-.5-1.3-.5-1.8 0s-.5 1.3 0 1.8l6.2 6.2c.5.5 1.3.5 1.8 0l6.2-6.2c.5-.5.5-1.3 0-1.8s-1.3-.5-1.8 0L11.6 12V1.2C11.6.6 11 0 10.3 0c-.7 0-1.2.6-1.2 1.2V12zM4 20c-.7 0-1.2-.6-1.2-1.2s.6-1.2 1.2-1.2h12.5c.7 0 1.2.6 1.2 1.2s-.5 1.2-1.2 1.2H4z" fill="#fff"/></svg>PK !<khd. . icons/download.svg<!-- 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/. --> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.1 12L4.9 7.9c-.5-.5-1.3-.5-1.8 0s-.5 1.3 0 1.8l6.2 6.2c.5.5 1.3.5 1.8 0l6.2-6.2c.5-.5.5-1.3 0-1.8s-1.3-.5-1.8 0L11.6 12V1.2C11.6.6 11 0 10.3 0c-.7 0-1.2.6-1.2 1.2V12zM4 20c-.7 0-1.2-.6-1.2-1.2s.6-1.2 1.2-1.2h12.5c.7 0 1.2.6 1.2 1.2s-.5 1.2-1.2 1.2H4z" fill="#3e3d40"/></svg>PK !<???? ? icons/help-16.svg<!-- 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/. --> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="rgba(249, 249, 250, .8)" d="M8 1a7 7 0 1 0 7 7 7.008 7.008 0 0 0-7-7zm0 13a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zM8 3.125A2.7 2.7 0 0 0 5.125 6a.875.875 0 0 0 1.75 0c0-1 .6-1.125 1.125-1.125a1.105 1.105 0 0 1 1.13.744.894.894 0 0 1-.53 1.016A2.738 2.738 0 0 0 7.125 9v.337a.875.875 0 0 0 1.75 0v-.37a1.041 1.041 0 0 1 .609-.824A2.637 2.637 0 0 0 10.82 5.16 2.838 2.838 0 0 0 8 3.125zm0 7.625A1.25 1.25 0 1 0 9.25 12 1.25 1.25 0 0 0 8 10.75z"/></svg>PK !<???@' ' icons/icon-highlight-32-v2.svg<!-- 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/. --> <svg width="32" height="32" xmlns="http://www.w3.org/2000/svg"><path d="M8 2a4 4 0 0 0-4 4h4V2zm12 0h-4v4h4V2zm8 0v4h4a4 4 0 0 0-4-4zM14 2h-4v4h4V2zm12 0h-4v4h4V2zm2 10h4V8h-4v4zm0 12a4 4 0 0 0 4-4h-4v4zm0-6h4v-4h-4v4zm-.882-4.334a4 4 0 0 0-5.57-.984l-7.67 5.662-3.936-2.76c.031-.193.05-.388.058-.584a4.976 4.976 0 0 0-2-3.978V8H4v2.1a5 5 0 1 0 3.916 8.948l2.484 1.738-2.8 1.964a4.988 4.988 0 1 0 2.3 3.266l17.218-12.35zM5 17.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm0 12a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm10.8-4.858l6.034 4.6a4 4 0 0 0 5.57-.984L19.28 22.2l-3.48 2.442z" fill="#989898"/></svg>PK !<??le e icons/icon-v2.svg<!-- 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/. --> <svg viewBox="0 0 32 32" width="32" height="32" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity"><path d="M8 2a4 4 0 0 0-4 4h4V2zm12 0h-4v4h4V2zm8 0v4h4a4 4 0 0 0-4-4zM14 2h-4v4h4V2zm12 0h-4v4h4V2zm2 10h4V8h-4v4zm0 12a4 4 0 0 0 4-4h-4v4zm0-6h4v-4h-4v4zm-.882-4.334a4 4 0 0 0-5.57-.984l-7.67 5.662-3.936-2.76c.031-.193.05-.388.058-.584a4.976 4.976 0 0 0-2-3.978V8H4v2.1a5 5 0 1 0 3.916 8.948l2.484 1.738-2.8 1.964a4.988 4.988 0 1 0 2.3 3.266l17.218-12.35zM5 17.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm0 12a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm10.8-4.858l6.034 4.6a4 4 0 0 0 5.57-.984L19.28 22.2l-3.48 2.442z"/></svg> PK !<8?,? ? ( icons/icon-welcome-face-without-eyes.svg<!-- 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/. --> <svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><style>.st0{fill:#fff}</style><g id="Visual-design"><g id="_x31_.2-Div-selection" transform="translate(-575 -503)"><g id="Introduction" transform="translate(250 503)"><g id="icon-welcomeface" transform="translate(325)"><g id="Layer_1_1_"><path id="Shape" class="st0" d="M11.4.9v2.9h-6c-.9 0-1.5.8-1.5 1.5v6H.8V3.8C.8 2.1 2.2.7 3.9.7h7.6v.2h-.1z"/><path id="Shape_1_" class="st0" d="M63.2 11.4h-3.1v-6c0-.8-.6-1.5-1.5-1.5h-6v-3h7.6c1.7 0 3.1 1.4 3.1 3.1l-.1 7.4z"/><path id="Shape_2_" class="st0" d="M52.6 63.2v-3.1h6c.9 0 1.5-.6 1.5-1.5v-6h3.1v7.6c0 1.7-1.4 3.1-3.1 3.1l-7.5-.1z"/><path id="Shape_3_" class="st0" d="M.8 52.7h3.1v6c0 .9.6 1.5 1.5 1.5h6v3.1H3.8c-1.7 0-3.1-1.4-3.1-3.1l.1-7.5z"/><path id="Shape_6_" class="st0" d="M33.3 49.2H33c-4.6-.1-7.8-3.6-7.9-3.8-.6-.8-.6-2 .1-2.7.8-.8 1.9-.6 2.6.1 0 0 2.3 2.6 5.2 2.6 1.8 0 3.6-.9 5.2-2.6.8-.8 1.9-.8 2.7 0s.8 1.9 0 2.7c-2.2 2.4-4.9 3.7-7.6 3.7z"/></g></g></g></g></g></svg>PK !<?:??$ $ icons/menu-fullpage.svg<!-- 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/. --> <svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 46 46"><style>.st2{fill:#004c66}</style><path id="bg" d="M7 42h32V5.1H7z" fill="#00fdff"/><g id="frame" transform="translate(0 6)"><path d="M40 5c.5 0 1 .4 1 1v24c0 .5-.5 1-1 1H6c-.6 0-1-.5-1-1V6c0-.6.4-1 1-1h34zM7 29h32V7H7v22z" fill="#3e3d40"/><path id="Fill-4" class="st2" d="M7 7h32V5H7z"/><path id="Fill-6" class="st2" d="M7 31h32v-2H7z"/></g><path id="dash" d="M38 11h1V9h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm-1 1h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-2-3H7v3h2v-1H8v-2zm-1-1h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1V9H7v2zm2-6H7v3h1V6h1V5zm1 1h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm5-1h-2v1h1v2h1V5z" fill="#00d1e6"/></svg>PK !<dtW- - icons/menu-myshot-white.svg<!-- 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/. --> <svg width="40" height="40" viewBox="0 0 46 46" xmlns="http://www.w3.org/2000/svg"><path d="M11 11.995a1 1 0 0 1 .995-.995h23.01a1 1 0 0 1 .995.995v23.01a1 1 0 0 1-.995.995h-23.01a1 1 0 0 1-.995-.995v-23.01zM11 25v-2h7v2h-7zm9-5h7v-2h-7v2zm9 5h7v-2h-7v2zm-9 4h7v-2h-7v2zm-2-18h2v25h-2V11zm9 0h2v25h-2V11z" fill="#FFF" fill-rule="evenodd"/></svg>PK !<?Z? icons/menu-myshot.svg<!-- 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/. --> <svg width="46" height="46" xmlns="http://www.w3.org/2000/svg"><path d="M11 11.995a1 1 0 0 1 .995-.995h23.01a1 1 0 0 1 .995.995v23.01a1 1 0 0 1-.995.995h-23.01a1 1 0 0 1-.995-.995v-23.01zM11 25v-2h7v2h-7zm9-5h7v-2h-7v2zm9 5h7v-2h-7v2zm-9 4h7v-2h-7v2zm-2-18h2v25h-2V11zm9 0h2v25h-2V11z" fill="#3E3D40" fill-rule="evenodd"/></svg>PK !<:(?? ? icons/menu-visible.svg<!-- 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/. --> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 46 46"><path d="M5 12c0-.6.5-1 1-1h34c.6 0 1 .5 1 1v24c0 .6-.5 1-1 1H6c-.6 0-1-.5-1-1V12zm2 23V13h32v22H7z" fill="#3e3d40"/><path d="M7 35h32V13H7z" fill="#00fdff"/><path d="M38 19h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm-1 1h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-2-3H7v3h2v-1H8v-2zm-1-1h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm2-6H7v3h1v-2h1v-1zm1 1h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm5-1h-2v1h1v2h1v-3z" fill="#00d1e6"/></svg>PK !<?j?x x log.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 !<f4?? makeUuid.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.makeUuid = (function() { // generates a v4 UUID return function makeUuid() { // eslint-disable-line no-unused-vars // get sixteen unsigned 8 bit random values const randomValues = window .crypto .getRandomValues(new Uint8Array(36)); return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { const i = Array.prototype.slice.call(arguments).slice(-2)[0]; // grab the `offset` parameter const r = randomValues[i] % 16|0, v = c === "x" ? r : (r & 0x3 | 0x8); return v.toString(16); }); }; })(); null; PK !<?w}? ? manifest.json{ "manifest_version": 2, "name": "Firefox Screenshots", "version": "39.0.0", "description": "Take clips and screenshots from the Web and save them temporarily or permanently.", "author": "Mozilla <screenshots-feedback@mozilla.com>", "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": ["<all_urls>", "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", "<all_urls>", "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 !<o??N N randomString.js/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* exported randomString */ "use strict"; this.randomString = function randomString(length, chars) { const randomStringChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; chars = chars || randomStringChars; let result = ""; for (let i = 0; i < length; i++) { result += chars[Math.floor(Math.random() * chars.length)]; } return result; }; null; PK !<A??Q Q selector/callBackground.js/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* globals log */ "use strict"; this.callBackground = function callBackground(funcName, ...args) { return browser.runtime.sendMessage({funcName, args}).then((result) => { if (result && result.type === "success") { return result.value; } else if (result && result.type === "error") { const exc = new Error(result.message || "Unknown error"); exc.name = "BackgroundError"; if ("errorCode" in result) { exc.errorCode = result.errorCode; } if ("popupMessage" in result) { exc.popupMessage = result.popupMessage; } throw exc; } else { log.error("Unexpected background result:", result); const exc = new Error(`Bad response type from background page: ${result && result.type || undefined}`); exc.resultType = result ? (result.type || "undefined") : "undefined result"; throw exc; } }); }; null; PK !<