package.json0000644000000000000000000000252712741721616012057 0ustar rootroot{ "name": "lightbeam", "title": "Lightbeam", "description": "Lightbeam is a Firefox add-on that allows you to see the third parties that are collecting information about your browsing activity, with and without your consent. Using interactive visualizations, Lightbeam shows you the relationships between these third parties and the sites you visit.", "author": "Mozilla Foundation", "license": "MPL 2.0", "version": "1.3.1", "id": "jid1-F9UJ2thwoAm5gQ@jetpack", "main": "lib/main.js", "permissions": { "multiprocess": true, "private-browsing": true }, "preferences": [ { "name": "defaultVisualization", "title": "Default visualization", "description": "Graph or list", "type": "menulist", "value": "graph", "options": [ { "value": "graph", "label": "Graph" }, { "value": "list", "label": "List" } ] }, { "name": "defaultFilter", "title": "Default filter", "description": "Time period", "type": "menulist", "value": "daily", "options": [ { "value": "daily", "label": "Daily" }, { "value": "weekly", "label": "Weekly" }, { "value": "recent", "label": "Recent" }, { "value": "last10sites", "label": "Last 10 sites" } ] } ] } install.rdf0000644000000000000000000000245112741721616011730 0ustar rootroot jid1-F9UJ2thwoAm5gQ@jetpack 2 true false 1.3.1 Lightbeam Lightbeam is a Firefox add-on that allows you to see the third parties that are collecting information about your browsing activity, with and without your consent. Using interactive visualizations, Lightbeam shows you the relationships between these third parties and the sites you visit. Mozilla Foundation data:text/xml,<placeholder/> 2 true {ec8030f7-c20a-464f-9b0e-13a3a9e97384} 38.0a1 43.0 bootstrap.js0000644000000000000000000000112112741721616012131 0ustar rootroot/* 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"; const { utils: Cu } = Components; const rootURI = __SCRIPT_URI_SPEC__.replace("bootstrap.js", ""); const COMMONJS_URI = "resource://gre/modules/commonjs"; const { require } = Cu.import(COMMONJS_URI + "/toolkit/require.js", {}); const { Bootstrap } = require(COMMONJS_URI + "/sdk/addon/bootstrap.js"); var { startup, shutdown, install, uninstall } = new Bootstrap(rootURI); lib/0000755000000000000000000000000012751274675010342 5ustar rootrootlib/tab/0000755000000000000000000000000012741721616011077 5ustar rootrootlib/tab/utils.js0000644000000000000000000001132712741721616012601 0ustar rootroot/* 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/. */ // ChromeTab // // This is a module for getting the tab a channel is loaded in, from the channel exports.getTabForChannel = getTabForChannel; exports.on = onTab; exports.getTabInfo = getTabInfo; const { Cc, Ci, components } = require('chrome'); const tabs = require('sdk/tabs'); const { getTabForBrowser, getTabForContentWindow } = require('sdk/tabs/utils'); var wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator); components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); // return a variety of info on the tab function getTabInfo(jpTab) { // Some windows don't have performance initialized (because they haven't been reloaded since the plugin was initialized? try { var chromeWindow = wm.getMostRecentWindow('navigator:browser'); var gBrowser = chromeWindow.gBrowser; var window = gBrowser.contentWindow.wrappedJSObject; return { gBrowser: gBrowser, tab: gBrowser.selectedTab, document: gBrowser.contentDocument, window: window, title: gBrowser.contentTitle, // nsIPrincipal principal: gBrowser.contentPrincipal, // security context uri: gBrowser.contentURI, // nsURI .spec to get string representation loadTime: window.performance.timing.responseStart // milliseconds at which page load was initiated }; } catch (e) { return null; } } function onTab(eventname, fn) { tabs.on(eventname, function (jptab) { var tabinfo = getTabInfo(jptab); fn(tabinfo); }); } // Below code is based on adhacker, taken from http://forums.mozillazine.org/viewtopic.php?f=19&p=6335275 // Erik Vold may have the most current information on this. function getTabForChannel(aHttpChannel) { var loadContext = getLoadContext(aHttpChannel); if (!loadContext) { // fallback return getTabForChannel2(aHttpChannel); } return getTabForLoadContext(loadContext); } // Special case in case we don't have a load context. function getTabForChannel2(aChannel) { var win = getWindowForChannel(aChannel); if (!win) return null; var tab = getTabForContentWindow(win); return tab; } // Get the tab for a LoadContext function getTabForLoadContext(aLoadContext) { var browser = aLoadContext.topFrameElement; if (browser) { // Should work in e10s or in non-e10s Firefox >= 39 var tab = getTabForBrowser(browser); if ( tab ) { tab.isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser); return tab; } } // fallback return getTabForLoadContext2(aLoadContext); } // Fallback for when we can't get the tab for a LoadContext via // topFrameElement. This happens in: // * Firefox < 38, where topFrameElement is not defined when not // in e10s. // * Firefox 38, where topFrameElement is defined when not in e10s, but // the getTabForBrowser call fails // * other cases where the tab simply cannot be figured out. This function // will return null in these cases. function getTabForLoadContext2(aLoadContext) { try { var win = aLoadContext.topWindow; if (win) { var tab = getTabForContentWindow(win); // http://developer.mozilla.org/en/docs/XUL:tab if (PrivateBrowsingUtils.isContentWindowPrivate) { tab.isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(win); } else { tab.isPrivate = PrivateBrowsingUtils.isWindowPrivate(win); // ESR 31 } return tab; } } catch(err1) { // requesting aLoadContext.topWindow when in e10s throws an error } return null; } function getLoadContext(aRequest) { try { // first try the notification callbacks var loadContext = aRequest.QueryInterface(Ci.nsIChannel) .notificationCallbacks.getInterface(Ci.nsILoadContext); return loadContext; } catch (err1) { // fail over to trying the load group try { if (!aRequest.loadGroup) return null; var loadContext = aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext); return loadContext; } catch (err2) { return null; } } } function getWindowForChannel(aRequest) { var oHttp = aRequest.QueryInterface(Ci.nsIHttpChannel); if (!oHttp.notificationCallbacks) { console.log("HTTP request missing callbacks: " + oHttp.originalURI.spec); return null; } var interfaceRequestor = oHttp.notificationCallbacks.QueryInterface(Ci.nsIInterfaceRequestor); try { return interfaceRequestor.getInterface(Ci.nsIDOMWindow); } catch (e) { console.log("Failed to to find nsIDOMWindow from interface requestor: " + e); return null; } } lib/tab/events.js0000644000000000000000000000106112741721616012737 0ustar rootroot/* 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/. */ /* global require, exports */ // Simple onTab handler to figure out what tab a connection corresponds to. 'use strict'; const tabs = require('sdk/tabs'); const { getTabInfo } = require('./utils'); function onTab(eventname, fn) { tabs.on(eventname, function (jptab) { var tabinfo = getTabInfo(jptab); fn(tabinfo); }); } exports.on = onTab; lib/shared/0000755000000000000000000000000012741721616011577 5ustar rootrootlib/shared/unload+.js0000644000000000000000000000622212741721616013474 0ustar rootroot/* 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/. */ /* global require, console, exports */ 'use strict'; const { Class } = require("sdk/core/heritage"); const unloadNS = require("sdk/core/namespace").ns(); const { when: unload } = require("sdk/system/unload"); var Unloader = exports.Unloader = Class({ initialize: function Unloader() { unloadNS(this).unloaders = []; unloadNS(this).unloadersUnload = unloadersUnload.bind(null, unloadNS(this).unloaders); // run the unloaders on unload unload(unloadNS(this).unloadersUnload); }, unload: function unload(callback, container) { // Calling with no arguments runs all the unloader callbacks if (!callback) { unloadNS(this).unloadersUnload(); return null; } let windowRemover = windowUnloader.bind(null, unloader, unloadNS(this).unloaders); // The callback is bound to the lifetime of the container if we have one if (container) { // Remove the unloader when the container unloads container.addEventListener("unload", windowRemover, false); // Wrap the callback to additionally remove the unload listener let origCallback = callback; callback = function () { container.removeEventListener("unload", windowRemover, false); origCallback(); }; } // Wrap the callback in a function that ignores failures function unloader() { try { callback(); } catch (e) { console.error(e); } } unloadNS(this).unloaders.push(unloader); // Provide a way to remove the unloader return removeUnloader.bind(null, unloader, unloadNS(this).unloaders); } }); function sliceUnloader(unloader, unloaders) { let index = unloaders.indexOf(unloader); if (index < 0) return []; return unloaders.splice(index, 1); } // wraps sliceUnloader and doesn't return anything function removeUnloader(unloader, unloaders) { sliceUnloader.apply(null, arguments); } function windowUnloader(unloader, unloaders) { sliceUnloader.apply(null, arguments).forEach(function (u) u()); } function unloadersUnload(unloaders) { // run all the pending unloaders unloaders.slice().forEach(function (u) u()); // clear the unload array unloaders.length = 0; } /** * Save callbacks to run when unloading. Optionally scope the callback to a * container, e.g., window. Provide a way to run all the callbacks. * * @usage unload(): Run all callbacks and release them. * * @usage unload(callback): Add a callback to run on unload. * @param [function] callback: 0-parameter function to call on unload. * @return [function]: A 0-parameter function that undoes adding the callback. * * @usage unload(callback, container) Add a scoped callback to run on unload. * @param [function] callback: 0-parameter function to call on unload. * @param [node] container: Remove the callback when this container unloads. * @return [function]: A 0-parameter function that undoes adding the callback. */ const gUnload = Unloader(); exports.unload = gUnload.unload.bind(gUnload); lib/shared/policy.js0000644000000000000000000001036712741721616013443 0ustar rootroot/* 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/. */ /* global require, exports */ 'use strict'; const { Cc, Ci } = require('chrome'); const { Class } = require('sdk/core/heritage'); const CP_NS = require('sdk/core/namespace').ns(); const { ensure } = require('sdk/system/unload'); const { validateOptions } = require('sdk/deprecated/api-utils'); const { id: ADDON_ID } = require('sdk/self'); const xpcom = require('sdk/platform/xpcom'); const CM = Cc["@mozilla.org/categorymanager;1"] .getService(Ci.nsICategoryManager); const ACCEPT = exports.ACCEPT = Ci.nsIContentPolicy.ACCEPT; const REJECT = exports.REJECT = Ci.nsIContentPolicy.REJECT_REQUEST; const accept = function () ACCEPT; let ContentPolicy_ID = 0; const RULES = { description: { map: function (v) { return v ? v : ''; }, is: ['string'] }, contract: { map: function (v) { if (v === undefined) { v = '@erikvold.com/content-policy.' + ADDON_ID + ';' + ContentPolicy_ID++; } return v; }, is: ['string'] }, entry: { is: ['string', 'undefined'] }, shouldLoad: { is: ['function', 'undefined'] }, shouldProcess: { is: ['function', 'undefined'] }, }; function getType(aType) { switch (aType) { case Ci.nsIContentPolicy.TYPE_SCRIPT: return 'script'; case Ci.nsIContentPolicy.TYPE_IMAGE: return 'image'; case Ci.nsIContentPolicy.TYPE_STYLESHEET: return 'stylesheet'; case Ci.nsIContentPolicy.TYPE_OBJECT: return 'object'; case Ci.nsIContentPolicy.TYPE_DOCUMENT: return 'document'; case Ci.nsIContentPolicy.TYPE_SUBDOCUMENT: return 'subdocument'; case Ci.nsIContentPolicy.TYPE_REFRESH: return 'refresh'; case Ci.nsIContentPolicy.TYPE_XBL: return 'xbl'; case Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST: return 'xhr'; case Ci.nsIContentPolicy.TYPE_PING: return 'ping'; // TODO: support more types } return 'other'; } const getTypeMemod = memoize(getType, 12, 1); function makeDetails(aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess) { return { type: getTypeMemod(aContentType), location: aContentLocation, origin: aRequestOrigin.spec, context: null, // TODO: support this in a safe way somehow.. mimeTypeGuess: String(aMimeTypeGuess) }; } let ContentPolicy = exports.ContentPolicy = Class({ initialize: function (options) { const self = this; options = CP_NS(self).options = validateOptions(options, RULES); CP_NS(self).shouldLoad = options.shouldLoad || accept; CP_NS(self).shouldProcess = options.shouldProcess || accept; let factory = CP_NS(this).factory = xpcom.Factory({ Component: getProvider(self), description: options.description, contract: options.contract }); let entry = options.entry || options.contract; CM.addCategoryEntry('content-policy', entry, factory.contract, false, true); ensure(this, 'destroy'); }, destroy: function () { // already destroyed? if (!CP_NS(this).options) return; let options = CP_NS(this).options; CP_NS(this).options = null; CP_NS(this).shouldLoad = accept; CP_NS(this).shouldProcess = accept; CM.deleteCategoryEntry('content-policy', options.entry || options.contract, false); } }); function getProvider(self) { return Class({ extends: xpcom.Unknown, interfaces: ['nsIContentPolicy'], shouldLoad: function (aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess, aExtra) { let load = CP_NS(self).shouldLoad(makeDetails.apply(null, arguments)); return (load == REJECT || (!load && load !== undefined)) ? REJECT : ACCEPT; }, shouldProcess: function (aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess, aExtra) { let load = CP_NS(self).shouldProcess(makeDetails.apply(null, arguments)); return (load == REJECT || (!load && load !== undefined)) ? REJECT : ACCEPT; } }); } function memoize(func) { let cache = Object.create(null); return function (a) { let key = a.toString(); if (!(key in cache)) { cache[key] = func.call(null, a); } return cache[key]; }; } lib/shared/menuitems.js0000644000000000000000000001456512741721616014156 0ustar rootroot/* 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/. */ /* global require, exports */ 'use strict'; const windowUtils = require("sdk/deprecated/window-utils"); const { Class } = require("sdk/core/heritage"); const { validateOptions } = require("sdk/deprecated/api-utils"); const { emit } = require("sdk/event/core"); const { isBrowser } = require("sdk/window/utils"); const { EventTarget } = require('sdk/event/target'); const { unload } = require('./unload+'); const menuitemNS = require("sdk/core/namespace").ns(); const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; function MenuitemOptions(options) { return validateOptions(options, { id: { is: ['string'] }, menuid: { is: ['undefined', 'string'] }, insertbefore: { is: ['undefined', 'string', 'object', 'number'] }, label: { is: ["string"] }, include: { is: ['string', 'undefined'] }, disabled: { is: ["undefined", "boolean"], map: function (v) !! v }, accesskey: { is: ["undefined", "string"] }, key: { is: ["undefined", "string"] }, checked: { is: ['undefined', 'boolean'] }, className: { is: ["undefined", "string"] }, onCommand: { is: ['undefined', 'function'] }, useChrome: { map: function (v) !! v } }); } let Menuitem = Class({ extends: EventTarget, initialize: function (options) { options = menuitemNS(this).options = MenuitemOptions(options); EventTarget.prototype.initialize.call(this, options); menuitemNS(this).destroyed = false; menuitemNS(this).unloaders = []; menuitemNS(this).menuitems = addMenuitems(this, options).menuitems; }, get id() menuitemNS(this).options.id, get label() menuitemNS(this).options.label, set label(val) updateProperty(this, 'label', val), get checked() menuitemNS(this).options.checked, set checked(val) updateProperty(this, 'checked', !! val), get disabled() menuitemNS(this).options.disabled, set disabled(val) updateProperty(this, 'disabled', !! val), get key() menuitemNS(this).options.key, set key(val) updateProperty(this, 'key', val), clone: function (overwrites) { let opts = Object.clone(menuitemNS(this).options); for (let key in overwrites) { if (overwrites.hasOwnProperty(key)) { opts[key] = overwrites[key]; } } return Menuitem(opts); }, get menuid() menuitemNS(this).options.menuid, set menuid(val) { let options = menuitemNS(this).options; options.menuid = val; forEachMI(function (menuitem, i, $) { updateMenuitemParent(menuitem, options, $); }); }, destroy: function () { if (!menuitemNS(this).destroyed) { menuitemNS(this).destroyed = true; menuitemNS(this).unloaders.forEach(function (u) u()); menuitemNS(this).unloaders = null; menuitemNS(this).menuitems = null; } return true; } }); function addMenuitems(self, options) { let menuitems = []; // setup window tracker windowUtils.WindowTracker({ onTrack: function (window) { if (menuitemNS(self).destroyed) return; if (options.include) { if (options.include != window.location) return; } else if (!isBrowser(window)) { return; } // add the new menuitem to a menu var menuitem = updateMenuitemAttributes( window.document.createElementNS(NS_XUL, "menuitem"), options); var menuitems_i = menuitems.push(menuitem) - 1; // add the menutiem to the ui let added = updateMenuitemParent(menuitem, options, function (id) window.document.getElementById(id)); menuitem.addEventListener("command", function () { if (!self.disabled) emit(self, 'command', options.useChrome ? window : null); }, true); // add unloader let unloader = function unloader() { if (menuitem.parentNode) { menuitem.parentNode.removeChild(menuitem); } menuitems[menuitems_i] = null; }; let remover = unload(unloader, window); menuitemNS(self).unloaders.push(function () { remover(); unloader(); }); } }); return { menuitems: menuitems }; } function updateMenuitemParent(menuitem, options, $) { // add the menutiem to the ui if (Array.isArray(options.menuid)) { let ids = options.menuid; for (var len = ids.length, i = 0; i < len; i++) { if (tryParent($(ids[i]), menuitem, options.insertbefore)) return true; } } else { return tryParent($(options.menuid), menuitem, options.insertbefore); } return false; } function updateMenuitemAttributes(menuitem, options) { menuitem.setAttribute("id", options.id); menuitem.setAttribute("label", options.label); if (options.accesskey) menuitem.setAttribute("accesskey", options.accesskey); if (options.key) menuitem.setAttribute("key", options.key); menuitem.setAttribute("disabled", !! options.disabled); if (options.image) { menuitem.classList.add("menuitem-iconic"); menuitem.style.listStyleImage = "url('" + options.image + "')"; } if (options.checked) menuitem.setAttribute('checked', options.checked); if (options.className) options.className.split(/\s+/).forEach(function (name) menuitem.classList.add(name)); return menuitem; } function updateProperty(menuitem, key, val) { menuitemNS(menuitem).options[key] = val; forEachMI(function (menuitem) { menuitem.setAttribute(key, val); }, menuitem); return val; } function forEachMI(callback, menuitem) { menuitemNS(menuitem).menuitems.forEach(function (mi, i) { if (!mi) return; callback(mi, i, function (id) mi.ownerDocument.getElementById(id)); }); } function tryParent(parent, menuitem, before) { if (parent) parent.insertBefore(menuitem, insertBefore(parent, before)); return !!parent; } function insertBefore(parent, before) { if (typeof before == "number") { switch (before) { case MenuitemExport.FIRST_CHILD: return parent.firstChild; } return null; } else if (typeof before == "string") { return parent.querySelector("#" + before); } return before; } function MenuitemExport(options) { return Menuitem(options); } MenuitemExport.FIRST_CHILD = 1; exports.Menuitem = MenuitemExport; lib/ui.js0000644000000000000000000001773612741721616011322 0ustar rootroot/* 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/. */ /* global require, exports, console */ 'use strict'; const self = require("sdk/self"); const data = self.data; const tabs = require('sdk/tabs'); const { isPrivate } = require("sdk/private-browsing"); const { ContentPolicy } = require('lib/shared/policy'); const ss = require('sdk/simple-storage'); const prefs = require("sdk/simple-prefs").prefs; const prefService = require("sdk/preferences/service"); const system = require("sdk/system"); const persist = require("./persist"); const { Connection, getAllConnections } = require("./connection"); const xulapp = require("sdk/system/xul-app"); const { ActionButton } =require("sdk/ui/button/action"); const mainPage = data.url("index.html"); var uiworker = null; exports.mainPage = mainPage; exports.attachToLightbeamPage = attachToLightbeamPage; // These attach page workers to new tabs. exports.onForWorker = function (eventname, handler) { if (uiworker) { uiworker.port.on(eventname, handler); } else { console.log('no uiworker to subscript to order'); } }; exports.emitForWorker = function (eventname, obj) { if (uiworker) { uiworker.port.emit(eventname, obj); } }; // Begin tab handlers. These are for sidebar functionality, which is not // present yet. // FIXME: Move tab handlers into a tab component // And specify what we're trying to do with it function lightbeamTabClosed(tab) { menuitem.label = "Show Lightbeam"; button.tooltip = "Show Lightbeam"; } function lightbeamTabReady(tab) { menuitem.label = "Close Lightbeam"; button.tooltip = "Close Lightbeam"; } function lightbeamTabDeactivate(tab) { menuitem.label = "Switch to Lightbeam Tab"; button.tooltip = "Switch to Lightbeam Tab"; } const blockmap = ss.storage.blockmap; var blocksites = Object.keys(blockmap); console.log("blocking " + blocksites.length + ' sites'); // This is the heart of the Lightbeam blocking functionality. ContentPolicy({ description: "Blocks user-defined blocklist from Lightbeam UI", shouldLoad: function ({ location: location, origin: origin }) { // ignore URIs with no host var topLevelDomain; try { topLevelDomain = Connection.getDomain(location.host); } catch (e) { // See Issue 374: https://github.com/mozilla/lightbeam/issues/374 // if there is no host, like in about:what, then the host getter throws return true; } if (blockmap[topLevelDomain]) { return false; } return true; } }); function handlePrivateTab(tab) { if (isPrivate(tab) && uiworker) { // console.error('tab is private and uiworker exists'); uiworker.port.emit("private-browsing"); // console.error('sent message'); return true; } } // if there is a private tab opened while the lightbeam tab is open, // then alert the user about it. tabs.on('open', handlePrivateTab); // Notify the user in case they open a private window. Connections are // visualized but never stored. function hasPrivateTab() { // console.error('hasPrivateTab: %s tabs to test', tabs.length); for (var i = 0; i < tabs.length; i++) { if (handlePrivateTab(tabs[i])) { break; // the presence of a Private Window has been detected } } } // Connect the tab to the content script of the UI page. There may only ever be // one UI page. function attachToLightbeamPage(worker) { console.debug("Attaching to lightbeam page"); uiworker = worker; // The blocklist is maintained on both sides to reduce latency. However, // this may cause sync errors. function onWorkerUpdateBlocklist(site, blockFlag) { if (blockFlag) { if (!blockmap[site]) { blockmap[site] = true; } } else { if (blockmap[site]) { delete blockmap[site]; } } uiworker.port.emit('update-blocklist', { domain: site, flag: blockFlag }); } function onBrowserPrefChanged(event) { console.debug("Received updated browser prefs", JSON.stringify(event)); if ("trackingProtection" in event) { prefService.set("privacy.trackingprotection.enabled", event.trackingProtection); } } function onPrefChanged(event) { console.debug("Received updated prefs", JSON.stringify(event)); if ("defaultVisualization" in event) { prefs.defaultVisualization = event.defaultVisualization; } if ("defaultFilter" in event) { prefs.defaultFilter = event.defaultFilter; } } // Send over the the blocklist initially so we can use it. worker.port.emit('update-blocklist-all', Object.keys(blockmap).map( function (site) { return { domain: site, flag: blockmap[site] }; })); function onWorkerReset() { // Reset buffered state Connection.reset(); // And stored state, including prefs persist.reset(); } function onUIReady() { worker.port.emit("updateUIFromMetadata", { version: self.version, browserVersion: system.version }); worker.port.emit("updateUIFromBrowserPrefs", { "trackingProtection": prefService.get("privacy.trackingprotection.enabled") }); worker.port.emit("updateUIFromPrefs", prefs); worker.port.emit("passStoredConnections", getAllConnections()); } function onWorkerDetach() { // console.error('detaching lightbeam view'); /* jshint validthis:true */ this.port.removeListener('reset', onWorkerReset); this.port.removeListener('uiready', onUIReady); this.port.removeListener('updateBlocklist', onWorkerUpdateBlocklist); this.port.removeListener("browserPrefChanged", onBrowserPrefChanged); this.port.removeListener("prefChanged", onPrefChanged); uiworker = null; this.destroy(); } worker.on("detach", onWorkerDetach); worker.port.on("reset", onWorkerReset); worker.port.on('uiready', onUIReady); worker.port.on('updateBlocklist', onWorkerUpdateBlocklist); worker.port.on("browserPrefChanged", onBrowserPrefChanged); worker.port.on("prefChanged", onPrefChanged); worker.port.emit('init'); // if there is a private window open, then alert the user about it. try { hasPrivateTab(); } catch (e) { console.error('Error testing with hasPrivateTab(): %o', e); } } // This lets us toggle between the 3 states (no lightbeam tab open, lightbeam // tab open but it's not the tab you're on, you're on the lightbeam tab) function getLightbeamTab() { for each(let tab in tabs) { if (tab.url.slice(0, mainPage.length) === mainPage) { return tab; } } } exports.getLightbeamTab = getLightbeamTab; // Set up the menu item to open the main UI page: var menuitem = require("lib/shared/menuitems").Menuitem({ id: "lightbeam_openUITab", menuid: "menu_ToolsPopup", label: "Show Lightbeam", onCommand: function () { openOrSwitchToOrClose(); }, insertbefore: "sanitizeItem", image: data.url("icons/lightbeam_logo-only_32x32.png") }); function openOrSwitchToOrClose() { // Open the Lightbeam tab, if it doesn't exist. var tab = getLightbeamTab(); if (!tab) { return tabs.open({ url: mainPage, onOpen: lightbeamTabReady, onClose: lightbeamTabClosed, onReady: lightbeamTabReady, onActivate: lightbeamTabReady, onDeactivate: lightbeamTabDeactivate }); } // Close it if it's active. if (tab === tabs.activeTab) { tab.close(); } else { // Otherwise, switch to the Lightbeam tab tab.activate(); tab.window.activate(); } } exports.openOrSwitchToOrClose = openOrSwitchToOrClose; // Set up the status bar button to open the main UI page: var button = ActionButton({ id: "lightbeam_Widget", label: "Lightbeam", tooltip: "Show Lightbeam", // Relative to the data directory icon: { "16": "./icons/lightbeam_logo-only_16x16.png", "32": "./icons/lightbeam_logo-only_32x32.png", "48": "./icons/lightbeam_logo-only_48x48.png", }, onClick: function () { openOrSwitchToOrClose(); } }); lib/persist.js0000644000000000000000000000447312741721616012370 0ustar rootroot/* 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/. */ /* global console, require, exports */ // All writes to storage in the addon process goes here. "use strict"; const ss = require('sdk/simple-storage'); const Request = require("sdk/request").Request; const prefs = require("sdk/simple-prefs").prefs; var storage = ss.storage; // Only these keys may exist as maps on ss.storage. Everything else is legacy. const STORAGE_KEYS = [ "blockmap", "connections", ]; // Delete oldest connections. When we hit the simple storage quota limit, // Firefox throws an exception that the user won't see. We tried switching to // indexdb (which is async) but abandoned it. localForage may be a viable // substitute. function checkStorageQuota() { while (ss.quotaUsage > 1) { var sliceStart = ss.storage.connections.length / 2; ss.storage.connections = ss.storage.connections.slice(sliceStart); } } // Flush connections to simple-storage. exports.storeConnections = function storeConnections(connections) { checkStorageQuota(); storage.connections = storage.connections.concat(connections); }; // Reset stored state, including preferences exports.reset = function reset() { storage.connections.length = 0; storage.blockmap = {}; storage.userId = generateUserId(); prefs.defaultVisualization = "graph"; prefs.defaultFilter = "daily"; }; // Generate a new user id. function generateUserId() { // Short hex string. let userId = Math.floor(0xFFFFFFFF * Math.random()).toString(16); storage.userId = userId + ":" + Date.now(); return storage.userId; } // Possibly rotate the user id. function maybeRotateUserId(forceChange) { let parts = storage.userId.split(":"); // 90 days in ms let MAX_LIFETIME_MS = 90 * 24 * 60 * 60 * 1000; let timeToChange = Date(parts[1] + MAX_LIFETIME_MS); if (forceChange || Date.now() >= timeToChange) { generateUserId(); } } // Initialize all of our storage if (!storage.connections) { storage.connections = []; } if (!storage.blockmap) { storage.blockmap = {}; } if (!storage.userId) { generateUserId(); } // Rotate user id if necessary maybeRotateUserId(); console.log('Current quota usage:', Math.round(ss.quotaUsage * 100)); lib/main.js0000644000000000000000000000445212741721616011620 0ustar rootroot/* 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/. */ /* global exports, require */ "use strict"; // require("sdk/preferences/service").set("extensions.sdk.console.logLevel", "debug"); const events = require("sdk/system/events"); const { PageMod } = require("sdk/page-mod"); const { data } = require("sdk/self"); const { Connection, addConnection } = require('./connection'); const ui = require('./ui'); var tabs = require("sdk/tabs"); // This is the heart of Lightbeam, we get all of our data from observing these // requests. events.on("http-on-examine-response", function (subject) { var connection = Connection.fromSubject(subject); if (connection.valid) { addConnection(connection); // Pass the message on to the UI ui.emitForWorker('connection', connection.toLog()); } }, true); // This lets us hook into page load events and communicate to page workers. PageMod({ include: ui.mainPage, contentScriptWhen: 'ready', contentScriptFile: [ data.url('content-script.js'), data.url('d3/d3.js'), data.url('events.js'), data.url('infobar.js'), data.url('lightbeam.js'), data.url('svgdataset.js'), data.url('aggregate.js'), data.url('picoModal/picoModal.js'), data.url('tooltip.js'), data.url('dialog.js'), data.url('ui.js'), data.url('parseuri.js'), data.url('graph.js'), data.url('list.js'), ], onAttach: ui.attachToLightbeamPage }); exports.main = function (options, callbacks) { let initialPage = null; switch (options.loadReason) { case "install": initialPage = "first-run.html"; break; //case "upgrade": // initialPage = "upgrade.html"; // break; } if (initialPage) { let initialPageUrl = data.url(initialPage); tabs.open({url: initialPageUrl, inBackground: true}); // Add a content script to open lightbeam if the corresponding button is // pressed in the inital page PageMod({ include: initialPageUrl, contentScriptWhen: 'ready', contentScriptFile: data.url('initialPage.js'), onAttach: function (worker) { worker.port.on('openLightbeam', ui.openOrSwitchToOrClose); } }); } }; lib/connection.js0000644000000000000000000002270212741721616013031 0ustar rootroot/* 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/. */ // Connection object. This module may try to do too many things. // // Convert an HTTP request (channel) to a loggable, visualizable connection // object, if possible const { Cc, Ci, Cr } = require('chrome'); const { on, off, emit } = require('sdk/event/core'); const { data } = require("sdk/self"); const addonUrl = data.url("index.html"); const persist = require("./persist"); const { setTimeout } = require("sdk/timers"); const ss = require('sdk/simple-storage'); // An array of connection objects serialized to an array. var connBatch = []; const connBatchSize = 200; var eTLDSvc = Cc["@mozilla.org/network/effective-tld-service;1"]. getService(Ci.nsIEffectiveTLDService); const { getTabForChannel } = require('./tab/utils'); exports.Connection = Connection; exports.addConnection = addConnection; function addConnection(connection) { connBatch.push(connection.toLog()); if (connBatch.length == connBatchSize) { flushToStorage(); } console.debug("got", connBatch.length, "connections"); } exports.getAllConnections = function getAllConnections() { console.debug("got", connBatch.length, "buffered connections", ss.storage.connections.length, "persisted connections"); return ss.storage.connections.concat(connBatch); }; function excludePrivateConnections(connections) { return connections.filter(function (connection) { return !connection[Connection.FROM_PRIVATE_MODE]; }); } function flushToStorage() { console.debug("flushing", connBatch.length, "buffered connections"); persist.storeConnections(excludePrivateConnections(connBatch)); connBatch.length = 0; } // Every 5 minutes, flush to storage. setTimeout(flushToStorage, 5 * 60 * 1000); // Get eTLD + 1 (e.g., example.com from foo.example.com) function getDomain(host) { try { return eTLDSvc.getBaseDomainFromHost(host); } catch (e if e.result === Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) { return false; } catch (e if e.result === Cr.NS_ERROR_HOST_IS_IP_ADDRESS) { return false; } catch (e) { console.error('getDomain(): unexpected error: ' + host + ': ' + e); throw e; } } Connection.getDomain = getDomain; // make it part of what we export Connection.reset = function () { connBatch.length = 0; }; // Get subdomain (e.g., foo from foo.example.com) function getSubdomain(host) { var domain = getDomain(host); return host.slice(0, host.length - domain.length); } function Connection() {} // subject comes from events.on("http-on-modify-request"); Connection.fromSubject = function (subject) { var conn = new Connection(); conn.restoreFromSubject(subject); return conn; }; // Functions below may include legacy code from the first version of Collusion. // Find the page the URL was originally loaded from to determine whether this // happened in first or third-party context. function getAjaxRequestHeader(channel) { var header = null; try { header = channel.getRequestHeader('X-Requested-With').toLowerCase() === 'xmlhttprequest'; } catch (e) { if (e.name === 'NS_ERROR_NOT_AVAILABLE') { /* header not found, do nothing */ } else { console.error('what is this? ' + Object.keys(e).join(',')); throw e; } } return header; } // Does this make sense from http-on-examine-response? The response will have // Set-Cookie headers, the request will have Cookie headers. We should // investigate nsIHttpChannel to make sure that the response channel has the // original request headers. function hasCookie(channel) { try { return !!channel.getRequestHeader('Cookie'); } catch (e) { return false; } } function getProtocol(uri) { return uri.scheme; } // See doc/Heuristic for determining first party-ness.md Connection.prototype.restoreFromSubject = function (event) { // Check to see if this is in fact a third-party connection, if not, return var channel = event.subject.QueryInterface(Ci.nsIHttpChannel); // The referrer may not be set properly, especially in the case where HTTPS // includes HTTP where the referrer is stripped due to mixed-content // attacks. Also it doesn't work for RTC or WebSockets, but since we are // only examining HTTP requests right now, that's all right. var source = channel.referrer; var target = channel.URI; var targetDomain = getDomain(target.host); var tab = null; try { tab = getTabForChannel(channel); } catch (e) { console.debug('EXCEPTION CAUGHT: No tab for connection'); tab = null; } var isAjax = getAjaxRequestHeader(channel); var valid = true; var browserUri = tab ? tab.linkedBrowser.currentURI : null; var browserSpec = browserUri && browserUri.spec; var browserDomain = null; try { browserDomain = browserUri && getDomain(browserUri.host); } catch (e) { // chances are the URL is about:blank, which has no host and throws an exception // console.error('Error getting host from: ' + browserUri.spec); } // This is probably the largest source of false positives. var sourceVisited = !isAjax && (browserDomain === targetDomain || browserSpec === 'about:blank'); // Connection.log('browserUri ' + browserUri.spec + (sourceVisited ? ' equals ' : ' does not equal') + ' target ' + ( target && target.spec)); if (sourceVisited) { source = target; } else if (!source) { // This may introduce faulty data. // console.error('No source for target ' + target.spec + ' (' + channel.referrer + ')'); source = target; // can't have a null source } var sourceDomain = getDomain(source.host); var cookie = hasCookie(channel); var sourceProtocol = getProtocol(source); var targetProtocol = getProtocol(target); var isSecure = targetProtocol === 'https'; var isPrivate = tab && tab.isPrivate; // Handle all failure conditions if (browserUri && browserUri.spec === addonUrl) { this.valid = false; this.message = 'Do not record connections made by this add-on'; // console.error(this.message); return this; } if (!sourceDomain) { this.valid = false; this.message = 'Invalid source domain: ' + source.host; // console.error(this.message); return this; } if (!targetDomain) { this.valid = false; this.message = 'Invalid target domain: ' + target.host; // console.error(this.message); return this; } if (target.host === 'localhost' || source.host === 'localhost') { this.valid = false; this.message = 'Localhost is not trackable'; // console.error(this.message); return this; } if (sourceProtocol === 'http' || sourceProtocol === 'https' || targetProtocol === 'http' || targetProtocol === 'https') { /* OK, do nothing */ } else { this.valid = false; this.message = 'Unsupported protocol: ' + sourceProtocol + ' -> ' + targetProtocol; // console.error(this.message); return this; } if (!tab) { this.valid = false; this.message = 'No tab found for request: ' + target.spec + ', isAjax: ' + isAjax; // console.error(this.message); return this; } // set instance values for return this.valid = true; this.source = sourceDomain; this.target = targetDomain; this.timestamp = Date.now(); this.contentType = channel.contentType || 'text/plain'; this.cookie = cookie; // The client went to this page in a first-party context. this.sourceVisited = sourceVisited; this.secure = isSecure; this.sourcePathDepth = source.path.split('/').length - 1; this.sourceQueryDepth = source.query ? target.query.split(/;|\&/).length : 0; this.sourceSub = getSubdomain(source.host); this.targetSub = getSubdomain(target.host); this.method = channel.requestMethod; this.status = channel.responseStatus; this.cacheable = !channel.isNoCacheResponse(); // We visualize private connections but never store them. this.isPrivate = isPrivate; this._sourceTab = tab; // Never logged, only for associating data with current tab // console.error((sourceVisited ? 'site: ' : 'tracker: ') + sourceDomain + ' -> ' + targetDomain + ' (' + browserUri.spec + ')'); }; // Connection - level methods (not on instances) // This may be supported by the addon-sdk events.on now. Connection.on = function (eventname, handler) { on(Connection, eventname, handler); }; Connection.off = function (eventname) { off(Connection, eventname); }; Connection.emit = function (eventname, arg1, arg2, arg3) { emit(Connection, eventname, arg1, arg2, arg3); }; // Constants for indexes of properties in array format Connection.SOURCE = 0; Connection.TARGET = 1; Connection.TIMESTAMP = 2; Connection.CONTENT_TYPE = 3; Connection.COOKIE = 4; Connection.SOURCE_VISITED = 5; Connection.SECURE = 6; Connection.SOURCE_PATH_DEPTH = 7; Connection.SOURCE_QUERY_DEPTH = 8; Connection.SOURCE_SUB = 9; Connection.TARGET_SUB = 10; Connection.METHOD = 11; Connection.STATUS = 12; Connection.CACHEABLE = 13; Connection.FROM_PRIVATE_MODE = 14; Connection.prototype.toLog = function () { if (!this.valid) { throw new Error('Do not log invalid connections: ' + this.message); } var theLog = [ this.source, this.target, this.timestamp.valueOf(), this.contentType, this.cookie, this.sourceVisited, this.secure, this.sourcePathDepth, this.sourceQueryDepth, this.sourceSub, this.targetSub, this.method, this.status, this.cacheable, this._sourceTab.isPrivate ]; if (this.isPrivate) { theLog.push(this.isPrivate); } return theLog; }; data/0000755000000000000000000000000012751274675010505 5ustar rootrootdata/list.js0000644000000000000000000005153012741721616012011 0ustar rootroot/* 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/. */ // List Visualization // Display data in tabular format (function (visualizations, global) { "use strict"; var list = new Emitter(); var breadcrumbStack = []; visualizations.list = list; list.name = "list"; list.on("init", onInit); // list.on("connection", onConnection); list.on("remove", onRemove); list.on("showFilteredTable", function (filter) { showFilteredTable(filter); }); list.on('reset', onReset); function onReset() { console.debug("reset list"); breadcrumbStack = []; onRemove(); aggregate.emit('load', global.allConnections); } function onInit() { // console.log('list::onInit()'); vizcanvas.classList.add("hide"); // we don't need vizcanvas here, so hide it // A D3 visualization has a two main components, data-shaping, and setting up the D3 callbacks // This binds our data to the D3 visualization and sets up the callbacks initList(); initializeHandlers(); toggleShowHideHiddenButton(); aggregate.on('update', onUpdate); } function onUpdate() { let { nodes } = aggregate; let oldNodeRows = getAllRows().map(function (row) row.getAttribute('data-name')); let newNodes = nodes.filter(function (node) { return oldNodeRows.indexOf(node.name) < 0; }); if (newNodes.length <= 0) { return; } document.getElementById('refresh-data-link').textContent = 'Click here to refresh list...'; document.getElementById('refresh-data-row').classList.add('show'); return; } function onConnection(conn) { var connection = aggregate.connectionAsObject(conn); } function onRemove() { // console.log('removing list'); // var startTime = Date.now(); resetCanvas(); aggregate.off('update', onUpdate); // console.log('It took %s ms to remove list view', Date.now() - startTime); } function initList() { var stage = document.querySelector('.stage'); // breadcrumb initBreadcrumb(); // add number of row selected label var selectedLabel = elem("div", { "class": "rows-selected-label blue-text" }, [ elem("div", { "class": "some-selected hidden" }, [ elem("span", { "class": "num-selected" }), " out of ", elem("span", { "class": "num-total" }), " sites selected", elem("br"), elem('span', { 'class': 'deselect' }, 'clear all selected') ]), elem("div", { "class": "none-selected" }, [ elem("span", { "class": "num-total" }), " sites" ]) ]); stage.appendChild(selectedLabel); // list header var table = elem("div", { 'class': 'list-table' }, [ elem('table', { 'role': 'grid', 'aria-label': 'Entering List table' }, [ elem('tr', { 'class': 'refresh', 'id': 'refresh-data-row' }, [ elem('td', { 'colspan': '7', 'id': 'refresh-data-link' }) ]), elem('thead', { 'class': 'header-table' }, [ elem('tr', { 'role': 'row', 'tabIndex': '0' }, [ elem('th', elem('input', { 'class': 'selected-header', type: 'checkbox', 'tabIndex': '-1' })), elem('th', { 'role': 'gridcell' }, 'Type'), elem('th', { 'role': 'gridcell' }, 'Prefs'), elem('th', { 'role': 'gridcell' }, 'Website'), elem('th', { 'role': 'gridcell' }, 'First Access'), elem('th', { 'role': 'gridcell' }, 'Last Access'), elem('th', { 'class': 'sort-numeric', 'role': 'gridcell' }, 'Sites Connected') ]) ]), ]), elem('div', { 'class': 'body-table' }, elem('table', { 'role': 'grid' }, elem('tbody', { 'class': 'list-body' }) ) ) ]); stage.appendChild(table); showFilteredTable(); // showing all data so no filter param is passed here updateBreadcrumb(); } function initBreadcrumb() { var stage = document.querySelector('.stage'); var breadcrumb = elem("div", { "class": "breadcrumb" }); stage.appendChild(breadcrumb); } function updateBreadcrumb(url) { // push to breadcrumbStack breadcrumbStack.push(url ? url : "All Sites"); // remove all child nodes in breadcrumb container before we start mapping breadcrumbs to UI again resetVisibleBreadcrumb(); // map breadcrumbs to UI mapBreadcrumbsToUI(); } var breadcrumbClickHandler = function (event) { var url = event.target.getAttribute("site-url"); var idxInStack = event.target.getAttribute("idx"); while (breadcrumbStack.length > idxInStack) { breadcrumbStack.pop(); } showFilteredTable(url); }; function mapBreadcrumbsToUI() { var breadcrumb = document.querySelector(".breadcrumb"); var lastIdxInStack = breadcrumbStack.length - 1; // add "All Sites" to breadcrumb container breadcrumb.appendChild(elem("div", { "class": "breadcrumb-chunk" }, breadcrumbStack[0])); // other than "All Sites", there is only 1 tier in breadcrumbStack // add that tier to breadcrumb container if (lastIdxInStack == 1) { breadcrumb.appendChild(elem("div", { "class": "arrow-left" })); breadcrumb.appendChild(elem("div", { "class": "breadcrumb-chunk no-click", "site-url": breadcrumbStack[lastIdxInStack] }, breadcrumbStack[lastIdxInStack])); } // other than "All Sites", there are more than 1 tier in breadcrumbStack // we only want to show "All Sites" and the last 2 tiers // so add the last 2 tiers to breadcrumb container if (lastIdxInStack >= 2) { // second last tier breadcrumb.appendChild(elem("div", { "class": "arrow-left" })); breadcrumb.appendChild(elem("div", { "class": "breadcrumb-chunk", "site-url": breadcrumbStack[lastIdxInStack - 1], "idx": (lastIdxInStack - 1) }, breadcrumbStack[lastIdxInStack - 1])); // last tier breadcrumb.appendChild(elem("div", { "class": "arrow-left" })); breadcrumb.appendChild(elem("div", { "class": "breadcrumb-chunk no-click", "site-url": breadcrumbStack[lastIdxInStack], "idx": lastIdxInStack }, breadcrumbStack[lastIdxInStack])); } // add breadcrumbs click event handler var allBreadcrumbChunks = document.querySelectorAll(".breadcrumb-chunk"); toArray(allBreadcrumbChunks).forEach(function (chunk) { if (!chunk.classList.contains("no-click")) { chunk.addEventListener("click", breadcrumbClickHandler, false); } }); } function resetVisibleBreadcrumb() { var breadcrumbContainer = document.querySelector(".breadcrumb"); while (breadcrumbContainer.firstChild) { breadcrumbContainer.removeChild(breadcrumbContainer.firstChild); } } function updateNumTotalRowsLabel() { var numTotal = getAllRows().length; var labels = document.querySelectorAll(".num-total"); for (var i = 0; i < labels.length; i++) { labels[i].textContent = numTotal; } } function updateRowSelectedLabel() { var numSelected = getSelectedRows().length; var selectedLabel = document.querySelector(".some-selected"); var noneSelectedLabel = document.querySelector(".none-selected"); if (numSelected > 0) { selectedLabel.querySelector(".num-selected").textContent = numSelected; selectedLabel.classList.remove("hidden"); noneSelectedLabel.classList.add("hidden"); } else { selectedLabel.classList.add("hidden"); noneSelectedLabel.classList.remove("hidden"); } } function resetSelectedRows() { let selectedRows = getSelectedRows(); for (let i = 0; i < selectedRows.length; i++) { let sel = selectedRows[i]; sel.querySelector('.selected-row').checked = false; sel.classList.remove('checked'); } // Also uncheck the header input box if it's checked document.querySelector('.selected-header').checked = false; // Update the selected rows header to reflect the changes updateRowSelectedLabel(); } var lastFilter = null; function showFilteredTable(filter) { console.debug("showFilteredTable", filter); if (lastFilter != filter) updateBreadcrumb(filter); lastFilter = filter; // remove existing table tbodys, if any var table = document.querySelector(".list-table"); var tbody = table.querySelector('.list-body'); var tbodyParent = tbody.parentElement; tbodyParent.removeChild(tbody); var nodes = getNodes(filter); console.debug("getNodes", nodes); tbodyParent.appendChild(createBody(nodes)); // update other UI elements document.querySelector('.selected-header').checked = false; updateNumTotalRowsLabel(); updateRowSelectedLabel(); } function getNodes(filter) { if (!filter) { // if no filter, show all return aggregate.getAllNodes(); } else { var nodeMap = aggregate.nodeForKey(filter); return Object.keys(nodeMap).map(function (key) { return nodeMap[key]; }); } } // A Node has the following properties: // contentTypes: [] // cookieCount: # // firstAccess: Date // howMany: # // method: [] // name: "" // nodeType: site | thirdparty | both // secureCount: # // status: [] // subdomain: [] // visitedCount: # function nodeToRow(node) { var settings = userSettings[node.name] || (node.nodeType == 'blocked' ? 'block' : ''); var iconUrl = node.nodeType === 'blocked' ? 'icons/lightbeam_icon_empty_list.png' : 'image/lightbeam_icon_list.png'; var listIcon = elem('img', { 'src': iconUrl, 'class': node.nodeType === 'blocked' ? 'no-update' : 'update-table', 'role': 'gridcell' }); var row = elem('tr', { 'class': 'node ' + node.nodeType, 'data-pref': settings, 'data-name': node.name, 'site-url': node.name, 'role': 'row', 'tabIndex': '0' }, [ elem('td', elem('input', { 'type': 'checkbox', 'class': 'selected-row', 'tabIndex': '-1' })), elem('td', { 'data-sort-key': node.nodeType, 'role': 'gridcell' }, node.nodeType === 'thirdparty' ? 'Third Party' : (node.nodeType === 'blocked' ? 'Unknown' : 'Visited')), elem('td', { 'class': 'preferences', 'data-sort-key': settings, 'role': 'gridcell' }, '\u00A0'), elem('td', { 'data-sort-key': node.name, 'role': 'gridcell' }, [ listIcon, node.name ]), elem('td', { 'data-sort-key': node.firstAccess, 'role': 'gridcell' }, (node.nodeType === 'blocked' ? 'Unknown' : formattedDate(node.firstAccess))), elem('td', { 'data-sort-key': node.lastAccess, 'role': 'gridcell' }, (node.nodeType === 'blocked' ? 'Unknown' : formattedDate(node.lastAccess))), elem('td', { 'data-sort-key': aggregate.getConnectionCount(node), 'role': 'gridcell' }, aggregate.getConnectionCount(node) + '') ]); if (node.nodeType !== 'blocked') { listIcon.addEventListener("mouseenter", tooltip.addTooltip); listIcon.addEventListener("mouseleave", tooltip.hide); row.addEventListener("mouseenter", function () { row.childNodes[3].firstChild.setAttribute("src", "image/lightbeam_icon_list_blue.png"); }); row.addEventListener("mouseleave", function () { row.childNodes[3].firstChild.setAttribute("src", iconUrl); }); } if (node.nodeType === 'blocked') { row.dataset.isBlocked = true; } return row; } function createBody(nodes) { return elem("tbody", { 'class': 'list-body' }, nodes.map(nodeToRow)); } function sort(item1, item2) { if (item1[0] < item2[0]) return -1; if (item2[0] < item1[0]) return 1; return 0; } function reverseSort(item1, item2) { if (item1[0] < item2[0]) return 1; if (item2[0] < item1[0]) return -1; return 0; } function sortTableOnColumn(table, n) { return function (evt) { // we could probably determine the column from the event.target // if this is sorted column, reverse // if this is reversed column, re-sort // if this is not sorted column, unset sorted flag on that column var reversed = evt.target.classList.contains('reverse-sorted'); var sorted = evt.target.classList.contains('sorted'); if (!(sorted || reversed)) { var oldcolumn = table.querySelector('.sorted, .reverse-sorted'); if (oldcolumn) { oldcolumn.classList.remove('sorted'); oldcolumn.classList.remove('reverse-sorted'); } } var tbody = table.querySelector('tbody'); var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr')).map(function (row) { if (evt.target.classList.contains('sort-numeric')) { return [parseInt(row.children[n].dataset.sortKey, 10), row]; } else { return [row.children[n].dataset.sortKey, row]; } }); if (sorted) { evt.target.classList.remove('sorted'); evt.target.classList.add('reverse-sorted'); rows.sort(reverseSort); } else { evt.target.classList.remove('reverse-sorted'); evt.target.classList.add('sorted'); rows.sort(sort); } var frag = document.createDocumentFragment(); var preFrag = document.createDocumentFragment(); rows.forEach(function (row) { var rowElement = row[1]; // Check if there are any preferences set for this row var prefVal = rowElement.attributes.getNamedItem('data-pref').value; if (prefVal) { // This row is marked with a preference and should // be appended to the top fragment. preFrag.appendChild(rowElement); } else { frag.appendChild(rowElement); } }); tbody.appendChild(preFrag); tbody.appendChild(frag); }; } function resetCanvas() { var listTable = document.querySelector('.stage .list-table'); if (listTable) { listTable.parentElement.removeChild(listTable); } var breadcrumb = document.querySelector('.stage .breadcrumb'); if (breadcrumb) { breadcrumb.parentElement.removeChild(breadcrumb); } breadcrumbStack = []; var selectedLabel = document.querySelector(".rows-selected-label"); if (selectedLabel) { selectedLabel.parentElement.removeChild(selectedLabel); } document.querySelector('.stage-stack').removeEventListener('click', listStageStackClickHandler, false); vizcanvas.classList.remove("hide"); } function getAllRows() { return Array.slice(document.querySelectorAll('.body-table tr')); } function getSelectedRows() { // returns selected rows as an Array return getAllRows().filter(function (item) { return item.querySelector('.selected-row:checked'); }); } // Event handlers function setUserSetting(row, pref) { var site = row.dataset.name; // change setting userSettings[site] = pref; // send change through to add-on global.self.port.emit('updateBlocklist', site, pref === 'block'); // modify row row.dataset.pref = pref; // Add sort order to preference column row.querySelector('.preferences').dataset.sortKey = pref; // uncheck the row row.querySelector('[type=checkbox]').checked = false; row.classList.remove("checked"); } // selectAllRows should only select VISIBLE rows function selectAllRows(flag) { var i; // apply flag to ALL rows first var rows = document.querySelectorAll(".body-table tr"); for (i = 0; i < rows.length; i++) { rows[i].querySelector(".selected-row").checked = flag; highlightRow(rows[i], flag); } // and then exclude all the hidden rows if (document.querySelector(".hide-hidden-rows")) { var hiddenRows = document.querySelectorAll(".list-table .body-table tr[data-pref=hide]"); for (i = 0; i < hiddenRows.length; i++) { hiddenRows[i].querySelector(".selected-row").checked = false; // makes sure the hidden rows are always unchecked highlightRow(hiddenRows[i], false); } } togglePrefButtons(); } function setPreferences(pref) { getSelectedRows().forEach(function (row) { setUserSetting(row, pref); }); document.querySelector('.selected-header').checked = false; updateRowSelectedLabel(); togglePrefButtons(); toggleShowHideHiddenButton(); } function toggleHiddenSites(target) { if (target.dataset.state === 'shown') { target.dataset.state = 'hidden'; target.textContent = 'Show Hidden'; document.querySelector('.stage-stack').classList.add('hide-hidden-rows'); } else { target.dataset.state = 'shown'; target.textContent = 'Hide Hidden'; document.querySelector('.stage-stack').classList.remove('hide-hidden-rows'); } } var listStageStackClickHandler = function (event) { var target = event.target; if (target.mozMatchesSelector('label[for=block-pref], label[for=block-pref] *')) { confirmBlockSitesDialog(function (confirmed) { if (confirmed) { setPreferences('block'); } }); } else if (target.mozMatchesSelector('label[for=hide-pref], label[for=hide-pref] *')) { if (doNotShowDialog(dialogNames.hideSites)) { setPreferences('hide'); } else { confirmHideSitesDialog(function (confirmed) { if (confirmed) { setPreferences('hide'); } }); } } else if (target.mozMatchesSelector('label[for=watch-pref], label[for=watch-pref] *')) { setPreferences('watch'); } else if (target.mozMatchesSelector('label[for=no-pref], label[for=no-pref] *')) { setPreferences(''); } else if (target.mozMatchesSelector('.toggle-hidden a')) { toggleHiddenSites(target); } }; // Install handlers function initializeHandlers() { try { document.querySelector('.selected-header').addEventListener('change', function (event) { selectAllRows(event.target.checked); }, false); document.querySelector('.list-footer').querySelector(".legend-toggle").addEventListener("click", function (event) { toggleLegendSection(event.target, document.querySelector('.list-footer')); }); document.querySelector('.stage-stack').addEventListener('click', listStageStackClickHandler, false); // Add handler for rows document.querySelector('.list-table').addEventListener('click', function (event) { var url = event.target.parentNode.dataset.sortKey; var node = event.target; if (node.mozMatchesSelector('td:first-child [type=checkbox]')) { while (node.mozMatchesSelector('.node *')) { node = node.parentElement; } highlightRow(node, node.querySelector("[type=checkbox]").checked); togglePrefButtons(); } else if (node.mozMatchesSelector('.update-table') && url) { showFilteredTable(url); } }, false); // Add handler to refresh rows var refreshRow = document.querySelector("#refresh-data-row"); refreshRow.addEventListener('click', function onClick() { var wereSelected, selected; refreshRow.classList.remove('show'); // update the table // what were selected should stay selected after the table has been updated wereSelected = getSelectedRows().map(function (row) { return row.dataset.name; }); showFilteredTable(lastFilter); selected = getAllRows().filter(function (row) { return wereSelected.indexOf(row.dataset.name) > -1; }) .map(function (rowToSelect) { rowToSelect.querySelector("[type=checkbox]").checked = true; highlightRow(rowToSelect, true); return; }); }, false); // Add handler to deselect rows document.querySelector('.deselect').addEventListener('click', function (event) { resetSelectedRows(); }, false); // Set sort handlers. nth-child(n+2) skips the checkbox column var table = document.querySelector(".list-table"); var headers = Array.prototype.slice.call(table.querySelectorAll('th:nth-child(n+2)')); headers.forEach(function (th, idx) { // idx+1 gives the actual column (skipping the checkbox the other way) th.addEventListener('click', sortTableOnColumn(table, idx + 1), false); }); } catch (e) { console.log('Error: %o', e); } } function highlightRow(node, rowChecked) { if (rowChecked) { node.classList.add("checked"); } else { node.classList.remove("checked"); } updateRowSelectedLabel(); } function togglePrefButtons() { var numChecked = document.querySelectorAll(".list-table .body-table tr input[type=checkbox]:checked").length; var toggleOn = numChecked > 0; var classToAdd = toggleOn ? "active" : "disabled"; var classToRemove = toggleOn ? "disabled" : "active"; // toggle on class toArray(document.querySelectorAll("input[name=pref-options] + label")).forEach(function (option) { option.classList.add(classToAdd); }); // toggle off class toArray(document.querySelectorAll("input[name=pref-options] + label")).forEach(function (option) { option.classList.remove(classToRemove); }); } function toggleShowHideHiddenButton() { if (document.querySelectorAll("[data-pref='hide']").length > 0) { document.querySelector(".toggle-hidden").classList.remove("disabled"); } else { document.querySelector(".toggle-hidden").classList.add("disabled"); } } })(visualizations, this); data/lightbeam.js0000644000000000000000000001436512741721616012777 0ustar rootroot/* 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/. */ (function (global) { 'use strict'; var visualizations = {}; var currentFilter; var userSettings = {}; var allConnections = []; var g = global; // Constants for indexes of properties in array format const SOURCE = 0; const TARGET = 1; const TIMESTAMP = 2; const CONTENT_TYPE = 3; const COOKIE = 4; const SOURCE_VISITED = 5; const SECURE = 6; const SOURCE_PATH_DEPTH = 7; const SOURCE_QUERY_DEPTH = 8; const SOURCE_SUB = 9; const TARGET_SUB = 10; const METHOD = 11; const STATUS = 12; const CACHEABLE = 13; const FROM_PRIVATE_MODE = 14; var vizcanvas = document.querySelector('.vizcanvas'); var mapDocument, mapcanvas; document.querySelector('.world-map').addEventListener('load', function (event) { mapDocument = event.target.contentDocument; mapcanvas = mapDocument.querySelector('.mapcanvas'); initMap(mapcanvas, mapDocument); }, false); // Export everything global.visualizations = visualizations; global.currentFilter = currentFilter; global.userSettings = userSettings; global.vizcanvas = vizcanvas; global.allConnections = allConnections; // DOM Utility global.elem = function elem(name, attributes, children) { // name is the tagName of an element // [optional] attributes can be null or undefined, or an object of key/values to setAttribute on, attribute values can be functions to call to get the actual value // [optional] children can be an element, text or an array (or null or undefined). If an array, can contain strings or elements var e = document.createElement(name); var val; if (attributes && (Array.isArray(attributes) || attributes.nodeName || typeof attributes === 'string')) { children = attributes; attributes = null; } try { if (attributes) { Object.keys(attributes).forEach(function (key) { if (attributes[key] === null || attributes[key] === undefined) return; if (typeof attributes[key] === 'function') { val = attributes[key](key, attributes); if (val) { e.setAttribute(key, val); } } else { e.setAttribute(key, attributes[key]); } }); } } catch (err) { console.log('attributes: not what we think they are: %o', attributes); } if (children) { if (!Array.isArray(children)) { children = [children]; // convenience, allow a single argument vs. an array of one } children.forEach(function (child) { if (child.nodeName) { e.appendChild(child); } else { // assumes child is a string e.appendChild(document.createTextNode(child)); } }); } return e; }; window.addEventListener('load', function (evt) { console.debug('window onload'); self.port.emit('uiready'); // Wire up events document.querySelector('[data-value=Graph]').setAttribute("data-selected", true); var visualizationName = "graph"; console.debug("current vis", visualizationName); g.currentVisualization = visualizations[visualizationName]; switchVisualization(visualizationName); }); function initCap(str) { return str[0].toUpperCase() + str.slice(1); } global.switchVisualization = function switchVisualization(name) { // var startTime = Date.now(); console.debug('switchVisualizations(' + name + ')'); if (g.currentVisualization != visualizations[name]) { g.currentVisualization.emit('remove'); } g.currentVisualization = visualizations[name]; resetAdditionalUI(); g.currentVisualization.emit('init'); self.port.emit("prefChanged", { defaultVisualization: name }); // console.log('it took %s ms to switch visualizations', Date.now() - startTime); }; function resetAdditionalUI() { // toggle off info panel document.querySelector("#content").classList.remove("showinfo"); var activeTab = document.querySelector(".info-panel-controls ul li.active"); if (activeTab) { // make the active tab inactive, if any activeTab.classList.remove("active"); activeTab.querySelector("img").classList.remove("hidden"); activeTab.querySelector("i").classList.add("hidden"); } // hide all help sections document.querySelector(".help-content .graph-view-help").classList.add("hidden"); document.querySelector(".help-content .list-view-help").classList.add("hidden"); // show vizcanvas again in case it is hidden document.querySelector(".vizcanvas").classList.remove("hide"); // toggle footer section accordingly document.querySelector(".graph-footer").classList.add("hidden"); document.querySelector(".list-footer").classList.add("hidden"); var vizName = g.currentVisualization.name; document.querySelector("." + vizName + "-footer").classList.remove("hidden"); } /**************************************** * Format date string */ global.formattedDate = function formattedDate(date, format) { var d = (typeof date == "number") ? new Date(date) : date; var month = ["Jan", "Feb", "Mar", "Apr", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"][d.getMonth()]; var formatted = month + " " + d.getDate() + ", " + d.getFullYear(); if (format == "long") { var dayInWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()]; formatted = dayInWeek + ", " + formatted + " " + ((d.getHours() == 12) ? 12 : (d.getHours() % 12)) + ':' + d.toLocaleFormat('%M') + ['AM', 'PM'][Math.floor(d.getHours() / 12)]; } return formatted; }; global.singularOrPluralNoun = function singularOrPluralNoun(num, str) { if (typeof num != "number") { num = parseFloat(num); } return (num !== 1) ? str + "s" : str; }; /**************************************** * update Stats Bar */ global.updateStatsBar = function updateStatsBar() { var dateSince = "just now"; if (global.allConnections.length > 0) { dateSince = formattedDate(global.allConnections[0][2]); } document.querySelector(".top-bar .date-gathered").textContent = dateSince; document.querySelector(".top-bar .third-party-sites").textContent = aggregate.trackerCount + " " + singularOrPluralNoun(aggregate.trackerCount, "THIRD PARTY SITE"); document.querySelector(".top-bar .first-party-sites").textContent = aggregate.siteCount + " " + singularOrPluralNoun(aggregate.siteCount, "SITE"); }; })(this); data/initialPage.js0000644000000000000000000000032412741721616013257 0ustar rootrootvar openLightbeamButton = document.querySelector('button#openLightbeam'); if (openLightbeamButton) { openLightbeamButton.addEventListener('click', function () { self.port.emit('openLightbeam', {}); }); } data/infobar.js0000644000000000000000000003126112741721616012455 0ustar rootroot/* 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/. */ (function (global) { // Used for managing the DOM for infobar part of the page 'use strict'; var g = global; global.initMap = function initMap(mapcanvas, mapDocument) { var onDragMap = false; var mapDragStart = {}; var oriMapViewBox = mapcanvas.getAttribute('viewBox'); // update info when clicking on a node in the graph visualization document.querySelector('#content').addEventListener('click', function (event) { // click could happen on .node or an element inside of .node if (event.target.mozMatchesSelector('.node, .node *')) { var node = event.target; var name; if (node.mozMatchesSelector('[type=checkbox], td [type=checkbox]')) return; while (node.mozMatchesSelector('.node *')) { node = node.parentElement; } if (node.dataset && node.dataset.isBlocked) { return; } name = node.getAttribute("data-name"); selectedNodeEffect(name); updateInfo(name); } }, false); document.querySelector(".connections-list ul").addEventListener("click", function (event) { var name = event.target.textContent; var previouslySelected = document.querySelector(".connections-list ul li[data-selected]"); if (previouslySelected) { document.querySelector(".connections-list ul li[data-selected]").removeAttribute("data-selected"); } event.target.setAttribute("data-selected", true); resetAllGlow("connected"); connectedNodeEffect(name); }); var currentRequest; // get server info from http://freegeoip.net function getServerInfo(nodeName, callback) { var info = parseUri(nodeName); // uses Steven Levithan's parseUri 1.2.2 var jsonURL = "http://freegeoip.net/json/" + info.host; var request = new XMLHttpRequest(); currentRequest = info.host; request.open("GET", jsonURL, true); request.onload = function () { if (currentRequest === info.host) { callback((request.status === 200) ? JSON.parse(request.responseText) : false); } }; request.send(null); } // reset map function resetMap() { var preHighlight = mapDocument.querySelectorAll(".highlight-country"); if (preHighlight) { toArray(preHighlight).forEach(function (element) { element.classList.remove("highlight-country"); }); } mapcanvas.setAttribute("viewBox", oriMapViewBox); } // update map function updateMap(countryCode) { var countryOnMap = mapcanvas.getElementById(countryCode); if (!countryOnMap) { console.log('no country found for country code "%s"', countryCode); return; } countryOnMap.classList.add('highlight-country'); // position the highlighted country in center var svgViewBox = mapcanvas.getAttribute("viewBox").split(" "); var worldDimen = mapcanvas.getBoundingClientRect(); var countryDimen = countryOnMap.getBoundingClientRect(); var ratio = svgViewBox[2] / worldDimen.width; var worldCenter = { x: 0.5 * worldDimen.width + worldDimen.left, y: 0.5 * worldDimen.height + worldDimen.top }; var countryCenter = { x: 0.5 * countryDimen.width + countryDimen.left, y: 0.5 * countryDimen.height + countryDimen.top }; var newViewBox = { x: (countryCenter.x - worldCenter.x) * ratio, y: (countryCenter.y - worldCenter.y) * ratio, w: svgViewBox[2], h: svgViewBox[3] }; setZoom(newViewBox, mapcanvas); } document.querySelector(".pref-action .block").addEventListener('click', function (evt) { var site = this.dataset.siteName; confirmBlockSitesDialog(function (confirmed) { if (confirmed) { userSettings[site] = 'block'; global.self.port.emit('updateBlocklist', site, true); showSitePref(site); } }); evt.preventDefault(); }); document.querySelector(".pref-action .unblock").addEventListener('click', function (evt) { var site = this.dataset.siteName; userSettings[site] = ''; global.self.port.emit('updateBlocklist', site, false); showSitePref(site); evt.preventDefault(); }); // updates info on the info panel function updateInfo(nodeName) { // get server info and then update content on the info panel getServerInfo(nodeName, function (data) { var nodeList = aggregate.nodeForKey(nodeName); showFavIcon(nodeName); showFirstAndLastAccess(nodeList[nodeName]); showSitePref(nodeName); showConnectionsList(nodeName, nodeList); // display site profile in Info Panel showSiteProfile(); // update map after we have loaded the SVG showServerLocation(data); }); } function showFavIcon(nodeName) { var title = document.querySelector('.holder .title'); while (title.childNodes.length) { title.removeChild(title.firstChild); } title.appendChild(elem(nodeName, { src: 'http://' + nodeName + '/favicon.ico', 'class': 'favicon' })); title.appendChild(document.createTextNode(nodeName)); } function showFirstAndLastAccess(site) { var firstAccess = formattedDate(site.firstAccess, "long"); var lastAccess = formattedDate(site.lastAccess, "long"); document.querySelector('.info-first-access').textContent = firstAccess; document.querySelector('.info-last-access').textContent = lastAccess; } function showSitePref(nodeName) { var prefTag = document.querySelector(".pref-tag"); var blockButton = document.querySelector(".pref-action .block"); var unblockButton = document.querySelector(".pref-action .unblock"); var sitePref = userSettings[nodeName]; if (sitePref) { prefTag.querySelector("img").src = "icons/lightbeam_icon_" + sitePref + ".png"; prefTag.querySelector("span").className = ""; prefTag.querySelector("span").classList.add(sitePref + "-text"); prefTag.querySelector("span").textContent = (sitePref == "hide") ? "hidden" : sitePref + "ed"; prefTag.classList.remove("hidden"); if (sitePref == "block") { unblockButton.classList.remove("hidden"); blockButton.classList.add("hidden"); } else { unblockButton.classList.add("hidden"); blockButton.classList.remove("hidden"); } } else { prefTag.classList.add("hidden"); unblockButton.classList.add("hidden"); blockButton.classList.remove("hidden"); } unblockButton.dataset.siteName = nodeName; blockButton.dataset.siteName = nodeName; } function showConnectionsList(nodeName, nodeList) { var htmlList = elem('ul'); var numConnectedSites = 0; for (var key in nodeList) { if (key != nodeName) { // connected site htmlList.appendChild(elem('li', {}, key)); numConnectedSites++; } } document.querySelector(".num-connected-sites").textContent = numConnectedSites + " " + singularOrPluralNoun(numConnectedSites, "site"); var list = document.querySelector(".connections-list"); list.removeChild(list.querySelector('ul')); list.appendChild(htmlList); } function showServerLocation(serverData) { if (!serverData || serverData.country_name === "Reserved") { document.querySelector("#country").textContent = "(Unable to find server location)"; resetMap(); } else { // update country info only when it is different from the current one if (serverData.country_name !== document.querySelector("#country").textContent) { resetMap(); document.querySelector("#country").textContent = serverData.country_name; updateMap(serverData.country_code.toLowerCase()); } } } function showSiteProfile() { var siteProfileTab = document.querySelector(".toggle-site-profile"); var contentToBeShown = document.querySelector(".site-profile-content"); var infoPanelOpen = document.querySelector("#content").classList.contains("showinfo"); var siteProfileTabActive = document.querySelector(".toggle-site-profile").classList.contains("active"); if (!infoPanelOpen) { document.querySelector("#content").classList.add("showinfo"); showInfoPanelTab(siteProfileTab, contentToBeShown); } if (infoPanelOpen) { if (!siteProfileTabActive) { // make the previously active tab inactive deactivatePreviouslyActiveTab(); showInfoPanelTab(siteProfileTab, contentToBeShown); } } document.querySelector(".toggle-site-profile").classList.remove("disabled"); } /* mapcanvas events */ mapcanvas.addEventListener("mousedown", function (event) { onDragMap = true; mapDragStart.x = event.clientX; mapDragStart.y = event.clientY; }, false); mapcanvas.addEventListener("mousemove", function (event) { if (onDragMap) { mapcanvas.style.cursor = "-moz-grab"; var offsetX = (Math.ceil(event.clientX) - mapDragStart.x); var offsetY = (Math.ceil(event.clientY) - mapDragStart.y); var box = getZoom(mapcanvas); box.x -= (offsetX * 10); box.y -= (offsetY * 10); mapDragStart.x += offsetX; mapDragStart.y += offsetY; setZoom(box, mapcanvas); } }, false); mapcanvas.addEventListener("mouseup", function (event) { onDragMap = false; mapcanvas.style.cursor = "default"; }, false); mapcanvas.addEventListener("mouseleave", function (event) { onDragMap = false; mapcanvas.style.cursor = "default"; }, false); mapDocument.addEventListener("wheel", function (event) { if (event.target.mozMatchesSelector(".mapcanvas, .mapcanvas *")) { zoomWithinLimit(event, mapcanvas, mapZoomInLimit, mapZoomOutLimit); } }, false); }; /* Info Panel Tabs ======================================== */ /* Toggle Site Profile */ document.querySelector(".toggle-site-profile").addEventListener("click", function () { var tabClicked = document.querySelector(".toggle-site-profile"); if (!tabClicked.classList.contains("disabled")) { var contentToBeShown = document.querySelector(".site-profile-content"); toggleInfoPanelTab(tabClicked, contentToBeShown); } }); /* Toggle About */ document.querySelector(".toggle-about").addEventListener("click", function () { var tabClicked = document.querySelector(".toggle-about"); var contentToBeShown = document.querySelector(".about-content"); toggleInfoPanelTab(tabClicked, contentToBeShown); }); /* Toggle Help Sections */ global.helpOnClick = function helpOnClick() { var tabClicked = document.querySelector(".toggle-help"); var contentToBeShown = document.querySelector(".help-content ." + global.currentVisualization.name + "-view-help"); toggleInfoPanelTab(tabClicked, contentToBeShown); }; document.querySelector(".toggle-help").addEventListener("click", helpOnClick); function toggleInfoPanelTab(tabClicked, contentToBeShown) { var infoPanelOpen = document.querySelector("#content").classList.contains("showinfo"); var isActiveTab = tabClicked.classList.contains("active"); if (infoPanelOpen) { if (isActiveTab) { // collapse info panel document.querySelector("#content").classList.remove("showinfo"); tabClicked.classList.remove("active"); tabClicked.querySelector("img").classList.remove("hidden"); tabClicked.querySelector("i").classList.add("hidden"); } else { // make the previously active tab inactive deactivatePreviouslyActiveTab(); // make the selected tab active showInfoPanelTab(tabClicked, contentToBeShown); } } else { // open the info panel and make the selected tab active document.querySelector("#content").classList.add("showinfo"); showInfoPanelTab(tabClicked, contentToBeShown); } } function deactivatePreviouslyActiveTab() { var previouslyActiveTab = document.querySelector(".info-panel-controls ul li.active"); if (previouslyActiveTab) { previouslyActiveTab.classList.remove("active"); previouslyActiveTab.querySelector("img").classList.remove("hidden"); previouslyActiveTab.querySelector("i").classList.add("hidden"); } } // make the selected tab active function showInfoPanelTab(tabClicked, contentToBeShown) { tabClicked.classList.add("active"); tabClicked.querySelector("img").classList.add("hidden"); tabClicked.querySelector("i").classList.remove("hidden"); hideAllInfoPanelContentExcept(contentToBeShown); } function hideAllInfoPanelContentExcept(elmToShow) { document.querySelector(".site-profile-content").classList.add("hidden"); document.querySelector(".help-content .graph-view-help").classList.add("hidden"); document.querySelector(".help-content .list-view-help").classList.add("hidden"); document.querySelector(".about-content").classList.add("hidden"); if (elmToShow) { elmToShow.classList.remove("hidden"); } } })(this); data/index.html0000644000000000000000000010326612741721616012501 0ustar rootroot Lightbeam
VISUALIZATION
graph list
DATA
Save Data
Reset Data
Give Us Feedback
Uninstall Lightbeam
DATA GATHERED SINCE
YOU HAVE VISITED
YOU HAVE CONNECTED WITH
TRACKING PROTECTION
First Access Date
Last Access Date

This site is currently .

Unblock Site Block Site
Server Location
 
Connected to since first access.
Visualization Help
Visualization
Graph View
Best for
Seeing site relationships
DESCRIPTION

In this Graph visualization, circular nodes are websites you have visited and triangular nodes are third party sites. Over time, this graph will become a network of connections and nodes to visualize your activity on the web.

This graph may get complex quickly; to reduce the amount of information displayed, use FILTERS to help focus on a set of data.

Features
Zoom In + Out - on scroll
Pan - on click + drag
Tooltips - on hover
Website Profile - on click
Legends and Controls

These buttons serve as toggles to help identify various elements on the graph.

  • Cookies identify when a site has stored some data in your browser.

When you set your site preferences:
  • Block means they are blocked from connecting to your browser.
  • Watch means they are highlighted.
These preferences can be set in the List Visualization.

Visualization Help
Visualization
List View
Best for
Seeing a text database
Description

In this list Visualization, you can see a list of all websites you have visited and sites you have not visited but still connect to you. This view assists with sorting and filtering large numbers of sites.

Features
Checkboxes - click + apply site preference
Sort By - click column heading to sort by Type, Preference, Website(A-Z), Date, Sites Connected
Legends and Controls

These buttons apply actions to specific sites.

When you set your site preferences:

  • Block means sites are blocked from connecting to your browser.
  • Hide means sites are not seen in the visualization.
  • Watch means sites are highlighted.

About Lightbeam
Version

By
Mozilla Foundation
About this Add-on

Using interactive visualizations, Lightbeam allows you to see the first and third party sites you interact with on the Web. As you browse, Lightbeam reveals the full depth of the Web today, including parts that are not transparent to the average user. Using two distinct interactive graphic representations — Graph and List — Lightbeam enables you to examine individual third parties, identify where they connect to your online activity and provides ways for you to engage with this unique view of the Web.

Mozilla partnered with the Social + Interactive Media (SIM) Centre at Emily Carr University of Art + Design to produce the aesthetic and functional data visualizations for Lightbeam. To read more about the collaboration please visit: http://www.simcentre.ca/.

Lightbeam Privacy Notice

We care about your privacy. Lightbeam is a browser add-on that collects and helps you visualize third party requests on any site you visit. If you choose to send Lightbeam data to Mozilla (that’s us), our privacy policy describes how we handle that data.

Things you should know
  • After you install Lightbeam, the add-on collects data to help you visualize third party requests when you visit sites.
    • When you visit a site and that site contacts a third party, Lightbeam collects the following type of data: Domains of the visited sites and third parties, the existence of cookies, and a rough timestamp of when the site was visited. To see a complete list, please visit here.
  • By default, data collected by Lightbeam remains in your browser and is not sent to us.
Mozilla Privacy Policy – Learn More
Lightbeam Privacy

Your privacy is an important factor that Mozilla (that's us) considers in the development of each of our products and services. We are committed to being transparent and open and want you to know how we receive information about you, and what we do with that information once we have it.

What do we mean by "personal information?"

For us, "personal information" means information which identifies you, like your name or email address.

Any information that falls outside of this is "non-personal information."

If we store your personal information with information that is non-personal, we will consider the combination as personal information. If we remove all personal information from a set of data then the remaining is non-personal information.

How do we learn information about you?

We learn information about you when:

  • you give it to us directly (e.g., when you choose to send us crash reports from Firefox); we collect it automatically through our products and services (e.g., when we check whether your version of Firefox is up to date);
  • we collect it automatically through our products and services (e.g., when we check whether your version of Firefox is up to date);
  • someone else tells us information about you (e.g., Thunderbird works with your email providers to set up your account); or
  • when we try and understand more about you based on information you've given to us (e.g., when we use your IP address to customize language for some of our services).

What do we do with your information once we have it?

When you give us personal information, we will use it in the ways for which you've given us permission. Generally, we use your information to help us provide and improve our products and services for you.

When do we share your information with others?
  • When we have gotten your permission to share it.
  • For processing or providing products and services to you, but only if those entities receiving your information are contractually obligated to handle the data in ways that are approved by Mozilla.
  • When we are fulfilling our mission of being open. We sometimes release information to make our products better and foster an open web, but when we do so, we will remove your personal information and try to disclose it in a way that minimizes the risk of you being re-identified.
  • When the law requires it. We follow the law whenever we receive requests about you from a government or related to a lawsuit. We'll notify you when we're asked to hand over your personal information in this way unless we're legally prohibited from doing so. When we receive requests like this, we'll only release your personal information if we have a good faith belief that the law requires us to do so. Nothing in this policy is intended to limit any legal defenses or objections that you may have to a third party's request to disclose your information.
  • When we believe it is necessary to prevent harm to you or someone else. We will only share your information in this way if we have a good faith belief that it is reasonably necessary to protect the rights, property or safety of you, our other users, Mozilla or the public.
  • If our organizational structure or status changes (if we undergo a restructuring, are acquired, or go bankrupt) we may pass your information to a successor or affiliate.

How do we store and protect your personal information?

We are committed to protecting your personal information once we have it. We implement physical, business and technical security measures. Despite our efforts, if we learn of a security breach, we'll notify you so that you can take appropriate protective steps.

We also don't want your personal information for any longer than we need it, so we only keep it long enough to do what we collected it for. Once we don't need it, we take steps to destroy it unless we are required by law to keep it longer.

What else do we want you to know?

We're a global organization and our computers are in several different places around the world. We also use service providers whose computers may also be in various countries. This means that your information might end up on one of those computers in another country, and that country may have a different level of data protection regulation than yours. By giving us information, you consent to this kind of transfer of your information. No matter what country your information is in, we comply with applicable law and will also abide by the commitments we make in this privacy policy.

If you are under 13, we don't want your personal information, and you must not provide it to us. If you are a parent and believe that your child who is under 13 has provided us with personal information, please contact us at lightbeam-privacy@mozilla.org to have your child's information removed.

What if we change this policy?

We may need to change this policy and when we do, we'll notify you.

Contact

IRC channel is #lightbeam on irc.mozilla.org.
GitHub: https://github.com/mozilla/lightbeam

Graph View
TOGGLE CONTROLS
Filter Hide
Visited Sites
Third Party Sites
Connections
Watched Sites
Blocked Sites
Cookies
recent site last 10 sites daily weekly
SITE PREFERENCES
Hide
Hide Hidden Sites
data/graph.js0000644000000000000000000003060312741721616012135 0ustar rootroot/* 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/. */ // Graph Visualization (one of 3 views: graph, clock, and list). This is way // too heavyweight for mobile right now. // Visualization of tracking data interconnections (function (visualizations, global) { "use strict"; // Bunch of utilities related to UI elements. const graphNodeRadius = { "graph": 12 }; // The graph is an emitter with a default size. var graph = new Emitter(); visualizations.graph = graph; graph.name = "graph"; var width = 750, height = 750; var force, vis; var edges, nodes; // There are three phases for a visualization life-cycle: // init does initialization and receives the existing set of connections // connection notifies of a new connection that matches existing filter // remove lets the visualization know it is about to be switched out so it can clean up graph.on('init', onInit); // graph.on('connection', onConnection); graph.on('remove', onRemove); graph.on('reset', onReset); /* for Highlighting and Colouring -------------------- */ var highlight = { visited: true, neverVisited: true, connections: true, cookies: true, watched: true, blocked: true }; // Restart the simulation. This is only called when there's a new connection we // haven't seen before. function onUpdate() { // new nodes, reheat graph simulation if (force) { console.debug('restarting graph due to update'); force.stop(); force.nodes(filteredAggregate.nodes); force.links(filteredAggregate.edges); force.start(); updateGraph(); colourHighlightNodes(highlight); } else { console.debug('the force is not with us'); } } function onInit() { console.debug('graph::onInit()'); console.debug('initializing graph from ' + filteredAggregate.nodes.length + ' connections'); // Handles all of the panning and scaling. vis = d3.select(vizcanvas); // A D3 visualization has a two main components, data-shaping, and setting up the D3 callbacks // This binds our data to the D3 visualization and sets up the callbacks initGraph(); aggregate.on('update', onUpdate); // Different visualizations may have different viewBoxes, so make sure we use the right one vizcanvas.setAttribute('viewBox', [0, 0, width, height].join(' ')); console.debug('graph::onInit end'); document.querySelector(".filter-display").classList.remove("hidden"); } function onRemove() { var startTime = Date.now(); if (force) { force.stop(); force = null; } resetCanvas(); document.querySelector(".filter-display").classList.add("hidden"); console.debug('it took %s ms to remove graph view', Date.now() - startTime); } function onReset() { onRemove(); aggregate.emit('load', global.allConnections); } // UTILITIES FOR CREATING POLYGONS function point(angle, size) { return [Math.round(Math.cos(angle) * size), -Math.round(Math.sin(angle) * size)]; } function polygon(points, size, debug) { var increment = Math.PI * 2 / points; var angles = [], i; for (i = 0; i < points; i++) { angles.push(i * increment + Math.PI / 2); // add 90 degrees so first point is up } return angles.map(function (angle) { return point(angle, size); }); } function polygonAsString(points, size) { var poly = polygon(points, size); return poly.map(function (pair) { return pair.join(','); }).join(' '); } // ACCESSOR FUNCTIONS // function scaleNode(node){ return 'translate(' + node.x + ',' + node.y + ') scale(' + (1 + .05 * node.weight) + ')'; } function visited(node) { return node.nodeType === 'site' || node.nodeType === 'both'; } function notVisited(node) { return node.nodeType === 'thirdparty'; } // function timestamp(node){ return node.lastAccess.toISOString(); } // function nodeHighlight(node){ return ( node.visitedCount > 0 ) ? highlight.highlightVisited : highlight.highlightNeverVisited; } // function sourceX(edge){ return edge.source.x; } // function sourceY(edge){ return edge.source.y; } // function targetX(edge){ return edge.target.x; } // function targetY(edge){ return edge.target.y; } // function edgeCookie(edge){ return edge.cookieCount > 0; } // function edgeHighlight(edge){ return highlight.connections; } // function edgeColoured(edge){ return edge.cookieCount > 0 && highlight.cookies; } function nodeName(node) { if (node) { return node.name; } return undefined; } function siteHasPref(site, pref) { return (userSettings.hasOwnProperty(site) && userSettings[site].contains(pref)); } function watchSite(node) { return siteHasPref(node.name, "watch"); } function blockSite(node) { return siteHasPref(node.name, "block"); } // SET UP D3 HANDLERS var ticking = false; function charge(d) { return -(500 + d.weight * 25); } function colourHighlightNodes(highlight) { var i; var watchedSites = document.querySelectorAll(".watched"); var blockedSites = document.querySelectorAll(".blocked"); if (highlight.watched) { for (i = 0; i < watchedSites.length; i++) { watchedSites[i].classList.add("watchedSites"); } } else { for (i = 0; i < watchedSites.length; i++) { watchedSites[i].classList.remove("watchedSites"); } } if (highlight.blocked) { for (i = 0; i < blockedSites.length; i++) { blockedSites[i].classList.add("blockedSites"); } } else { for (i = 0; i < blockedSites.length; i++) { blockedSites[i].classList.remove("blockedSites"); } } } function initGraph() { // Initialize D3 layout and bind data console.debug('initGraph()'); force = d3.layout.force() .nodes(filteredAggregate.nodes) .links(filteredAggregate.edges) .charge(charge) .size([width, height]) .start(); updateGraph(); // Terrible hack. Something about the d3 setup is wrong, and forcing onClick // causes d3 to redraw in a way that most of the graph is visible on screen. global.helpOnClick(); global.helpOnClick(); colourHighlightNodes(highlight); // update method var lastUpdate, lastTick; lastUpdate = lastTick = Date.now(); var draws = []; var ticks = 0; const second = 1000; const minute = 60 * second; force.on('tick', function ontick(evt) { // find a way to report how often tick() is called, and how long it takes to run // without trying to console.log() every 5 milliseconds... if (ticking) { console.log('overlapping tick!'); return; } ticking = true; var nextTick = Date.now(); ticks++; lastTick = nextTick; if ((lastTick - lastUpdate) > second) { // console.log('%s ticks per second, each draw takes %s milliseconds', ticks, Math.floor(d3.mean(draws))); lastUpdate = lastTick; draws = []; ticks = 0; } edges.each(function (d, i) { // `this` is the DOM node this.setAttribute('x1', d.source.x); this.setAttribute('y1', d.source.y); this.setAttribute('x2', d.target.x); this.setAttribute('y2', d.target.y); if (d.cookieCount) { this.classList.add('cookieYes'); } else { this.classList.remove('cookieYes'); } if (highlight.connections) { this.classList.add('highlighted'); } else { this.classList.remove('highlighted'); } if (d.cookieCount && highlight.cookies) { this.classList.add('coloured'); } else { this.classList.remove('coloured'); } }); nodes.each(function (d, i) { // `this` is the DOM node this.setAttribute('transform', 'translate(' + d.x + ',' + d.y + ') scale(' + (1 + 0.05 * d.weight) + ')'); this.setAttribute('data-timestamp', d.lastAccess.toISOString()); if (d.nodeType === 'site' || d.nodeType === 'both') { this.classList.add('visitedYes'); this.classList.remove('visitedNo'); } else { this.classList.add('visitedNo'); this.classList.remove('visitedYes'); } if ((d.nodeType === 'site' || d.nodeType === 'both') && highlight.visited) { this.classList.add('highlighted'); } else if ((d.nodeType === 'thirdparty') && highlight.neverVisited) { this.classList.add('highlighted'); } else { this.classList.remove('highlighted'); } }); var endDraw = Date.now(); draws.push(endDraw - lastTick); if (force) { nodes.call(force.drag); } ticking = false; }); } function updateGraph() { console.debug('updateGraph()'); // Data binding for links edges = vis.selectAll('.edge') .data(filteredAggregate.edges, nodeName); edges.enter().insert('line', ':first-child') .classed('edge', true); edges.exit() .remove(); nodes = vis.selectAll('.node') .data(filteredAggregate.nodes, nodeName); nodes.enter().append('g') .classed('visitedYes', visited) .classed('visitedNo', notVisited) .classed("watched", watchSite) .classed("blocked", blockSite) .call(addShape) .attr('data-name', nodeName) .on('mouseenter', tooltip.show) .on('mouseleave', tooltip.hide) .classed('node', true); nodes.exit() .remove(); } function addFavicon(selection) { selection.append("svg:image") .attr("class", "favicon") .attr("width", "16") // move these to the favicon class in css .attr("height", "16") .attr("x", "-8") // offset to make 16x16 favicon appear centered .attr("y", "-8") .attr("xlink:href", function (node) { return 'http://' + node.name + '/favicon.ico'; }); } function addCircle(selection) { selection .append('circle') .attr('cx', 0) .attr('cy', 0) .attr('r', graphNodeRadius.graph) .classed('site', true); } function addShape(selection) { selection.filter('.visitedYes').call(addCircle).call(addFavicon); selection.filter('.visitedNo').call(addTriangle).call(addFavicon); } function addTriangle(selection) { selection .append('polygon') .attr('points', polygonAsString(3, 20)) .attr('data-name', function (node) { return node.name; }); } // FIXME: Move this out of visualization so multiple visualizations can use it. function resetCanvas() { // You will still need to remove timer events var parent = vizcanvas.parentNode; var newcanvas = vizcanvas.cloneNode(false); var vizcanvasDefs = document.querySelector(".vizcanvas defs").cloneNode(true); newcanvas.appendChild(vizcanvasDefs); parent.replaceChild(newcanvas, vizcanvas); vizcanvas = newcanvas; aggregate.off('update', onUpdate); } var graphLegend = document.querySelector(".graph-footer"); function legendBtnClickHandler(legendElm) { legendElm.querySelector(".legend-controls").addEventListener("click", function (event) { if (event.target.mozMatchesSelector(".btn, .btn *")) { var btn = event.target; while (btn.mozMatchesSelector('.btn *')) { btn = btn.parentElement; } btn.classList.toggle("active"); } }); } legendBtnClickHandler(graphLegend); graphLegend.querySelector(".legend-toggle-visited").addEventListener("click", function (event) { var visited = document.querySelectorAll(".visitedYes"); toggleVizElements(visited, "highlighted"); highlight.visited = !highlight.visited; }); graphLegend.querySelector(".legend-toggle-never-visited").addEventListener("click", function (event) { var neverVisited = document.querySelectorAll(".visitedNo"); toggleVizElements(neverVisited, "highlighted"); highlight.neverVisited = !highlight.neverVisited; }); graphLegend.querySelector(".legend-toggle-connections").addEventListener("click", function (event) { var cookiesConnections = document.querySelectorAll(".edge"); toggleVizElements(cookiesConnections, "highlighted"); highlight.connections = !highlight.connections; }); graphLegend.querySelector(".legend-toggle-cookies").addEventListener("click", function (event) { var cookiesConnections = document.querySelectorAll(".cookieYes"); toggleVizElements(cookiesConnections, "coloured"); highlight.cookies = !highlight.cookies; }); graphLegend.querySelector(".legend-toggle-watched").addEventListener("click", function (event) { highlight.watched = !highlight.watched; colourHighlightNodes(highlight); }); graphLegend.querySelector(".legend-toggle-blocked").addEventListener("click", function (event) { highlight.blocked = !highlight.blocked; colourHighlightNodes(highlight); }); graphLegend.querySelector(".legend-toggle").addEventListener("click", function (event) { toggleLegendSection(event.target, graphLegend); }); })(visualizations, this); data/font-awesome.css0000644000000000000000000005157512741721616013627 0ustar rootroot/*! * Font Awesome 3.0.1 * the iconic font designed for use with Twitter Bootstrap * ------------------------------------------------------- * The full suite of pictographic icons, examples, and documentation * can be found at: http://fortawesome.github.com/Font-Awesome/ * * License * ------------------------------------------------------- * - The Font Awesome font is licensed under the SIL Open Font License - http://scripts.sil.org/OFL * - Font Awesome CSS, LESS, and SASS files are licensed under the MIT License - * http://opensource.org/licenses/mit-license.html * - The Font Awesome pictograms are licensed under the CC BY 3.0 License - http://creativecommons.org/licenses/by/3.0/ * - Attribution is no longer required in Font Awesome 3.0, but much appreciated: * "Font Awesome by Dave Gandy - http://fortawesome.github.com/Font-Awesome" * Contact * ------------------------------------------------------- * Email: dave@davegandy.com * Twitter: http://twitter.com/fortaweso_me * Work: Lead Product Designer @ http://kyruus.com */ @font-face { font-family: 'FontAwesome'; src: url('font/fontawesome-webfont.woff?') format('woff'); font-weight: normal; font-style: normal; } /* Font Awesome styles ------------------------------------------------------- */ [class^="icon-"], [class*=" icon-"] { font-family: FontAwesome; font-weight: normal; font-style: normal; text-decoration: inherit; -webkit-font-smoothing: antialiased; /* sprites.less reset */ display: inline; width: auto; height: auto; line-height: normal; vertical-align: baseline; background-image: none; background-position: 0% 0%; background-repeat: repeat; margin-top: 0; } /* more sprites.less reset */ .icon-white, .nav-pills > .active > a > [class^="icon-"], .nav-pills > .active > a > [class*=" icon-"], .nav-list > .active > a > [class^="icon-"], .nav-list > .active > a > [class*=" icon-"], .navbar-inverse .nav > .active > a > [class^="icon-"], .navbar-inverse .nav > .active > a > [class*=" icon-"], .dropdown-menu > li > a:hover > [class^="icon-"], .dropdown-menu > li > a:hover > [class*=" icon-"], .dropdown-menu > .active > a > [class^="icon-"], .dropdown-menu > .active > a > [class*=" icon-"], .dropdown-submenu:hover > a > [class^="icon-"], .dropdown-submenu:hover > a > [class*=" icon-"] { background-image: none; } [class^="icon-"]:before, [class*=" icon-"]:before { text-decoration: inherit; display: inline-block; speak: none; } /* makes sure icons active on rollover in links */ a [class^="icon-"], a [class*=" icon-"] { display: inline-block; } /* makes the font 33% larger relative to the icon container */ .icon-large:before { vertical-align: -10%; font-size: 1.3333333333333333em; } .btn [class^="icon-"], .nav [class^="icon-"], .btn [class*=" icon-"], .nav [class*=" icon-"] { display: inline; /* keeps button heights with and without icons the same */ } .btn [class^="icon-"].icon-large, .nav [class^="icon-"].icon-large, .btn [class*=" icon-"].icon-large, .nav [class*=" icon-"].icon-large { line-height: .9em; } .btn [class^="icon-"].icon-spin, .nav [class^="icon-"].icon-spin, .btn [class*=" icon-"].icon-spin, .nav [class*=" icon-"].icon-spin { display: inline-block; } .nav-tabs [class^="icon-"], .nav-pills [class^="icon-"], .nav-tabs [class*=" icon-"], .nav-pills [class*=" icon-"] { /* keeps button heights with and without icons the same */ } .nav-tabs [class^="icon-"], .nav-pills [class^="icon-"], .nav-tabs [class*=" icon-"], .nav-pills [class*=" icon-"], .nav-tabs [class^="icon-"].icon-large, .nav-pills [class^="icon-"].icon-large, .nav-tabs [class*=" icon-"].icon-large, .nav-pills [class*=" icon-"].icon-large { line-height: .9em; } li [class^="icon-"], .nav li [class^="icon-"], li [class*=" icon-"], .nav li [class*=" icon-"] { display: inline-block; width: 1.25em; text-align: center; } li [class^="icon-"].icon-large, .nav li [class^="icon-"].icon-large, li [class*=" icon-"].icon-large, .nav li [class*=" icon-"].icon-large { /* increased font size for icon-large */ width: 1.5625em; } ul.icons { list-style-type: none; text-indent: -0.75em; } ul.icons li [class^="icon-"], ul.icons li [class*=" icon-"] { width: .75em; } .icon-muted { color: #eeeeee; } .icon-border { border: solid 1px #eeeeee; padding: .2em .25em .15em; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } .icon-2x { font-size: 2em; } .icon-2x.icon-border { border-width: 2px; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .icon-3x { font-size: 3em; } .icon-3x.icon-border { border-width: 3px; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; } .icon-4x { font-size: 4em; } .icon-4x.icon-border { border-width: 4px; -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; } .pull-right { float: right; } .pull-left { float: left; } [class^="icon-"].pull-left, [class*=" icon-"].pull-left { margin-right: .3em; } [class^="icon-"].pull-right, [class*=" icon-"].pull-right { margin-left: .3em; } .btn [class^="icon-"].pull-left.icon-2x, .btn [class*=" icon-"].pull-left.icon-2x, .btn [class^="icon-"].pull-right.icon-2x, .btn [class*=" icon-"].pull-right.icon-2x { margin-top: .18em; } .btn [class^="icon-"].icon-spin.icon-large, .btn [class*=" icon-"].icon-spin.icon-large { line-height: .8em; } .btn.btn-small [class^="icon-"].pull-left.icon-2x, .btn.btn-small [class*=" icon-"].pull-left.icon-2x, .btn.btn-small [class^="icon-"].pull-right.icon-2x, .btn.btn-small [class*=" icon-"].pull-right.icon-2x { margin-top: .25em; } .btn.btn-large [class^="icon-"], .btn.btn-large [class*=" icon-"] { margin-top: 0; } .btn.btn-large [class^="icon-"].pull-left.icon-2x, .btn.btn-large [class*=" icon-"].pull-left.icon-2x, .btn.btn-large [class^="icon-"].pull-right.icon-2x, .btn.btn-large [class*=" icon-"].pull-right.icon-2x { margin-top: .05em; } .btn.btn-large [class^="icon-"].pull-left.icon-2x, .btn.btn-large [class*=" icon-"].pull-left.icon-2x { margin-right: .2em; } .btn.btn-large [class^="icon-"].pull-right.icon-2x, .btn.btn-large [class*=" icon-"].pull-right.icon-2x { margin-left: .2em; } .icon-spin { display: inline-block; -moz-animation: spin 2s infinite linear; -o-animation: spin 2s infinite linear; -webkit-animation: spin 2s infinite linear; animation: spin 2s infinite linear; } @-moz-keyframes spin { 0% { -moz-transform: rotate(0deg); } 100% { -moz-transform: rotate(359deg); } } @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(359deg); } } @-o-keyframes spin { 0% { -o-transform: rotate(0deg); } 100% { -o-transform: rotate(359deg); } } @-ms-keyframes spin { 0% { -ms-transform: rotate(0deg); } 100% { -ms-transform: rotate(359deg); } } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(359deg); } } @-moz-document url-prefix() { .icon-spin { height: .9em; } .btn .icon-spin { height: auto; } .icon-spin.icon-large { height: 1.25em; } .btn .icon-spin.icon-large { height: .75em; } } /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen readers do not read off random characters that represent icons */ .icon-glass:before { content: "\f000"; } .icon-music:before { content: "\f001"; } .icon-search:before { content: "\f002"; } .icon-envelope:before { content: "\f003"; } .icon-heart:before { content: "\f004"; } .icon-star:before { content: "\f005"; } .icon-star-empty:before { content: "\f006"; } .icon-user:before { content: "\f007"; } .icon-film:before { content: "\f008"; } .icon-th-large:before { content: "\f009"; } .icon-th:before { content: "\f00a"; } .icon-th-list:before { content: "\f00b"; } .icon-ok:before { content: "\f00c"; } .icon-remove:before { content: "\f00d"; } .icon-zoom-in:before { content: "\f00e"; } .icon-zoom-out:before { content: "\f010"; } .icon-off:before { content: "\f011"; } .icon-signal:before { content: "\f012"; } .icon-cog:before { content: "\f013"; } .icon-trash:before { content: "\f014"; } .icon-home:before { content: "\f015"; } .icon-file:before { content: "\f016"; } .icon-time:before { content: "\f017"; } .icon-road:before { content: "\f018"; } .icon-download-alt:before { content: "\f019"; } .icon-download:before { content: "\f01a"; } .icon-upload:before { content: "\f01b"; } .icon-inbox:before { content: "\f01c"; } .icon-play-circle:before { content: "\f01d"; } .icon-repeat:before { content: "\f01e"; } /* \f020 doesn't work in Safari. all shifted one down */ .icon-refresh:before { content: "\f021"; } .icon-list-alt:before { content: "\f022"; } .icon-lock:before { content: "\f023"; } .icon-flag:before { content: "\f024"; } .icon-headphones:before { content: "\f025"; } .icon-volume-off:before { content: "\f026"; } .icon-volume-down:before { content: "\f027"; } .icon-volume-up:before { content: "\f028"; } .icon-qrcode:before { content: "\f029"; } .icon-barcode:before { content: "\f02a"; } .icon-tag:before { content: "\f02b"; } .icon-tags:before { content: "\f02c"; } .icon-book:before { content: "\f02d"; } .icon-bookmark:before { content: "\f02e"; } .icon-print:before { content: "\f02f"; } .icon-camera:before { content: "\f030"; } .icon-font:before { content: "\f031"; } .icon-bold:before { content: "\f032"; } .icon-italic:before { content: "\f033"; } .icon-text-height:before { content: "\f034"; } .icon-text-width:before { content: "\f035"; } .icon-align-left:before { content: "\f036"; } .icon-align-center:before { content: "\f037"; } .icon-align-right:before { content: "\f038"; } .icon-align-justify:before { content: "\f039"; } .icon-list:before { content: "\f03a"; } .icon-indent-left:before { content: "\f03b"; } .icon-indent-right:before { content: "\f03c"; } .icon-facetime-video:before { content: "\f03d"; } .icon-picture:before { content: "\f03e"; } .icon-pencil:before { content: "\f040"; } .icon-map-marker:before { content: "\f041"; } .icon-adjust:before { content: "\f042"; } .icon-tint:before { content: "\f043"; } .icon-edit:before { content: "\f044"; } .icon-share:before { content: "\f045"; } .icon-check:before { content: "\f046"; } .icon-move:before { content: "\f047"; } .icon-step-backward:before { content: "\f048"; } .icon-fast-backward:before { content: "\f049"; } .icon-backward:before { content: "\f04a"; } .icon-play:before { content: "\f04b"; } .icon-pause:before { content: "\f04c"; } .icon-stop:before { content: "\f04d"; } .icon-forward:before { content: "\f04e"; } .icon-fast-forward:before { content: "\f050"; } .icon-step-forward:before { content: "\f051"; } .icon-eject:before { content: "\f052"; } .icon-chevron-left:before { content: "\f053"; } .icon-chevron-right:before { content: "\f054"; } .icon-plus-sign:before { content: "\f055"; } .icon-minus-sign:before { content: "\f056"; } .icon-remove-sign:before { content: "\f057"; } .icon-ok-sign:before { content: "\f058"; } .icon-question-sign:before { content: "\f059"; } .icon-info-sign:before { content: "\f05a"; } .icon-screenshot:before { content: "\f05b"; } .icon-remove-circle:before { content: "\f05c"; } .icon-ok-circle:before { content: "\f05d"; } .icon-ban-circle:before { content: "\f05e"; } .icon-arrow-left:before { content: "\f060"; } .icon-arrow-right:before { content: "\f061"; } .icon-arrow-up:before { content: "\f062"; } .icon-arrow-down:before { content: "\f063"; } .icon-share-alt:before { content: "\f064"; } .icon-resize-full:before { content: "\f065"; } .icon-resize-small:before { content: "\f066"; } .icon-plus:before { content: "\f067"; } .icon-minus:before { content: "\f068"; } .icon-asterisk:before { content: "\f069"; } .icon-exclamation-sign:before { content: "\f06a"; } .icon-gift:before { content: "\f06b"; } .icon-leaf:before { content: "\f06c"; } .icon-fire:before { content: "\f06d"; } .icon-eye-open:before { content: "\f06e"; } .icon-eye-close:before { content: "\f070"; } .icon-warning-sign:before { content: "\f071"; } .icon-plane:before { content: "\f072"; } .icon-calendar:before { content: "\f073"; } .icon-random:before { content: "\f074"; } .icon-comment:before { content: "\f075"; } .icon-magnet:before { content: "\f076"; } .icon-chevron-up:before { content: "\f077"; } .icon-chevron-down:before { content: "\f078"; } .icon-retweet:before { content: "\f079"; } .icon-shopping-cart:before { content: "\f07a"; } .icon-folder-close:before { content: "\f07b"; } .icon-folder-open:before { content: "\f07c"; } .icon-resize-vertical:before { content: "\f07d"; } .icon-resize-horizontal:before { content: "\f07e"; } .icon-bar-chart:before { content: "\f080"; } .icon-twitter-sign:before { content: "\f081"; } .icon-facebook-sign:before { content: "\f082"; } .icon-camera-retro:before { content: "\f083"; } .icon-key:before { content: "\f084"; } .icon-cogs:before { content: "\f085"; } .icon-comments:before { content: "\f086"; } .icon-thumbs-up:before { content: "\f087"; } .icon-thumbs-down:before { content: "\f088"; } .icon-star-half:before { content: "\f089"; } .icon-heart-empty:before { content: "\f08a"; } .icon-signout:before { content: "\f08b"; } .icon-linkedin-sign:before { content: "\f08c"; } .icon-pushpin:before { content: "\f08d"; } .icon-external-link:before { content: "\f08e"; } .icon-signin:before { content: "\f090"; } .icon-trophy:before { content: "\f091"; } .icon-github-sign:before { content: "\f092"; } .icon-upload-alt:before { content: "\f093"; } .icon-lemon:before { content: "\f094"; } .icon-phone:before { content: "\f095"; } .icon-check-empty:before { content: "\f096"; } .icon-bookmark-empty:before { content: "\f097"; } .icon-phone-sign:before { content: "\f098"; } .icon-twitter:before { content: "\f099"; } .icon-facebook:before { content: "\f09a"; } .icon-github:before { content: "\f09b"; } .icon-unlock:before { content: "\f09c"; } .icon-credit-card:before { content: "\f09d"; } .icon-rss:before { content: "\f09e"; } .icon-hdd:before { content: "\f0a0"; } .icon-bullhorn:before { content: "\f0a1"; } .icon-bell:before { content: "\f0a2"; } .icon-certificate:before { content: "\f0a3"; } .icon-hand-right:before { content: "\f0a4"; } .icon-hand-left:before { content: "\f0a5"; } .icon-hand-up:before { content: "\f0a6"; } .icon-hand-down:before { content: "\f0a7"; } .icon-circle-arrow-left:before { content: "\f0a8"; } .icon-circle-arrow-right:before { content: "\f0a9"; } .icon-circle-arrow-up:before { content: "\f0aa"; } .icon-circle-arrow-down:before { content: "\f0ab"; } .icon-globe:before { content: "\f0ac"; } .icon-wrench:before { content: "\f0ad"; } .icon-tasks:before { content: "\f0ae"; } .icon-filter:before { content: "\f0b0"; } .icon-briefcase:before { content: "\f0b1"; } .icon-fullscreen:before { content: "\f0b2"; } .icon-group:before { content: "\f0c0"; } .icon-link:before { content: "\f0c1"; } .icon-cloud:before { content: "\f0c2"; } .icon-beaker:before { content: "\f0c3"; } .icon-cut:before { content: "\f0c4"; } .icon-copy:before { content: "\f0c5"; } .icon-paper-clip:before { content: "\f0c6"; } .icon-save:before { content: "\f0c7"; } .icon-sign-blank:before { content: "\f0c8"; } .icon-reorder:before { content: "\f0c9"; } .icon-list-ul:before { content: "\f0ca"; } .icon-list-ol:before { content: "\f0cb"; } .icon-strikethrough:before { content: "\f0cc"; } .icon-underline:before { content: "\f0cd"; } .icon-table:before { content: "\f0ce"; } .icon-magic:before { content: "\f0d0"; } .icon-truck:before { content: "\f0d1"; } .icon-pinterest:before { content: "\f0d2"; } .icon-pinterest-sign:before { content: "\f0d3"; } .icon-google-plus-sign:before { content: "\f0d4"; } .icon-google-plus:before { content: "\f0d5"; } .icon-money:before { content: "\f0d6"; } .icon-caret-down:before { content: "\f0d7"; } .icon-caret-up:before { content: "\f0d8"; } .icon-caret-left:before { content: "\f0d9"; } .icon-caret-right:before { content: "\f0da"; } .icon-columns:before { content: "\f0db"; } .icon-sort:before { content: "\f0dc"; } .icon-sort-down:before { content: "\f0dd"; } .icon-sort-up:before { content: "\f0de"; } .icon-envelope-alt:before { content: "\f0e0"; } .icon-linkedin:before { content: "\f0e1"; } .icon-undo:before { content: "\f0e2"; } .icon-legal:before { content: "\f0e3"; } .icon-dashboard:before { content: "\f0e4"; } .icon-comment-alt:before { content: "\f0e5"; } .icon-comments-alt:before { content: "\f0e6"; } .icon-bolt:before { content: "\f0e7"; } .icon-sitemap:before { content: "\f0e8"; } .icon-umbrella:before { content: "\f0e9"; } .icon-paste:before { content: "\f0ea"; } .icon-lightbulb:before { content: "\f0eb"; } .icon-exchange:before { content: "\f0ec"; } .icon-cloud-download:before { content: "\f0ed"; } .icon-cloud-upload:before { content: "\f0ee"; } .icon-user-md:before { content: "\f0f0"; } .icon-stethoscope:before { content: "\f0f1"; } .icon-suitcase:before { content: "\f0f2"; } .icon-bell-alt:before { content: "\f0f3"; } .icon-coffee:before { content: "\f0f4"; } .icon-food:before { content: "\f0f5"; } .icon-file-alt:before { content: "\f0f6"; } .icon-building:before { content: "\f0f7"; } .icon-hospital:before { content: "\f0f8"; } .icon-ambulance:before { content: "\f0f9"; } .icon-medkit:before { content: "\f0fa"; } .icon-fighter-jet:before { content: "\f0fb"; } .icon-beer:before { content: "\f0fc"; } .icon-h-sign:before { content: "\f0fd"; } .icon-plus-sign-alt:before { content: "\f0fe"; } .icon-double-angle-left:before { content: "\f100"; } .icon-double-angle-right:before { content: "\f101"; } .icon-double-angle-up:before { content: "\f102"; } .icon-double-angle-down:before { content: "\f103"; } .icon-angle-left:before { content: "\f104"; } .icon-angle-right:before { content: "\f105"; } .icon-angle-up:before { content: "\f106"; } .icon-angle-down:before { content: "\f107"; } .icon-desktop:before { content: "\f108"; } .icon-laptop:before { content: "\f109"; } .icon-tablet:before { content: "\f10a"; } .icon-mobile-phone:before { content: "\f10b"; } .icon-circle-blank:before { content: "\f10c"; } .icon-quote-left:before { content: "\f10d"; } .icon-quote-right:before { content: "\f10e"; } .icon-spinner:before { content: "\f110"; } .icon-circle:before { content: "\f111"; } .icon-reply:before { content: "\f112"; } .icon-github-alt:before { content: "\f113"; } .icon-folder-close-alt:before { content: "\f114"; } .icon-folder-open-alt:before { content: "\f115"; } data/first-run.html0000644000000000000000000000414612741721616013320 0ustar rootroot Welcome to Lightbeam!

Thanks for downloading Lightbeam!

  1. Open the Lightbeam tab by clicking the logo in the Add-on bar.
  2. Lightbeam starts recording connections as soon as it's installed. If you open it now, you'll see a blank screen because nothing's been recorded yet.

    To start visualizing your online interactions, open a new tab, navigate to a site, and then check back to the Lightbeam tab to see how your connections graph appears.

    • Learn more about Mozilla's Lightbeam for Firefox on our project page
    • Found a bug? Report an issue on Github
    • Don't forget to leave a review on our Mozilla Addons page!
data/first-run.css0000644000000000000000000000247312741721616013145 0ustar rootroot/* 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/. */ body { background: #5d667a; color:#333; font-family: "Open Sans", sans-serif; margin:0; /* unset browser styles */ padding-bottom:16px; } .center { text-align: center; } img.center, button.center { display: block; margin: 0 auto; } img#lightbeam-logo { margin-bottom:-18px; } body > ol, body > ul { width: 578px; margin: 0 auto; } ol#steps { /* unset default styles */ list-style-type:none; padding:0px; /* counter for custom numbers */ counter-reset: steps-counter; } ol#steps > li { background:white; padding:20px; border-radius: 0.5em; margin-bottom:24px; position:relative; } ol#steps > li:before { content: counter(steps-counter); counter-increment: steps-counter; font-size:31px; font-weight:bold; position: absolute; top: 8px; left: -68px; background:white; width:48px; height:48px; border-radius:24px; text-align:center; } ol#steps p { margin:0; /* unset default styles */ margin-bottom: 16px; } ol#steps > li p:last-child { margin-bottom: 0; } ul#contact { padding-left:16px } button#openLightbeam { font-size:18px; font-weight:strong; padding:8px; } data/events.js0000644000000000000000000000300112741721616012330 0ustar rootroot/* 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/. */ // Basic implementation of an event emitter for visualization plugins // This may be built-in to jetpack, but it's not available on the HTML side so // we need to keep this. function Emitter() { this._listeners = {}; } Emitter.prototype.on = function on(eventName, listener) { if (!this._listeners[eventName]) { this._listeners[eventName] = []; } this._listeners[eventName].push(listener); }; Emitter.prototype.once = function once(eventName, listener) { var self = this; var wrapped = function wrapped(msg1, msg2, msg3) { listener(msg1, msg2, msg3); self.removeListener(eventName, wrapped); }; this.on(eventName, wrapped); }; Emitter.prototype.off = function off(eventName, listener) { if (!this._listeners[eventName]) return; var listenerIndex = this._listeners[eventName].indexOf(listener); if (listenerIndex < 0) return; this._listeners[eventName].splice(listenerIndex, 1); }; Emitter.prototype.removeAllListeners = function removeAllListeners(eventName) { this._listeners[eventName] = []; }; Emitter.prototype.clear = function clear() { this._listeners = {}; }; Emitter.prototype.emit = function emit(eventName, message, msg2, msg3) { if (!this._listeners[eventName]) return; this._listeners[eventName].forEach(function (listener) { listener(message, msg2, msg3); }); }; data/dialog.js0000644000000000000000000002017612741721616012277 0ustar rootroot/* 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/. */ /* Dialog / Popup ===================================== */ // dialog names (used as dialog identifiers) const dialogNames = { "resetData": "resetData", "blockSites": "blockSites", "hideSites": "hideSites", "startUploadData": "startUploadData", "stopUploadData": "stopUploadData", "privateBrowsing": "privateBrowsing", "trackingProtection": "trackingProtection", "saveOldData": "saveOldData" }; // options: name, title, message, type, dnsPrompt(Do Not Show), imageUrl function dialog(options, callback) { createDialog(options, callback); } function createDialog(options, callback) { var modal = picoModal({ content: createDialogContent(options), closeButton: false, overlayClose: false, overlayStyles: { backgroundColor: "#000", opacity: 0.75 } }); addDialogEventHandlers(modal, options, function (userResponse) { callback(userResponse); }); } function createDialogContent(options) { return dialogTitleBar(options) + dialogMessage(options) + dialogControls(options); } function dialogTitleBar(options) { return "
" + (options.title || " ") + "
"; } function dialogMessage(options) { return "
" + (options.imageUrl ? "
" : "") + "
" + (options.message || " ") + "
" + "
"; } function dialogControls(options) { var doNotShowAgainPrompt = "
Do not show this again.
"; // control buttons var controlButtons = "
"; var okButton = "OK"; var cancelButton = "Cancel"; // check dialog type // alert dialog only needs a single button - "OK" // else we show both "OK" and "Cancel" buttons if (options.type == "alert") { controlButtons += "OK"; } else { if (navigator.appVersion.indexOf("Win") > -1) { // runs on Windows controlButtons += okButton + cancelButton; } else { // runs on OS other than Windows controlButtons += cancelButton + okButton; } } controlButtons += "
"; return "
" + (options.dnsPrompt ? doNotShowAgainPrompt : "") + controlButtons + "
"; } function addDialogEventHandlers(modal, options, callback) { var dialogContainer = modal.modalElem(); // OK button click event handler var okButton = dialogContainer.querySelector(".pico-close.dialog-ok"); okButton.addEventListener("click", function () { if (dialogContainer.querySelector(".dialog-dns input") && dialogContainer.querySelector(".dialog-dns input").checked) { // Do Not Show } modal.close(); callback(true); }); // Cancel button click event handler var cancelButton = dialogContainer.querySelector(".pico-close.dialog-cancel"); if (cancelButton) { cancelButton.addEventListener("click", function () { modal.close(); callback(false); }); } var keyDownHandler = function (event) { // disable Tab if (event.keyCode == "9") { event.preventDefault(); } // press Esc to close the dialog (functions the same as clicking Cancel) if (event.keyCode == "27") { // Esc key pressed modal.close(); callback(false); } }; document.addEventListener("keydown", keyDownHandler); modal.afterClose(function () { document.removeEventListener("keydown", keyDownHandler); }); // for Upload Data dialog if (dialogContainer.querySelector(".toggle-pp")) { dialogContainer.querySelector(".toggle-pp").addEventListener("click", function (event) { dialogContainer.querySelector(".pico-content .privacy-policy").classList.toggle("collapsed"); }); } restrictTabWithinDialog(modal); } function restrictTabWithinDialog(modal) { var dialogContainer = modal.modalElem(); assignTabIndices(modal); dialogContainer.addEventListener("keypress", function (event) { event.stopPropagation(); var focusedElm = document.activeElement; // Tab key is pressed if (event.keyCode == "9") { var currentTabIndex = parseInt(focusedElm.getAttribute("tabIndex")); var nextElem = dialogContainer.querySelector("[tabIndex='" + (currentTabIndex + 1) + "']"); if (nextElem) { nextElem.focus(); } else { dialogContainer.querySelector("[tabIndex='0']").focus(); } } // when the focused element is the OK or Cancel button and Enter key is pressed // mimic mouse clicking on button if (event.keyCode == "13" && focusedElm.mozMatchesSelector(".pico-content .dialog-btns a")) { focusedElm.click(); } }); } function assignTabIndices(modal) { var dialogContainer = modal.modalElem(); var allElemsInDialog = dialogContainer.querySelectorAll("*"); var tabIndex = 0; toArray(allElemsInDialog).forEach(function (elem, i) { if (elem.nodeName.toLowerCase() == "a" || elem.nodeName.toLowerCase() == "input") { elem.setAttribute("tabIndex", tabIndex); tabIndex++; } }); dialogContainer.querySelector("[tabIndex='0']").focus(); } function informUserOfUnsafeWindowsDialog() { dialog({ "type": "alert", "name": dialogNames.privateBrowsing, "dnsPrompt": true, "title": "Private Browsing", "message": "

You have one or more private browsing windows open.

" + "

Connections made in private browsing windows will be visualized in Lightbeam but that data is not stored.

" + "

Information gathered in private browsing mode will be deleted whenever Lightbeam is restarted, and is not collected at all when Lightbeam is not open..

", "imageUrl": "image/lightbeam_popup_privacy.png" }, function (confirmed) {} ); } function confirmBlockSitesDialog(callback) { dialog({ "name": dialogNames.blockSites, "title": "Block Sites", "message": "

Warning:

" + "

Blocking sites will prevent any and all content from being loaded from selected domains, for example: [example.com, example.net] and all of their subdomains [mail.example.com, news.example.net etc.].

" + "

This can prevent some sites from working and degrade your internet experience. Please use this feature carefully.

", "imageUrl": "image/lightbeam_popup_blocked.png" }, callback ); } function confirmHideSitesDialog(callback) { dialog({ "name": dialogNames.hideSites, "dnsPrompt": true, "title": "Hide Sites", "message": "

These sites will not be shown in Lightbeam visualizations, including List View, unless you specifically toggle them back on with the Show Hidden Sites button.

" + "

You can use this to ignore trusted sites from the data.

", "imageUrl": "image/lightbeam_popup_hidden.png" }, callback ); } function confirmResetDataDialog(callback) { dialog({ "name": dialogNames.resetData, "title": "Reset Data", "message": "

Pressing OK will delete all Lightbeam information including connection history, user preferences, block sites list etc.

" + "

Your browser will be returned to the state of a fresh install of Lightbeam.

", "imageUrl": "image/lightbeam_popup_warningreset.png" }, callback); } function confirmTrackingProtectionDialog(callback) { dialog({ "name": dialogNames.trackingProtection, "title": "Tracking Protection", "message": "

Warning:

" + "

Enabling this experimental feature will block elements that track your online behavior.

" + "

This can prevent some sites from working and degrade your internet experience. Please use this feature carefully.

" + "

Please report any problems you find.

" + '

Learn more...

', "imageUrl": "image/lightbeam_popup_blocked.png" }, callback); } data/content-script.js0000644000000000000000000000327512741721616014015 0ustar rootroot/* 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/. */ (function (global) { // This is the e10s/message passing content script that ties the workers to the // addon. It can see most of the addon, the window is either not visible or not // mutable so we use unsafeWindow below. This handles the post message // connections and does a little UI work on the side. self.port.on('connection', function (connection) { global.allConnections.push(connection); global.aggregate.emit('connection', connection); }); self.port.on('passStoredConnections', function (connections) { global.allConnections = connections; global.aggregate.emit('load', global.allConnections); }); self.port.on('update-blocklist', function (domain) { global.aggregate.emit('update-blocklist', domain); }); self.port.on('update-blocklist-all', function (domains) { global.aggregate.emit('update-blocklist-all', domains); }); self.port.on('init', function () { console.debug('content-script::init()'); global.aggregate.emit('load', global.allConnections); }); self.port.on("updateUIFromMetadata", function (metadata) { console.debug("Got add-on metadata", metadata); global.aggregate.emit("updateUIFromMetadata", metadata); }); self.port.on("updateUIFromBrowserPrefs", function (browserPrefs) { console.debug("Got set browser prefs", browserPrefs); global.aggregate.emit("updateUIFromBrowserPrefs", browserPrefs); }); self.port.on("updateUIFromPrefs", function (prefs) { console.debug("Got set prefs", prefs); global.aggregate.emit("updateUIFromPrefs", prefs); }); })(this); data/aggregate.js0000644000000000000000000004055312741721616012767 0ustar rootroot/* 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/. */ // Graph Visualization // Visualization of tracking data interconnections (function (global) { "use strict"; // An emitter that lists nodes and edges so we can build the data structure // used by all 3 visualizers. var aggregate = new Emitter(); global.aggregate = aggregate; global.filteredAggregate = { nodes: [], edges: [] }; // The name of the current filter (daily, weekly, recent, last10sites) aggregate.currentFilter = "daily"; aggregate.trackerCount = 0; aggregate.siteCount = 0; // d3 has functionality to build graphs out of lists of nodes and edges. aggregate.nodes = []; aggregate.edges = []; aggregate.recentSites = []; aggregate.initialized = false; aggregate.nodemap = {}; aggregate.edgemap = {}; function resetData() { console.debug('aggregate::resetData'); aggregate.getBlockedDomains().forEach(function (domain) { console.debug("deleting", domain); delete userSettings[domain]; }); aggregate.nodemap = {}; aggregate.edgemap = {}; aggregate.nodes = []; aggregate.edges = []; aggregate.trackerCount = 0; aggregate.siteCount = 0; aggregate.recentSites = []; global.currentVisualization.emit('reset'); updateStatsBar(); } aggregate.on('reset', resetData); aggregate.getBlockedDomains = function () { return Object.keys(userSettings).filter(function (domain) { return userSettings[domain] == 'block'; }); }; aggregate.getAllNodes = function () { var blockedDomains = aggregate.getBlockedDomains(); console.debug("getAllNodes", JSON.stringify(blockedDomains)); return aggregate.nodes.concat(blockedDomains.map(function (domain) { return { site: domain, nodeType: 'blocked', name: domain }; })); }; aggregate.getConnectionCount = function (node) { if (node.nodeType === 'blocked') return 0; let connections = Object.keys(aggregate.nodeForKey(node.name)).length; return connections - 1 > 0 ? connections - 1 : 0; }; aggregate.nodeForKey = function (key) { var result = {}; var linkedNodes = []; if (aggregate.nodemap[key]) { linkedNodes = aggregate.nodemap[key].linkedFrom.concat(aggregate.nodemap[key].linkedTo); result[key] = aggregate.nodemap[key]; } else { linkedNodes = []; result[key] = {}; } linkedNodes.forEach(function (nodeName) { var node = aggregate.nodemap[nodeName]; var temp = {}; for (var p in node) { if (node.hasOwnProperty(p) && !(p == "linkedFrom" || p == "linkedTo")) { temp[p] = node[p]; } } result[nodeName] = temp; }); return result; }; aggregate.connectionAsObject = function (conn) { if (Array.isArray(conn)) { return { source: conn[SOURCE], target: conn[TARGET], timestamp: new Date(conn[TIMESTAMP]), contentType: conn[CONTENT_TYPE], cookie: conn[COOKIE], sourceVisited: conn[SOURCE_VISITED], secure: conn[SECURE], sourcePathDepth: conn[SOURCE_PATH_DEPTH], sourceQueryDepth: conn[SOURCE_QUERY_DEPTH], sourceSub: conn[SOURCE_SUB], targetSub: conn[TARGET_SUB], method: conn[METHOD], status: conn[STATUS], cacheable: conn[CACHEABLE] }; } return conn; }; // Pass the list of connections to build the graph structure to pass to d3 for // visualizations. function onLoad(connections) { var startTime = Date.now(); console.debug("aggregate::onLoad", connections.length, "connections", aggregate.currentFilter); connections.forEach(onConnection); aggregate.initialized = true; filteredAggregate = aggregate.filters[aggregate.currentFilter](); // Tell the visualization that we're ready. if (global.currentVisualization) { global.currentVisualization.emit('init'); } updateStatsBar(); console.debug('aggregate::onLoad end, took %s ms', Date.now() - startTime); } function updateUIFromMetadata(metadata) { console.debug("in aggregate metadata"); global.updateUIFromMetadata(metadata); } function updateUIFromBrowserPrefs(browserPrefs) { console.debug("in aggregate browser prefs"); global.updateUIFromBrowserPrefs(browserPrefs); } function updateUIFromPrefs(prefs) { console.debug("in aggregate prefs"); global.updateUIFromPrefs(prefs); } aggregate.on('load', onLoad); aggregate.on("updateUIFromMetadata", updateUIFromMetadata); aggregate.on("updateUIFromBrowserPrefs", updateUIFromBrowserPrefs); aggregate.on("updateUIFromPrefs", updateUIFromPrefs); // Constants for indexes of properties in array format const SOURCE = 0; const TARGET = 1; const TIMESTAMP = 2; const CONTENT_TYPE = 3; const COOKIE = 4; const SOURCE_VISITED = 5; const SECURE = 6; const SOURCE_PATH_DEPTH = 7; const SOURCE_QUERY_DEPTH = 8; const SOURCE_SUB = 9; const TARGET_SUB = 10; const METHOD = 11; const STATUS = 12; const CACHEABLE = 13; // Check that recent sites include the domain. This is another potential source // of false positives. aggregate.isDomainVisited = function isDomainVisited(domain) { return aggregate.recentSites.length && (aggregate.recentSites.indexOf(domain) > -1); }; function onConnection(conn) { // A connection has the following keys: // source (url), target (url), timestamp (int), contentType (str), cookie (bool), sourceVisited (bool), secure(bool), sourcePathDepth (int), sourceQueryDepth(int) // We want to shape the collection of connections that represent points in time into // aggregate data for graphing. Each connection has two endpoints represented by GraphNode objects // and one edge represented by a GraphEdge object, but we want to re-use these when connections // map to the same endpoints or edges. var connection = aggregate.connectionAsObject(conn); var sourcenode, targetnode, edge, nodelist, updated = false; // Maintain the list of sites visited in dated order // console.log('check for recent sites: %s: %s', connection.source, connection.sourceVisited); if (connection.sourceVisited) { // console.log('source visited: %s -> %s', connection.source, connection.target); var site = connection.target; var siteIdx = aggregate.recentSites.indexOf(site); if (aggregate.recentSites.length && siteIdx === (aggregate.recentSites.length - 1)) { // most recent site is already at the end of the recentSites list, do nothing } else { if (siteIdx > -1) { // if site is already in list (but not last), remove it aggregate.recentSites.splice(siteIdx, 1); } aggregate.recentSites.push(site); // push site on end of list if it is not there updated = true; } } else { // console.log('source not visited: %s -> %s', connection.source, connection.target); } // Retrieve the source node and update, or create it if not found if (aggregate.nodemap[connection.source]) { sourcenode = aggregate.nodemap[connection.source]; if (connection.sourceVisited && sourcenode.nodeType == "thirdparty") { // the previously "thirdparty" site has now become a "visited" site // +1 on visited sites counter and -1 on trackers counter aggregate.siteCount++; aggregate.trackerCount--; } sourcenode.update(connection, true); } else { sourcenode = new GraphNode(connection, true); aggregate.nodemap[connection.source] = sourcenode; aggregate.nodes.push(sourcenode); if (connection.sourceVisited) { aggregate.siteCount++; } else { aggregate.trackerCount++; } // console.log('new source: %s, now %s nodes', sourcenode.name, aggregate.nodes.length); updated = true; } // Retrieve the target node and update, or create it if not found if (aggregate.nodemap[connection.target]) { targetnode = aggregate.nodemap[connection.target]; targetnode.update(connection, false); } else { targetnode = new GraphNode(connection, false); aggregate.nodemap[connection.target] = targetnode; aggregate.nodes.push(targetnode); // all nodes if (connection.sourceVisited) { aggregate.siteCount++; // Can this ever be true? } else { aggregate.trackerCount++; } // console.log('new target: %s, now %s nodes', targetnode.name, aggregate.nodes.length); updated = true; } // Create edge objects. Could probably do this lazily just for the graph view if (aggregate.edgemap[connection.source + '->' + connection.target]) { edge = aggregate.edgemap[connection.source + '->' + connection.target]; edge.update(connection); } else { edge = new GraphEdge(sourcenode, targetnode, connection); aggregate.edgemap[edge.name] = edge; aggregate.edges.push(edge); // updated = true; } if (updated) { aggregate.update(); } updateStatsBar(); } aggregate.on('connection', onConnection); function onBlocklistUpdate({ domain, flag }) { if (flag === true) { userSettings[domain] = 'block'; } else if (userSettings[domain] == 'block') { delete userSettings[domain]; } } aggregate.on('update-blocklist', onBlocklistUpdate); // Read the blocklist to memory function onBlocklistUpdateAll(domains) { (domains || []).forEach(onBlocklistUpdate); } aggregate.on('update-blocklist-all', onBlocklistUpdateAll); // Used only by the graph view. function GraphEdge(source, target, connection) { var name = source.name + '->' + target.name; if (aggregate.edgemap[name]) { return aggregate.edgemap[name]; } this.source = source; this.target = target; this.name = name; if (connection) { this.cookieCount = connection.cookie ? 1 : 0; } return this; // console.log('edge: %s', this.name); } GraphEdge.prototype.lastAccess = function () { return (this.source.lastAccess > this.target.lastAccess) ? this.source.lastAccess : this.target.lastAccess; }; GraphEdge.prototype.firstAccess = function () { return (this.source.firstAccess < this.target.firstAccess) ? this.source.firstAccess : this.target.firstAccess; }; GraphEdge.prototype.update = function (connection) { this.cookieCount = connection.cookie ? this.cookieCount + 1 : this.cookieCount; }; // A graph node represents one end of a connection, either a target or a source // Where a connection is a point in time with a timestamp, a graph node has a time range // represented by firstAccess and lastAccess. Where a connection has a contentType, a node // has an array of content types. Booleans in graph nodes become boolean pairs in graph nodes // (for instance, some connections may have cookies and some may not, which would result in both // cookie and notCookie being true). We set an initial position randomly to keep the force graph // from exploding. // function GraphNode(connection, isSource) { this.firstAccess = this.lastAccess = connection.timestamp; this.linkedFrom = []; this.linkedTo = []; this.contentTypes = []; this.subdomain = []; this.method = []; this.status = []; this.visitedCount = 0; this.secureCount = 0; this.cookieCount = 0; this.howMany = 0; if (connection) { this.update(connection, isSource); } // FIXME: Get the width and height from the add-on somehow var width = 1000; var height = 1000; // Set defaults for graph this.x = this.px = (Math.random() - 0.5) * 800 + width / 2; this.y = this.py = (Math.random() - 0.5) * 800 + height / 2; this.weight = 0; } GraphNode.prototype.update = function (connection, isSource) { if (!this.name) { this.name = isSource ? connection.source : connection.target; // console.log('node: %s', this.name); } if (connection.timestamp > this.lastAccess) { this.lastAccess = connection.timestamp; } if (connection.timestamp < this.firstAccess) { this.firstAccess = connection.timestamp; } if (isSource && (this.linkedTo.indexOf(connection.target) < 0)) { this.linkedTo.push(connection.target); } if ((!isSource) && (this.linkedFrom.indexOf(connection.source) < 0)) { this.linkedFrom.push(connection.source); } if (this.contentTypes.indexOf(connection.contentType) < 0) { this.contentTypes.push(connection.contentType); } if (connection.sourceVisited) { this.visitedCount++; } if (this.subdomain.indexOf(connection.sourceSub) < 0) { this.subdomain.push(connection.sourceSub); } if (connection.cookie) { this.cookieCount++; } if (connection.secure) { this.secureCount++; } if (this.method.indexOf(connection.method) < 0) { this.method.push(connection.method); } if (this.status.indexOf(connection.status) < 0) { this.status.push(connection.status); } this.howMany++; if (this.visitedCount / this.howMany == 1) { this.nodeType = 'site'; } else if (this.visitedCount / this.howMany === 0) { this.nodeType = 'thirdparty'; } else { this.nodeType = 'both'; } return this; }; // Filtering function sitesSortedByDate() { return aggregate.recentSites.map(function (sitename) { return aggregate.nodemap[sitename]; }); } aggregate.sitesSortedByDate = sitesSortedByDate; function aggregateFromNodes(nodes) { var localmap = {}; var edgemap = {}; nodes.forEach(function (node) { localmap[node.name] = node; node.linkedFrom.forEach(function (nodename) { var linkedNode = aggregate.nodemap[nodename]; var edge = new GraphEdge(node, linkedNode); edgemap[edge.name] = edge; localmap[nodename] = linkedNode; }); node.linkedTo.forEach(function (nodename) { var linkedNode = aggregate.nodemap[nodename]; var edge = new GraphEdge(node, linkedNode); edgemap[edge.name] = edge; localmap[nodename] = linkedNode; }); }); return { nodes: Object.keys(localmap).map(function (name) { return localmap[name]; }), edges: Object.keys(edgemap).map(function (name) { return edgemap[name]; }) }; } // filters aggregate.filters = { daily: function daily() { var now = Date.now(); var then = now - (24 * 60 * 60 * 1000); var sortedNodes = sitesSortedByDate(); // find index where we go beyond date var i; for (i = sortedNodes.length - 1; i > -1; i--) { if (sortedNodes[i].lastAccess.valueOf() < then) { break; } } // i is always 1 too low at the point i++; // put it back var filteredNodes = sortedNodes.slice(i); // Done filtering return aggregateFromNodes(filteredNodes); }, weekly: function weekly() { var now = Date.now(); var then = now - (7 * 24 * 60 * 60 * 1000); var sortedNodes = sitesSortedByDate(); // find index where we go beyond date var i; for (i = sortedNodes.length - 1; i > -1; i--) { if (sortedNodes[i].lastAccess < then) { break; } } i++; // we decrement too far, put it back var filteredNodes = sortedNodes.slice(i); // console.log('weekly filter after: %s', filteredNodes.length); return aggregateFromNodes(filteredNodes); }, last10sites: function last10sites() { var sortedNodes = sitesSortedByDate(); var filteredNodes = sortedNodes.slice(-10); return aggregateFromNodes(filteredNodes); }, recent: function recent() { var sortedNodes = sitesSortedByDate(); var filteredNodes = sortedNodes.slice(-1); return aggregateFromNodes(filteredNodes); } }; function switchFilter(name) { if (aggregate.currentFilter == name) { return; } aggregate.currentFilter = name; global.self.port.emit("prefChanged", { defaultFilter: name }); aggregate.update(); } aggregate.switchFilter = switchFilter; // Underscore debounce function // // Returns a function, that, as long as it continues to be invoked, will not // be triggered. The function will be called after it stops being called for // N milliseconds. If `immediate` is passed, trigger the function on the // leading edge, instead of the trailing. var debounce = function debounce(func, wait, immediate) { var timeout; return function () { var context = this, args = arguments; var later = function () { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; }; aggregate.update = debounce(function update() { // FIXME: maybe not for list view if (global.currentVisualization && global.currentVisualization.name !== 'graph') { console.debug('do not update aggregate for view', currentVisualization.name); } if (aggregate.initialized) { global.filteredAggregate = aggregate.filters[aggregate.currentFilter](); aggregate.emit('update'); } updateStatsBar(); }); })(this); data/OpenSans.css0000644000000000000000000000117412741721616012737 0ustar rootroot/* 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/. */ @font-face { font-family: 'Open Sans'; font-style: italic; font-weight: 400; src: url('font/OpenSans-LightItalic.ttf') format('truetype') } @font-face { font-family: 'Open Sans'; font-style: bold; font-weight: 700; src: url('font/OpenSans-Bold.ttf') format('truetype') } @font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 400; src: url('font/OpenSans-Light.ttf') format('truetype') } data/picoModal/0000755000000000000000000000000012741721616012403 5ustar rootrootdata/picoModal/picoModal.js0000644000000000000000000003044312741721616014654 0ustar rootroot/** * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /** * A self-contained modal library */ (function(window, document) { "use strict"; /** Returns whether a value is a dom node */ function isNode(value) { if ( typeof Node === "object" ) { return value instanceof Node; } else { return value && typeof value === "object" && typeof value.nodeType === "number"; } } /** Returns whether a value is a string */ function isString(value) { return typeof value === "string"; } /** * Generates observable objects that can be watched and triggered */ function observable() { var callbacks = []; return { watch: callbacks.push.bind(callbacks), trigger: function( modal ) { var unprevented = true; var event = { preventDefault: function preventDefault () { unprevented = false; } }; for (var i = 0; i < callbacks.length; i++) { callbacks[i](modal, event); } return unprevented; } }; } /** * A small interface for creating and managing a dom element */ function Elem( elem ) { this.elem = elem; } /** * Creates a new div */ Elem.div = function ( parent ) { var elem = document.createElement('div'); (parent || document.body).appendChild(elem); return new Elem(elem); }; Elem.prototype = { /** Creates a child of this node */ child: function () { return Elem.div(this.elem); }, /** Applies a set of styles to an element */ stylize: function(styles) { styles = styles || {}; if ( typeof styles.opacity !== "undefined" ) { styles.filter = "alpha(opacity=" + (styles.opacity * 100) + ")"; } for (var prop in styles) { if (styles.hasOwnProperty(prop)) { this.elem.style[prop] = styles[prop]; } } return this; }, /** Adds a class name */ clazz: function (clazz) { this.elem.className += " " + clazz; return this; }, /** Sets the HTML */ html: function (content) { if ( isNode(content) ) { this.elem.appendChild( content ); } else { this.elem.innerHTML = content; } return this; }, /** Returns the width of this element */ getWidth: function () { return this.elem.clientWidth; }, /** Adds a click handler to this element */ onClick: function(callback) { if (this.elem.attachEvent) { this.elem.attachEvent('onclick', callback); } else { this.elem.addEventListener('click', callback); } return this; }, /** Removes this element from the DOM */ destroy: function() { document.body.removeChild(this.elem); }, /** Hides this element */ hide: function() { this.elem.style.display = "none"; }, /** Shows this element */ show: function() { this.elem.style.display = "block"; }, /** Executes a callback on all the ancestors of an element */ anyAncestor: function ( predicate ) { var elem = this.elem; while ( elem ) { if ( predicate( new Elem(elem) ) ) { return true; } else { elem = elem.parentNode; } } return false; } }; /** Generates the grey-out effect */ function buildOverlay( getOption, close ) { return Elem.div() .clazz("pico-overlay") .clazz( getOption("overlayClass", "") ) .stylize({ display: "block", position: "fixed", top: "0px", left: "0px", height: "100%", width: "100%", zIndex: 10000 }) .stylize(getOption('overlayStyles', { opacity: 0.5, background: "#000" })) .onClick(function () { if ( getOption('overlayClose', true) ) { close(); } }); } /** Builds the content of a modal */ function buildModal( getOption, close ) { var elem = Elem.div() .clazz("pico-content") .clazz( getOption("modalClass", "") ) .stylize({ display: 'block', position: 'fixed', zIndex: 10001, left: "50%", top: "50px" }) .html( getOption('content') ) .onClick(function (event) { var isCloseClick = new Elem(event.target) .anyAncestor(function (elem) { return /\bpico-close\b/.test(elem.elem.className); }); if ( isCloseClick ) { close(); } }); var width = getOption('width', elem.getWidth()); elem .stylize({ width: width + "px", margin: "0 0 0 " + (-(width / 2) + "px") }) .stylize( getOption('modalStyles', { backgroundColor: "white", padding: "20px", borderRadius: "5px" }) ); return elem; } /** Builds the close button */ function buildClose ( elem, getOption ) { if ( getOption('closeButton', true) ) { return elem.child() .html( getOption('closeHtml', "×") ) .clazz("pico-close") .clazz( getOption("closeClass") ) .stylize( getOption('closeStyles', { borderRadius: "2px", cursor: "pointer", height: "15px", width: "15px", position: "absolute", top: "5px", right: "5px", fontSize: "16px", textAlign: "center", lineHeight: "15px", background: "#CCC" }) ); } } /** Builds a method that calls a method and returns an element */ function buildElemAccessor( builder ) { return function () { return builder().elem; }; } /** * Displays a modal */ function picoModal(options) { if ( isString(options) || isNode(options) ) { options = { content: options }; } var afterCreateEvent = observable(); var beforeShowEvent = observable(); var afterShowEvent = observable(); var beforeCloseEvent = observable(); var afterCloseEvent = observable(); /** * Returns a named option if it has been explicitly defined. Otherwise, * it returns the given default value */ function getOption ( opt, defaultValue ) { var value = options[opt]; if ( typeof value === "function" ) { value = value( defaultValue ); } return value === undefined ? defaultValue : value; } /** Hides this modal */ function forceClose () { shadowElem().hide(); modalElem().hide(); afterCloseEvent.trigger(iface); } /** Gracefully hides this modal */ function close () { if ( beforeCloseEvent.trigger(iface) ) { forceClose(); } } /** Wraps a method so it returns the modal interface */ function returnIface ( callback ) { return function () { callback.apply(this, arguments); return iface; }; } // The constructed dom nodes var built; /** Builds a method that calls a method and returns an element */ function build ( name ) { if ( !built ) { var modal = buildModal(getOption, close); built = { modal: modal, overlay: buildOverlay(getOption, close), close: buildClose(modal, getOption) }; afterCreateEvent.trigger(iface); } return built[name]; } var modalElem = build.bind(window, 'modal'); var shadowElem = build.bind(window, 'overlay'); var closeElem = build.bind(window, 'close'); var iface = { /** Returns the wrapping modal element */ modalElem: buildElemAccessor(modalElem), /** Returns the close button element */ closeElem: buildElemAccessor(closeElem), /** Returns the overlay element */ overlayElem: buildElemAccessor(shadowElem), /** Shows this modal */ show: function () { if ( beforeShowEvent.trigger(iface) ) { shadowElem().show(); closeElem(); modalElem().show(); afterShowEvent.trigger(iface); } return this; }, /** Hides this modal */ close: returnIface(close), /** * Force closes this modal. This will not call beforeClose * events and will just immediately hide the modal */ forceClose: returnIface(forceClose), /** Destroys this modal */ destroy: function () { modalElem = modalElem().destroy(); shadowElem = shadowElem().destroy(); closeElem = undefined; }, /** * Updates the options for this modal. This will only let you * change options that are re-evaluted regularly, such as * `overlayClose`. */ options: function ( opts ) { options = opts; }, /** Executes after the DOM nodes are created */ afterCreate: returnIface(afterCreateEvent.watch), /** Executes a callback before this modal is closed */ beforeShow: returnIface(beforeShowEvent.watch), /** Executes a callback after this modal is shown */ afterShow: returnIface(afterShowEvent.watch), /** Executes a callback before this modal is closed */ beforeClose: returnIface(beforeCloseEvent.watch), /** Executes a callback after this modal is closed */ afterClose: returnIface(afterCloseEvent.watch) }; return iface; } if ( typeof window.define === "function" && window.define.amd ) { window.define(function () { return picoModal; }); } else { window.picoModal = picoModal; } }(window, document)); data/picoModal/LICENSE.md0000644000000000000000000000211612741721616014007 0ustar rootrootThe MIT License (MIT) --------------------- Copyright (c) 2012 James Frasca Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. data/font/0000755000000000000000000000000012751274675011453 5ustar rootrootdata/d3/0000755000000000000000000000000012751274675011013 5ustar rootrootdata/upgrade.html0000644000000000000000000000376712741721616013026 0ustar rootroot Welcome to Lightbeam!

Thanks for upgrading Lightbeam!

  1. Major changes this release include an updated data format, refactoring the content scripts to omit references to unsafeWindow, and refactoring storage. Due to storage changes, previously stored connections will be wiped out with this upgrade. We expect users' connections graph to be restored to their normal state in a few days, since quota limits restrict the amount of data stored anyway. For a complete list of changes, please visit our github repository.

  2. First-time contributors for this release include Francois Marier, Spyros Livathinos, and Andrew William-Smith. We are very fortunate to have such wonderful contributors in our community.

    • Learn more about Mozilla's Lightbeam for Firefox on our project page
    • Found a bug? Report an issue on Github
    • Don't forget to leave a review on our Mozilla Addons page!
data/ui.js0000644000000000000000000003564012741721616011457 0ustar rootroot/* 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/. */ (function (global) { // Bunch of utilities related to UI elements. const graphNodeRadius = { "graph": 12 }; var g = global; global.graphNodeRadius = graphNodeRadius; /* Convert a NodeList to Array */ global.toArray = function toArray(nl) { return Array.prototype.slice.call(nl, 0); }; /************************************************** * For accessibility: * if the current focused element is an anchor, press Enter will mimic mouse click on that element */ document.addEventListener("keypress", function (event) { var focusedElm = document.activeElement; if (event.keyCode == "13" && focusedElm.mozMatchesSelector("a") && !focusedElm.getAttribute("href")) { focusedElm.click(); } }); /* Lightbeam Logo Click handler ====================== */ document.querySelector(".main header").addEventListener("click", function () { location.reload(); }); /************************************************** * Buttons */ function dropdownGroup(btnGroup, callback) { callback = callback || function () {}; var allOptions = btnGroup.querySelectorAll(".dropdown_options a"); toArray(allOptions).forEach(function (option) { option.addEventListener("click", function (e) { btnGroup.querySelector("[data-selected]").removeAttribute("data-selected"); e.target.setAttribute("data-selected", true); callback(e.target.getAttribute("data-value")); }); }); } /* Bind click event listener to each of the btn_group memebers */ var btnGroupArray = toArray(document.querySelectorAll(".btn_group")); btnGroupArray.forEach(function (btnGroup) { dropdownGroup(btnGroup, function (val) { val = val.toLowerCase(); switch (val) { case 'graph': case 'list': switchVisualization(val); break; case 'recent': case 'last10sites': case 'daily': case 'weekly': aggregate.switchFilter(val); document.querySelector(".filter-display header").textContent = btnGroup.querySelector("[data-selected]").textContent; break; default: console.log("selected val=" + val); } }); }); document.querySelector(".toggle-btn.tracking-btn").addEventListener("click", function (event) { var elmClicked = event.target; if (elmClicked.mozMatchesSelector("input")) { var toggleBtn = document.querySelector(".tracking-btn"); if (elmClicked.checked) { confirmTrackingProtectionDialog(function (confirmed) { if (confirmed) { elmClicked.checked = true; toggleBtnOnEffect(toggleBtn); global.self.port.emit("browserPrefChanged", { "trackingProtection": true }); } else { elmClicked.checked = false; } }); } else { elmClicked.checked = false; toggleBtnOffEffect(toggleBtn); global.self.port.emit("browserPrefChanged", { "trackingProtection": false }); } } }); function toggleBtnOnEffect(toggleBtn) { toggleBtn.querySelector(".toggle-btn-innner").classList.add("checked"); toggleBtn.querySelector(".switch").classList.add("checked"); toggleBtn.querySelector(".on-off-text").classList.add("checked"); toggleBtn.querySelector(".on-off-text").textContent = "ON"; } function toggleBtnOffEffect(toggleBtn) { toggleBtn.querySelector(".toggle-btn-innner").classList.remove("checked"); toggleBtn.querySelector(".switch").classList.remove("checked"); toggleBtn.querySelector(".on-off-text").classList.remove("checked"); toggleBtn.querySelector(".on-off-text").textContent = "OFF"; } function downloadAsJson(data, defaultFilename) { var file = new Blob([data], { type: 'application/json' }); var reader = new FileReader(); var a = document.createElement('a'); reader.onloadend = function () { a.href = reader.result; a.download = defaultFilename; a.target = '_blank'; document.body.appendChild(a); a.click(); }; reader.readAsDataURL(file); } document.querySelector(".download").addEventListener('click', function (evt) { // console.log('received export data'); downloadAsJson([JSON.stringify(allConnections)], 'lightbeamData.json'); evt.preventDefault(); // window.open('data:application/json,' + exportFormat(allConnections)); }); document.querySelector('.reset-data').addEventListener('click', function () { confirmResetDataDialog(function (confirmed) { if (confirmed) { // currentVisualization.emit('remove'); allConnections = []; global.self.port.emit('reset'); aggregate.emit('reset'); // WARNING: this is a race condition. Since the event emitters are // async, we were running into the situation where the page was reloaded // before aggregate::resetData was finished, resulting in #506 // // TODO: using a short timeout for now, would be better to use a Promise setTimeout(500, function () { location.reload(); /* reload page */ }); } }); }); global.getZoom = function getZoom(canvas) { try { var box = canvas.getAttribute('viewBox') .split(/\s/) .map(function (i) { return parseInt(i, 10); }); return { x: box[0], y: box[1], w: box[2], h: box[3] }; } catch (e) { console.log('error in getZoom, called with %o instead of an element'); console.log('Caller: %o', caller); return null; } }; global.setZoom = function setZoom(box, canvas) { // TODO: code cleanup if both cases use basically the same code canvas.setAttribute('viewBox', [box.x, box.y, box.w, box.h].join(' ')); }; /* Scroll over visualization to zoom in/out ========================= */ /* define viewBox limits * graph view default viewBox = " 0 0 750 750 " * map = " 0 0 2711.3 1196.7 " */ const graphZoomInLimit = { w: 250, h: 250 }; const graphZoomOutLimit = { w: 4000, h: 4000 }; const mapZoomInLimit = { w: (2711.3 / 5), h: (1196.7 / 5) }; const mapZoomOutLimit = { w: 2711.3, h: 1196.7 }; const svgZoomingRatio = 1.1; document.querySelector(".stage").addEventListener("wheel", function (event) { if (event.target.mozMatchesSelector(".vizcanvas, .vizcanvas *") && g.currentVisualization.name != "list") { if (g.currentVisualization.name == "graph") { zoomWithinLimit(event.deltaY, vizcanvas, graphZoomInLimit, graphZoomOutLimit); } } }, false); // check to see if the viewBox of the targeting svg is within the limit we define function checkWithinZoomLimit(targetSvg, zoomType, zoomLimit) { var currentViewBox = getZoom(targetSvg); if (zoomType == "in") { var withinZoomInLimit = (currentViewBox.w > zoomLimit.w && currentViewBox.h > zoomLimit.h); if (zoomLimit.x && zoomLimit.y) { withinZoomInLimit = withinZoomInLimit && (currentViewBox.x < zoomLimit.x && currentViewBox.y < zoomLimit.y); } return withinZoomInLimit; } else { var withinZoomOutLimit = (currentViewBox.w <= zoomLimit.w && currentViewBox.h <= zoomLimit.h); return withinZoomOutLimit; } } // Check to see if the viewBox of the targeting svg is within the limit we define // if yes, zoom function zoomWithinLimit(scrollDist, targetSvg, zoomInLimit, zoomOutLimit) { var i; if (scrollDist >= 1) { // scroll up to zoom out for (i = 1; i <= scrollDist; i++) { if (checkWithinZoomLimit(targetSvg, "out", zoomOutLimit)) { svgZooming(targetSvg, (1 / svgZoomingRatio)); } } } if (scrollDist <= -1) { // scroll down to zoom in for (i = scrollDist; i <= -1; i++) { if (checkWithinZoomLimit(targetSvg, "in", zoomInLimit)) { svgZooming(targetSvg, svgZoomingRatio); } } } } // Apply zoom level function svgZooming(target, ratio) { var box = getZoom(target); var newViewBox = generateNewViewBox(target, box, ratio); setZoom(newViewBox, target); } function generateNewViewBox(target, box, ratio) { var oldWidth = box.w; var newWidth = oldWidth / ratio; var offsetX = (newWidth - oldWidth) / 2; var oldHeight = box.h; var newHeight = oldHeight / ratio; var offsetY = (newHeight - oldHeight) / 2; box.w = box.w / ratio; box.h = box.h / ratio; box.x = box.x - offsetX; box.y = box.y - offsetY; return box; } /* Pan by dragging ======================================== */ var onDragGraph = false; var graphDragStart = {}; /* vizcanvas */ document.querySelector(".stage").addEventListener("mousedown", function (event) { if (event.target.mozMatchesSelector(".vizcanvas, .vizcanvas *") && !event.target.mozMatchesSelector(".node, .node *")) { onDragGraph = true; graphDragStart.x = event.clientX; graphDragStart.y = event.clientY; } }, false); document.querySelector(".stage").addEventListener("mousemove", function (event) { if (event.target.mozMatchesSelector(".vizcanvas") && !event.target.mozMatchesSelector(".node, .node *") && onDragGraph) { vizcanvas.style.cursor = "-moz-grab"; var offsetX = (Math.ceil(event.clientX) - graphDragStart.x); var offsetY = (Math.ceil(event.clientY) - graphDragStart.y); var box = getZoom(vizcanvas); box.x -= (offsetX * box.w / 700); box.y -= (offsetY * box.h / 700); graphDragStart.x += offsetX; graphDragStart.y += offsetY; setZoom(box, vizcanvas); } }, false); document.querySelector(".stage").addEventListener("mouseup", function (event) { onDragGraph = false; vizcanvas.style.cursor = "default"; }, false); document.querySelector(".stage").addEventListener("mouseleave", function (event) { onDragGraph = false; vizcanvas.style.cursor = "default"; }, false); /* Legend & Controls ===================================== */ global.toggleLegendSection = function toggleLegendSection(eventTarget, legendElm) { var elmToToggle = legendElm.querySelector(".legend-controls"); if (elmToToggle.classList.contains("hidden")) { elmToToggle.classList.remove("hidden"); eventTarget.textContent = "Hide"; } else { elmToToggle.classList.add("hidden"); eventTarget.textContent = "Show"; } }; global.toggleVizElements = function toggleVizElements(elements, classToggle) { toArray(elements).forEach(function (elm) { elm.classList.toggle(classToggle); }); }; /* Glowing Effect for Graph/Clock & Highlighting Effect for List ============= */ global.selectedNodeEffect = function selectedNodeEffect(name) { if (g.currentVisualization.name == "graph") { resetAllGlow("all"); addGlow(name, "selected"); } if (g.currentVisualization.name == "list") { resetHighlightedRow(); } }; global.connectedNodeEffect = function connectedNodeEffect(name) { // console.log(name); if (g.currentVisualization.name != "list") { var glow = document.querySelector(".connected-glow"); while (glow) { glow = document.querySelector(".connected-glow"); glow.parentNode.removeChild(glow); } addGlow(name, "connected"); } else { resetHighlightedRow(); var row = document.querySelector(".list-table tr[data-name='" + name + "']"); if (row) { row.classList.add("selected-connected-row"); } } }; // for Graph & Clock global.addGlow = function addGlow(name, type) { type = (type == "selected") ? "selected-glow" : "connected-glow"; var viz = g.currentVisualization.name; var gNodes = document.querySelectorAll(".node[data-name='" + name + "']"); toArray(gNodes).forEach(function (gNode) { var glowProps = calculateGlowSize(gNode, viz); d3.select(gNode) .insert('circle', ":first-child") .attr('cx', glowProps.cx) .attr('cy', glowProps.cy) .attr('r', glowProps.radius) .attr("fill", "url(#" + type + ")") .classed(type, true); }); }; global.calculateGlowSize = function calculateGlowSize(gNode, viz) { var glowProps = {}; var siteNode = gNode.childNodes[0]; var shape = siteNode.nodeName.toLowerCase(); var radius = graphNodeRadius[g.currentVisualization.name]; if (viz == "graph") { if (shape == "polygon") radius *= 2.2; glowProps.radius = radius + 22; } else { glowProps.radius = radius * 4; } glowProps.cx = siteNode.getAttribute("cx") || 0; glowProps.cy = siteNode.getAttribute("cy") || 0; return glowProps; }; // for Graph global.resetAllGlow = function resetAllGlow(type) { var selectedGlow; var connectedGlow; if (type == "selected" || type == "all") { while (document.querySelector(".selected-glow")) { selectedGlow = document.querySelector(".selected-glow"); selectedGlow.parentNode.removeChild(selectedGlow); } } if (type == "connected" || type == "all") { while (document.querySelector(".connected-glow")) { connectedGlow = document.querySelector(".connected-glow"); connectedGlow.parentNode.removeChild(connectedGlow); } } }; // for List global.resetHighlightedRow = function resetHighlightedRow() { var preHighlighted = document.querySelector(".list-table .selected-connected-row"); if (preHighlighted) { preHighlighted.classList.remove("selected-connected-row"); } }; /************************************************** * Singular / Plural Noun */ global.singularOrPluralNoun = function singularOrPluralNoun(num, str) { if (typeof num != "number") { num = parseFloat(num); } return (num === 1) ? str : str + "s"; }; function updateUIFromMetadata(event) { if ("version" in event) { document.querySelector('#version-number').textContent = event.version; } if ("browserVersion" in event) { const firefoxVersionRe = /^([0-9]+)(\.([0-9]+))?/; var majorVersion = Number(firefoxVersionRe.exec(event.browserVersion)[1]); var section = document.querySelector('.tracking-section'); if (majorVersion >= 35) { section.classList.remove("hidden"); } else { section.classList.add("hidden"); } } } function updateUIFromBrowserPrefs(event) { if ("trackingProtection" in event) { var toggleBtn = document.querySelector(".tracking-btn"); if (event.trackingProtection) { toggleBtn.querySelector("input").checked = true; toggleBtnOnEffect(toggleBtn); } else { toggleBtn.querySelector("input").checked = false; toggleBtnOffEffect(toggleBtn); } } } function updateUIFromPrefs(event) { if ("defaultVisualization" in event) { global.currentVisualization = visualizations[event.defaultVisualization]; if (global.currentVisualization) { console.debug("Got viz"); } else { console.error("NO viz"); } } if ("defaultFilter" in event) { aggregate.currentFilter = event.defaultFilter; document.querySelector('a[data-value=' + aggregate.currentFilter + ']') .dataset.selected = true; document.querySelector(".filter-display header").textContent = document.querySelector(".btn_group.session") .querySelector("[data-selected]").textContent; } } // Exports global.updateUIFromMetadata = updateUIFromMetadata; global.updateUIFromBrowserPrefs = updateUIFromBrowserPrefs; global.updateUIFromPrefs = updateUIFromPrefs; })(this); data/tooltip.js0000644000000000000000000000567212741721616012536 0ustar rootroot/* 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/. */ // Manage hover-over tooltips (works differently in graph, list, clock views) (function (global) { var tooltipTimer; var tooltip; // for Clock view function showTooltip(event) { if (!tooltip) { tooltip = document.getElementById('tooltip'); } tooltip.style.left = '-1000px'; tooltip.style.display = 'inline-block'; // console.error(event, event.target, event.target.dataset); tooltip.textContent = event.target.getAttribute(["data-name"]); var rect = event.target.querySelector(":last-child").getClientRects()[0]; var tooltipWidth = tooltip.offsetWidth; tooltip.style.top = (rect.top - 40) + 'px'; tooltip.style.left = (rect.left + (rect.width / 2) - (tooltipWidth / 2)) + 'px'; setTooltipTimeout(); return false; } // for Graph view function d3ShowTooltip(node, idx) { if (!tooltip) { tooltip = document.getElementById('tooltip'); } tooltip.style.left = '-1000px'; tooltip.style.display = 'inline-block'; // console.error(event, event.target, event.target.dataset); tooltip.textContent = node.name; var shapeNode = this.querySelector(".site") || this.querySelector("[data-name]"); // look for "site"(circle) node or "tracker(triangle)" node var rect = shapeNode.getClientRects()[0]; var tooltipWidth = tooltip.offsetWidth; tooltip.style.top = (rect.top - 40) + 'px'; tooltip.style.left = (rect.left + (rect.width / 2) - (tooltipWidth / 2)) + 'px'; return false; } // for List view function listShowTooltip(event) { if (!tooltip) { tooltip = document.getElementById('tooltip'); } tooltip.style.left = '-1000px'; tooltip.style.display = 'inline-block'; // console.error(event, event.target, event.target.dataset); tooltip.textContent = "go to " + event.target.parentElement.getAttribute(["data-sort-key"]) + "'s site list"; var rect = event.target.getClientRects()[0]; var tooltipWidth = tooltip.offsetWidth; tooltip.style.top = (rect.top - 40) + 'px'; tooltip.style.left = (rect.left + (rect.width / 2) - (tooltipWidth / 2)) + 'px'; setTooltipTimeout(); return false; } function setTooltipTimeout() { if (tooltipTimer) { clearTimeout(tooltipTimer); } tooltipTimer = setTimeout(function () { timeoutTooltip(); }, 2000); } function timeoutTooltip() { tooltip.style.display = 'none'; tooltip.timer = null; } function hideTooltip() { timeoutTooltip(); return false; } function add(node) { node.addEventListener('mouseenter', showTooltip, false); node.addEventListener('mouseleave', hideTooltip, false); } function remove(node) { node.removeEventListener('mouseenter', showTooltip); node.removeEventListener('mouseleave', hideTooltip); } global.tooltip = { add: add, remove: remove, show: d3ShowTooltip, hide: hideTooltip, addTooltip: listShowTooltip }; })(this); data/svgdataset.js0000644000000000000000000000253512741721616013204 0ustar rootroot/* 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/. */ (function (global) { function dataAttrToKey(attr) { return attr.slice(5).split('-').map(function (part, index) { if (index) { return part[0].toUpperCase() + part.slice(1); } return part; }).join(''); } function dataKeyToAttr(key) { return 'data-' + key.replace(/([A-Z])/, '-$1').toLowerCase(); } function svgdataset(elem) { // work around the fact that SVG elements don't have dataset attributes var ds = function (key, value) { if (value === undefined) { // act as getter value = elem.getAttribute(dataKeyToAttr(key)); try { return JSON.parse(value); } catch (e) { return value; } } else { var s = JSON.stringify(value); elem.setAttribute(dataKeyToAttr(key), s); return s; } }; // Create read-only shortcuts for convenience Array.prototype.forEach.call(elem.attributes, function (attr) { if (attr.name.startsWith('data-')) { try { ds[dataAttrToKey(attr.name)] = JSON.parse(attr.value); } catch (e) { ds[dataAttrToKey(attr.name)] = attr.value; } } }); return ds; } global.svgdataset = svgdataset; })(this); data/style.css0000644000000000000000000004723512741721616012361 0ustar rootroot/* 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/. */ /* Visualization colour scheme: trackers: #E73547 visited: #6CB4F9 connections: #434242 background: #2E2B2B text: #EAEAEA dropdown buttons: #363636 sidebar "link" buttons: #939393 */ /* New colour scheme: sidebar, info panel background:#404850 */ html, body{ margin: 0; padding: 0; height: 100%; background-color: #000; color: #EAEAEA; overflow: hidden; font-size: 12px; } body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,aside,section,hgroup,footer,nav,figure,header,article,input,textarea,p,blockquote,th,td,select { margin: 0; padding: 0; -moz-box-sizing: border-box; box-sizing: border-box; font-family: "Open Sans"; } a{ color: #73A4B8; text-decoration: none; } a:hover{ color: #6FC3E5; } .large-header{ color: #73A4B8; font-size: 18px; } .all-cap-header{ font-size: 10px; text-transform: uppercase; font-weight: bold; } .blue-text { color: #73A4B8; } .grey-text{ color: #3E454D; } .main{ position: relative; height: 100%; } .controls{ width: 170px; padding: 15px 15px; float: left; z-index: 5; height: 100%; overflow-x: hidden; overflow-y: auto; background: #404850; } .controls .btn{ width:100%; } .controls header{ height: 35px; margin-bottom: 20px; } .main header:hover{ cursor: pointer; } .logo{ width: 150px; margin-left: -6px; } .hidden{ display: none !important; } .controls .section-header{ display: block; margin: 15px 0 5px 5px; } /* Dialog/Popup Styling ================================ */ .pico-content{ color: #000; border-radius: 3px; box-shadow: 5px 5px #555; } .pico-content .dialog-title{ position: absolute; top: 0; left: 0; width: 100%; text-transform: uppercase; line-height: 35px; font-weight: bold; letter-spacing: 1px; color: #fff; background: #4CC7E6; border-radius: 3px 3px 0 0; padding-left: 20px; } .pico-content .dialog-content{ overflow: hidden; margin-top: 30px; } .pico-content p{ margin-top: 15px; } .pico-content .dialog-sign{ float: left; width: 20%; } .pico-content .dialog-sign img{ margin-top: 10px; width: 100%; height: 100%; } .pico-content .dialog-message{ float: left; width: calc( 80% - 20px ); margin-left: 20px; } .pico-content .dialog-controls{ clear: both; } .pico-content .dialog-dns{ margin: 20px 0; } .pico-content .dialog-btns{ float: right; } .pico-content .dialog-ok, .pico-content .dialog-cancel{ display: inline-block; background: #4CC7E6; color: #fff; font-size: 10px; font-weight: bold; border-radius: 3px; padding: 5px 25px; margin: 10px 0 0 10px; cursor: pointer; } .pico-content .dialog-ok:focus, .pico-content .dialog-cancel:focus{ outline: 1px dotted grey; } .pico-content .privacy-policy{ height: 150px; overflow-y: auto; border: 1px dashed #ccc; padding: 10px 0; margin: 0 5px; } .pico-content .privacy-policy.collapsed{ display: none; } .privacy-policy ul{ margin-left: 15px; list-style-type: circle !important; } /* Button Styling ================================ */ .btn{ margin-bottom: 5px; background: #171E25; letter-spacing: 1px; border-radius: 5px; } .btn a:hover, .btn_group .dropdown_options a:hover{ background: #6FC3E5; cursor: pointer; } .btn_group .dropdown_options a:hover{ margin: 0 2px; } .btn_group a.selected_time, .btn_group a.selected_visualization{ color: #EAEAEA; } .btn a{ border-radius: 3px; } .btn_group div.dropdown_options a, .btn a{ margin: 0; display: block; padding-left: 10px; background: #171E25; line-height: 28px; text-transform: capitalize; color: #fff; } .btn_group .dropdown_options a{ border-radius: 0; border-top: 2px solid #12181B; } .btn_group .dropdown_options a[data-selected]{ background: #73A4B8; margin: 0 2px; border-top: 2px solid #6FC3E5; } .btn_group .dropdown_options a:first-child[data-selected]{ box-shadow: 0 -2px #6FC3E5; } .btn_group .dropdown_options a:last-child[data-selected]{ box-shadow: none; border-top: 2px solid #6FC3E5; } .btn_group .dropdown_options a:first-child{ border-radius: 3px 3px 0 0; border-top: none; } .btn_group .dropdown_options a:last-child{ border-radius: 0 0 3px 3px; box-shadow: 0 5px #12181B; } .btn_group img, .btn img{ width: 15px; height: 15px; margin-right: 15px; position: relative; top: 3px; } .controls .links{ text-align: center; line-height: 30px; margin-top: 15px; } .controls .links img{ width: 15px; height: 15px; } .toggle-btn input{ display: none; } .toggle-btn .toggle-btn-innner{ display: inline-block; background: #5F6771; border-radius: 5px; width: 60px; height: 28px; padding: 3px; cursor: pointer; } .toggle-btn .toggle-btn-innner > * { display: inline-block; } .toggle-btn .toggle-btn-innner .switch{ float: left; width: 45%; height: 100%; background: #303539; border-radius: 5px; } .toggle-btn .toggle-btn-innner .on-off-text{ float: right; line-height: 20px; font-size: 10px; font-weight: bold; margin: 0 5px; } .toggle-btn input + .toggle-btn-innner.checked{ background: #25292D; } .toggle-btn input + .toggle-btn-innner .switch.checked{ float: right; background: #4CC7E6; } .toggle-btn input + .toggle-btn-innner .on-off-text.checked{ float: left; } /* Button Styling ends ================================ */ #content{ flex:1; position: relative; z-index: 0; border-top: 1px solid #555555; height: 100%; min-height: 0; } .content-flex{ display:flex; flex-direction:column; height:100%; width:calc(100% - 170px); } .top-bar{ background-color:#fff; flex:none; padding: 20px 20px 5px 20px; text-transform: uppercase; } .stats-section{ float: left; color: #3E454D; } .stats-section section{ float: left; margin-right: 20px; } .tracking-section{ float: right; text-align: center; } .tracking-btn label{ position: relative; top: 10px; } .tracking-section .label{ color: #3E454D; margin-right: 5px; } .tracking-section div{ display: inline-block; } .info{ width: 8px; height: 100%; float: right; border-left: none; transition: width 0.5s ease; overflow-x: hidden; overflow-y: auto; background: #404850; } .info-panel-controls{ display: block; float: right; z-index: 1000; margin: 10px 0 0 0; } .info-panel-controls ul{ list-style-type: none; } .info-panel-controls ul li{ width: 28px; height: 28px; position: relative; bottom: 10px; margin-bottom: 5px; padding: 18px; background: #20272E; border-radius: 2px 0 0 2px; cursor: pointer; font-size: 15px; } .info-panel-controls ul li.active{ background: #404850; } .info-panel-controls img{ width: 15px; height: 15px; } .info-panel-controls img, .info-panel-controls .icon-chevron-right{ position: relative; right: 8px; bottom: 8px; } .toggle-site-profile.disabled{ cursor: default; opacity: 0.2; } .info .holder{ display:flex; flex-direction:column; width: 300px; height:100%; padding: 17px 23px 8px 18px; } /* Help Sections */ .help-content svg{ width: 15px; height: 15px; position: relative; top: 2px; } .info .holder > div .large-header{ margin-bottom: 10px; } .info .holder > div .large-header img{ width: 16px; height: 16px; } .grey-label{ display: inline-block; margin-right: 5px; color: #777; letter-spacing: 1px; font-weight: bold; } .help-content .blue-text.all-cap-header, .about-content .blue-text.all-cap-header{ margin-bottom: 10px; font-weight: bold; } .info .holder section{ padding: 15px 0; } .info .holder section:not(:last-of-type){ border-bottom: 1px solid #303840; } .info .holder section p{ margin: 10px 0; } .info .holder section ul{ list-style-type: none; } .info .holder section ul.bullet-form{ list-style-type: circle; } .info .holder section ul.bullet-form li{ list-style-type: disc; margin-left: 15px; } .privacy-policy header{ margin-top: 15px; } .info .holder section img{ width: 15px; height: 15px; margin-right: 5px; } .feature-name{ display: inline-block; width: 120px; font-weight: bold; text-transform: capitalize; } /* Info Panel(Site Profile) Sections */ .map-section{ margin: 10px 0 10px 0; } .world-map, .connections-list ul{ background-color: #303539; border-radius:5px; } .connections-list{ flex:1; display:flex; flex-direction:column; } .pref-tag img{ width: 15px; height: 15px; position: relative; top: 3px; } /* End Map Styles */ .info .holder .favicon{ width: 20px; height: 20px; margin-right: 10px; } .showinfo .info{ width: 300px; } .info .filters{ margin-bottom: 1em; } .info .filters h2{ white-space: nowrap; font-size: 10px; font-color: #CCC; width: 100%; } .info .closed p{ display: none; } .info .disclosure{ cursor: pointer; } .info .disclosure:before{ content: "▼"; padding-right: 1em; } .info .closed .disclosure:before{ content: "▶"; } .sorted:after, .reverse-sorted:after{ padding:0px 5px; content: '▾'; position: relative; -moz-transition:all 0.3s linear; } .sorted:after{ transform:rotate(0deg); } .reverse-sorted:after{ transform:rotate(180deg); } .connections-list ul{ line-height: 15px; list-style-type: none; min-height: 150px; overflow-y: auto; flex:1; } .connections-list ul li{ padding:2px 2px 2px 10px; cursor: pointer; } .connections-list ul li:hover{ background-color: #6FC3E5; } .connections-list ul li[data-selected]{ background-color: #73A4B8; } .connections-list ul li.disabled{ color: #555; pointer-events: none; } .stage-stack{ padding: 20px 0 20px 40px; flex-direction: column; display: flex; position: relative; height: 100%; overflow: hidden; } .stage{ flex: 1; min-height: 0; /*margin: 20px 0px;*/ } .showinfo .stage-stack{ /*margin-right: 300px;*/ } /* SVG STYLES */ text { fill: #fff; font-size: 9px; font-variant: small-caps; text-anchor: middle; } #tooltip{ display: none; position: absolute; background-color: #FFF; color: #010203; padding: 5px 10px; box-shadow: 0px 2px #4CC7E6; border-radius: 5px; } #tooltip:after{ content: ''; width: 0; height: 0; border: 10px solid transparent; border-top: 10px solid #FFF; position: absolute; top: 100%; left: 50%; margin-left: -10px; } /* CLOCK Visualization */ .source, .source-sites, .target, .target-sites{ fill: #FFF; stroke: none; } .source.highlighted, .target.highlighted{ opacity: 1; } .source text, .target text{ font-size: 6px; text-anchor: right; stroke: #FFF; visibility: hidden; } .times-label{ font-size: 15px; } .times-am-pm-label{ fill: #3E454D; font-size: 10px; } .tracker rect{ visibility: hidden; } .tracker:hover text, .tracker:hover rect{ visibility: visible; } #timerhand{ fill: rgba(76, 199, 230, .6); stroke: rgba(76, 199, 230, .6); strokewidth: 3px; } .greyed-out{ opacity: 0.2; } /*.clicked-node.node circle:last-child{ stroke-width: 2; fill: #fff; } .clicked-node.node.source circle:last-child{ stroke: #128764; } .clicked-node.node.target circle:last-child{ stroke: #F1C40F; } .colluded-source.node circle:last-child, .colluded-target.node circle:last-child{ stroke: #fff; stroke-width: 1; }*/ /* Graph Visualization */ .filter-display{ display: inline-block; position: absolute; background: rgba(0,0,0,0.9); } .filter-display header{ text-transform: capitalize; font-size: 30px; } .visitedSites, .unvisitedSites, .selectedSites, .colludedSites{ fill: #FFF; } .connectionLine{ stroke: #FFF; stroke-width: 2; } .watchedSites{ fill: #6FC3E5 !important; } .watch-text{ color: #6FC3E5; } .hide-text{ color: #FE7E00; } .blockedSites{ fill: #E02A61 !important; } .block-text{ color: #E02A61; } .cookie-text{ color: #6C0AAA; } .edge{ stroke: #FFF; opacity: 0.3; } .visitedYes, .visitedNo, .visitedBoth{ fill: #FFFFFF; opacity: 0.3; } .visitedYes.highlighted, .visitedNo.highlighted, .visitedBoth.highlighted, .edge.highlighted{ opacity: 1; } .cookieYes.coloured, .cookieBoth.coloured, .cookies{ stroke: #6C0AAA; } /* List Visualization */ .hide { display: none; width: 0; height: 0; } .breadcrumb{ display: inline-block; overflow: hidden; margin-bottom: 15px; } .breadcrumb .breadcrumb-chunk{ float: left; cursor: pointer; color: #555; } .breadcrumb .breadcrumb-chunk.no-click{ cursor: default; color: #4CC7E6; } .breadcrumb .arrow-left { float: left; width: 0; height: 0; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-right:10px solid #303539; margin: 7px 10px 0 10px; } .rows-selected-label{ float: right; margin-right: 10px; text-align: right; } .rows-selected-label div{ display: inline-block; } .rows-selected-label .deselect { color: white; font-weight: bold; margin-top: 5px; } .rows-selected-label .deselect:hover { cursor: pointer; } .num-selected, .num-total{ font-weight: bold; } .list-table{ border-collapse: collapse; height: 100%; margin-right: 10px; } .list-table table{ width: 100%; border-collapse: collapse; border-spacing: 0; } .header-table{ background-color:#404040; margin-right: 20px; } .body-table{ height: calc(100% - 40px); overflow-y: scroll; } .list-table th, .list-table td{ border-bottom: 1px solid #555; line-height: 35px; margin: 0; } .list-table th{ text-align: left; cursor:pointer; -moz-user-select: -moz-none !important; } .list-table [role]{ outline: 0; } .list-table tr[site-url]{ cursor: pointer; } .list-table tr.selected-connected-row{ background-color: #73A4B8; } [data-pref=watch]{ color: teal; } .preferences{ background-size: 16px; background-repeat: no-repeat; background-position: center left; } [data-pref=watch] .preferences{ background-image: url(icons/lightbeam_icon_watch.png); } [data-pref=block]{ color: red; } [data-pref=block] .preferences{ background-image: url(icons/lightbeam_icon_block.png); } [data-pref=hide]{ color: orange; } [data-pref=hide] .preferences{ background-image: url(icons/lightbeam_icon_hide.png); } .hide-hidden-rows [data-pref=hide]{ display: none; } .list-table tbody tr:hover, .list-table tbody tr.checked{ background: rgba(255,255,255,1); color: #000; } .list-table .visited-row td{ color: #6178FA; } .list-table .third-row td{ color: #FF617B; } .header-table th:nth-child(1), .body-table td:nth-child(1){ padding-left:10px; width: 35px; } .header-table th:nth-child(2), .body-table td:nth-child(2){ width: 6em; } .header-table th:nth-child(3), .body-table td:nth-child(3){ width: 5em; } .body-table td:nth-child(4) img{ width: 16px; margin-left: 4px; margin-right: 4px; } .header-table th:nth-child(5), .body-table td:nth-child(5){ width: 10em; } .header-table th:nth-child(6), .body-table td:nth-child(6){ width: 10em; } .header-table th:nth-child(7){ width: 130px; } .body-table td:nth-child(7){ width: 115px; text-align: right; padding-right: 15px; } /* Legend for Graph and Site Preferences for List*/ .graph-footer, .list-footer{ margin-right: 8px; flex: none; } .list-footer{ margin-top: 50px; } .legend-header{ overflow: hidden; border-bottom: 1px solid #fff; padding-bottom: 5px; } .legend-header div:first-of-type{ float: left; } .legend-header div:last-of-type{ width: 170px; float: right; } .legend-header .legend-toggle{ float:right; } .legend-header header{ display: inline-block; margin-bottom: 3px; } .legend-header .legend-toggle{ display: inline-block; font-size: 10px; cursor: pointer; } .legend-controls{ clear: both; padding: 10px 0; } .legend-controls .legend-label{ margin-bottom: 15px; font-size: 10px; letter-spacing: 1px; border-radius: 5px; width: 170px; } .legend-controls .column{ float: left; } .legend-controls .legend-canvas-small, .legend-controls .legend-canvas-large{ height: 18px; position: relative; top: 6px; margin-right: 5px; } .legend-controls .column .btn img{ position: relative; top: 4px; margin-right: 5px; } .legend-controls .legend-canvas-small{ width: 17px; } .legend-controls .legend-canvas-large{ width: 34px; } .legend-controls .btn a{ font-size: 10px; } .graph-footer .btn { width: 170px; margin-right: 30px; margin-bottom: 8px; } .graph-footer .btn a{ border-top: 2px solid #171E25; color: #555; } .graph-footer .btn a:hover{ border-top: 2px solid #fff !important; opacity: 1; color: #fff; } .graph-footer .btn.active a{ border-top: 2px solid #6FC3E5; background: #73A4B8; color: #fff; } .graph-footer .btn_group{ float: right; width: 150px; } .graph-footer .btn_group .all-cap-header{ margin-right: 10px; } .graph-footer .btn_group .dropdown_options{ width: 170px; } .list-footer .align-left, .list-footer .align-right{ display: inline-block; overflow: hidden; } .align-left{ float: left; } .align-right{ float: right; } .list-footer .align-left .btn{ float: left; width: 140px; margin-right: 20px; } .list-footer .align-right .btn{ float: left; margin-left: 20px; } .list-footer .align-right .btn a{ padding: 0 15px; } .list-footer .btn.disabled{ opacity: 0.15; pointer-events: none; } .list-footer .btn{ opacity: 1; pointer-events: auto; } .list-footer .btn a, .list-footer input[name=pref-options] + label{ background: #888; color: #fff; } .list-footer .btn a:hover, .list-footer input[name=pref-options] + label:hover{ background: #6FC3E5; cursor: pointer; } .list-footer label[for=block-pref]{ box-shadow: 0 2px #E02A61; } .list-footer label[for=hide-pref], .list-footer .btn.toggle-hidden{ box-shadow: 0 2px #FE7E00; } .list-footer label[for=watch-pref]{ box-shadow: 0 2px #6FC3E5; } .list-footer input[name=pref-options]{ display: none; } .list-footer input[name=pref-options] + label{ margin-right: 10px; width: 140px; line-height: 28px; } .list-footer input[name=pref-options] + label img{ width: 15px; height: 15px; margin: 0 5px; } /*.list-footer input[name=pref-options]:checked + label{ border: 1px solid red; }*/ .list-footer input[name=pref-options] + label .radio-dot{ width: 15px; height: 15px; background-image: url(image/Lightbeam_radio_off.png); background-repeat: no-repeat; background-size: 15px 15px; display: inline-block; margin-left: 5px; position: relative; top: 3px; } .list-footer input[name=pref-options]:checked + label .radio-dot{ background-image: url(image/Lightbeam_radio_on.png); } tr.refresh { text-align: center; background-color: rgb(23, 30, 37); visibility: collapse; } tr.refresh.show { visibility: visible; } tr.refresh.show:hover { background-color: rgb(115, 164, 184); cursor: pointer; } data/parseuri.js0000644000000000000000000000203312741721616012662 0ustar rootroot// parseUri 1.2.2 // (c) Steven Levithan // MIT License function parseUri(str) { var o = parseUri.options, m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), uri = {}, i = 14; while (i--) uri[o.key[i]] = m[i] || ""; uri[o.q.name] = {}; uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { if ($1) uri[o.q.name][$1] = $2; }); return uri; } parseUri.options = { strictMode: false, key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"], q: { name: "queryKey", parser: /(?:^|&)([^&=]*)=?([^&]*)/g }, parser: { strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ } }; data/map.svg0000644000000000000000000110252212741721616011775 0ustar rootrootdata/image/0000755000000000000000000000000012751274675011567 5ustar rootrootdata/image/lightbeam_icon_help.png0000644000000000000000000000063212741721616016241 0ustar rootrootPNG  IHDR22)x3PLTEܤtRNS 0@P`p#IDATxŕn _ۊJ*}h5{N`24ɰ;!=Guzn5PSiv@;(\F=]A: G+Ys٬ժnN0P(2 &%<; c_n<maGL)Tvaz: p zWn:#HhL~6bM)bĀ חRT+5傁*PA$JʥnrgSU D/@eIENDB`data/image/lightbeam_popup_warningreset.png0000644000000000000000000001121212741721616020230 0ustar rootrootPNG  IHDRn<cPLTEMMMMMMMMMMMMMMMMT\cjqy2tRNS 0@P`p#IDATx܉Σ6@aHcaa?Uk!>+K\<0J0 }ϻ8АsU񞹶tp8I_wn7\({ʢ ,P$7?Z]ʜL!|Gs{"X͛%D>yl$/.71w]\|_2]H;s}|9ƹ|b^O6Ki;=Vؗ^* =%Re]RGϮ0CT7战\|L]׏='$d96XsYS=R)GOُr>J꽹ǽF+~WSrB]ݎrcEOmJë~KQvz.}OWztɍU;L]lVz 64%|<ȕX: V_{^5߷}Oyil;ʙchg)H\h:%i.xAX5W<"VW[xkfg65$w:x$^ƚ+G[X5}:kY7S1kuj*~LRGScoXhR(H, \ R_ʯ܌}B/M l͞Დ{\ŮJA.6Gw {}fL8fnIK)塚0K'Y1eu4 VęЪ5xi( ;Oe <%l{9WqJ`ĩ0fvNKAdV\rIkKncqIb)Q_?e?jS=@3fߎ(-t%`&Os+S,2OW·u.σl򴐁CW:*p}o* ѣ}y6ӯ`G \ jb2ՖbkK+dR,A+X_Q?q08swJ@ -WpM^/cFF9ڃْ|v7 ;,SyxʼUC04S'-42&J1( !8SA): ⛐YϡҟEܹJΛ_a3@0 G_k!g: ЋԳ߇NU q+Wį"N"t{"=S铀VhF(̸Ӆ3lO5e?a{bڱhPX} UGIz<ݽ?az瞤/0_gQ@v.厅=g"P[ȀcY g7֤`IeMahS(7^:Cܩ|G@Vܧf+m!q֩<mt&鉖l֣5=v1<;U =]wJacEonA_hm̰íZvߡv Jgl/Ýa cn[>;þHGx^Sg#t;Ýpg1ñg<iV~ewoRuC:yVb &)OaR7! ;jI?ΰq xOʏaK76-23=;o*owKPy~ptCO!gRKb#>I# 'U\yxC`$XC<[qx/Lz|U~Zx-<<=wDx_ˬ0Qzwy偽iG M{`YU>>G@6 wTOEGq4ou&ojgWh(N|~`vˡ;啺jžRB:oAZgDN?:Tv}C@WG {?|wiGݥkod/!X=In`xG,1k߿ `}G9LP4}pkOGofA-&plɣ4t."|(0ɠNjc0“(z -a!0 &%܎Z u#aJԏa60]^t~3o1G^Y}]ZQzP i0W^Vur _Uj8N u|eG ;\7>< ǚSڀc#lO=Ze`AS1VZdX-f XrFiɕ/geh 9949y F8M`x6Qk􄈝ΜC>7Vkx1LGczN9gd% kjB!xΞqs{I۴6i~eĐ0"j-LMߦ ^b:-I˽}WRФ읓7N+ s4|u;//LW Nm[r yE|W[<]*|pȷR?^pb]n^_g~E|WY杭 ʼ'y;\ !xϽ)o7a1;l!,qښ9eaoc֔kbaāzK7f!@|5c1,Erާ˻ =Z!F<9Y#_ z@ =b'ޛ9Lj'|1˳Jc+@?ƏY!e|0 u!.k-a$@fۀ1?`CoYzwCY#cCYBX>s$4m!9S:%,W`,­v՘`k>Ž;0FG0a Flw&͘ܡ$~YM /bj34"̷N֖ Ā|{Ķm B 8<+b[-cs#F-a?1n^ޙ!b'ƀ"^lrĀ*V1< waʘU~IWHNV~✘oq, 7GEv0|mNxsm/ o2oIAVf]/zgK%Jx@xCth~3dU!鏹1!x<+AohJ<`O'n-/C?nusv -b~=&+\ 34dk'P=5 ! -rSI{Đ5uJG~R/wۥԆEݹ\^ҟ6=˹ˇx& <]'Ӌi 9[}qǚuَ;фu*SBc޺][6ʻ n-:?|e,_3|GiXBraarWb][Pu2ͫYReGS, N[whj {lUynO׻ɥ8feԔ](`Z)VͣV1u8,ud2fDJcUJ_/1<=IENDB`data/image/lightbeam_popup_privacy.png0000644000000000000000000001310112741721616017174 0ustar rootrootPNG  IHDRn<PLTEMMMMMMMMyMM{MmMWtMzM^MMT\cjqyÌCtRNS 000@@@PPP```pppﰗxIDATxܱ0@@B X9N݅fٙA_iПDe g%EoЋYi1C;A58ՠb@aPAaP0( 觰>|Ya>.-~ج~],[$C|4H<S?1Hҧlqv.]͢/t'⮯I(\𘃼(IMW@:g͖/l6aLqnSU4.j3/ôidn;ba6ʶaZ eadvRk.?+aы 'YfnE$Yb^$i^Ll'i)La S0(  0)La S{w޴np@JJ{ґn$c,,݆ý9[e! Xd<ۘ-iS)h 7MGF60E(]"JV>6 V,myB4e)1up$eQP[(#Oe>ohVa=2K j7ZlCey)AMÃ- uUŪ YDAKfYv"_D`!|Ya>qoRd똆X?@_7,-dW,N ' 08nh)͡ [a%qBU?B~[C&ߌ\TPwa%Ce[b-!AVTߑ3-+a H[_! 5jS Qc(Ԋ:1r{V*e1:A#gZBVL#jR]h?aNw?$VԮ;8o&ߘDf^| @Zg薣OOKb++$\{-xK$ ~~Z_rZ2Zc97POo: oMD9,Ic+}7RaA ZY$m[ n-~7ߙwARcXɺ ST+#wfH@+2wT@i̲]]WMg^ۓd9+ouk/✃@ZB嶤#P_U@`U܅jKle鍗 c}~C?tHl4ec_qƴv(q3hҞ[PXcv|P`<G_k8?puPB8KFJC4&1P^2NL%АTSuV2loej*0ܠ Cq&t<'h)L!XQ2CqƗ0`M/soQ 8_9)J;`',Y.3z.h` A'tG+SkN`ǷxroM %sG()U%- ]2DpL8Yzʺ#Ɯ\8#kBY`Bko'PVe:bvݳ,!X_NL ] !2i\}]ӌwF!̘Qbf4Z izDĻx[FJb?E +1-V+1z:Fg6jb ?xck7yИ+Y_|œcۅO~. Ѱ|pJ@Ż^>\.st:.Pbuכoxzad۳kvs5|}o6b:<;=*.rzVM]ӯ6ơx˻Ǖ 74M|Ǔ;VjmqS[]o ;"#PpES9o}71xyS]4]xٯ)R,[fMa@!P݅a6L(۔ Gi|#=CPvaZ4=ݰbAz!N^;s)<zǃ, .H =0+Q37f4`b&E¹TȐv0e z;8¤(W8Gj3up0 ߉C.f|e m"[>J.L i yœYe*ym`H_8 COp*Q'V/& Z86T-!ڲnz Sr3g ]^SqAXBQ EOCPЃi ,(B1j0Y6=ډ ?eB&XyoJtٶ;tٱo% ](DsJ0iCXLA#ϩg,/F"`wtaG\U&L;X0wǽ0&OY+I;[0+ͦ &yGT,w*xª>F{[r>PY5a7OY)0rb\ea˜t'w>#W>:VzwwŪ¬˛ywiy#|NiKϵctc}az>/#0˼Xb , 0cI,Hdorb0/7<!<EQN@ Nj40q岧ҟQ|Kh3;L†8 9<9Zgveakz:|u|MwM،>|x.`yǘ[.GWOqQib;0]W|0A,:Tgi1mDˬf YkR+u" ,H C{ev>鎕مM|8#7㼣-\\64,_3KCf`bw̹ݎ0 o?`w 61_7Q 4@Uīafaü,{o)~g=XB&Ks[z"M ֲVq<]834p].8Y6.]zlb[YdVNT>^?|]ۗ|5/lwSŻ[9vm;+(A&_&6%({K٠ fʥyw Pc?J/?Mulx`(t㒢,$&{ayCK5lrħG͙Dž< k"aS{>6Ouŷ;M->j_m&m`EjPdv4IgIdX[MnK?%ynf_o f64k8ԼS&<߿Sd>A=x=rVŅr箜q]q^rF4afaeֆ3ԟwلBLJ*c*D|J*c yœ;œ;*œ;֠²cXiGe;œ+yGO9?V=yG_9f=yG_;Z*,{ޱXP;Z*ҿAe;*,|Qa,נ²T5F²[Pa ?VSa=cs>諰yTX6˹ w UXҖ ˞w,Ae;*,|Qa5cc>訰yTX6˩ wTX-yT?hUXTX\ UXPaߖ*,DK_eoAe}Ok *,xRaQm Paa [M? y*¯ š k*š š kw Q`h J@E]hZÛI& 0 ?n,Bሪ7BXxhhko~9˦P8d+R8UY_({D*Q}"U\]8s fO$(SkILRw?ӺSQK-|ughIENDB`data/image/lightbeam_popup_hidden.png0000644000000000000000000001400512741721616016756 0ustar rootrootPNG  IHDRn<PLTE>ENMq:>ENMq:>ENMq:>ENMq:>ENMq:>ENMq:>END}KMq:>ENMq:>ENCpMq:>ENJMq:>ENMq:>ENMq:>EN@YfMq:>ENMq:>ENHJMq:>EN?MX@UaA]kBftCn~DvE~FFGHIJKLMq:p[PEtRNS  $(,000048<@@@@PPP```pppppTjIDATx= a"22gX" @T ge5XlOn sOP!$");;YC2 E¶mQU Lڝ+zR~3U&;3{XEJZ ^kMU_52!OU߇b.}x]fBfUQU3y3hE;Tu.TUbi.i׃{2~2_2&_2&_\$j/7U8Ϋ`\ds>U |:r j0‰$jPaNf5>UTwlAê*d6\7?/,:b̖3~L)V?8Dͬbݛ-Qx)q~$GGKp%Hwqb.lf.QvgN]Lҝ](q}e|%O+-ͼ?Yu1[>)-kD|u577TֶA=P)egD<3N2&'W XKD.Mqk 7p[6oy>'[DˆGnڨ #%F85Cl Nfky 9 dY}Nijc%V3^ZkCNf/vUyvYۜmN2_u֫Rc˼_|]cwŎ^ g;yaG;eψ<;洑 X⒍ ލA.psȋc\%&U9%Y'F=x<9B}LH(LJ0(>erC?zgdyRoط弒C)ΨI9fo1G2|"%8~tCV |7Qi]&V l}FY^Ol'lyJl9Ă'3FKuY^ c5gU-y&[mF6َQ nU*јG2m+aMO9ӬP5MHѥ@ qɛ۩N!Mg1䫧;QɧcL!L#G҅^TX.K6UOQũ y^L #@/Lxڒ5g:)2TJ9+3l4 f0Yx>mK}? Z.&b._ 2J_FF['cyX.fΚmHC-躑3kؑngT d.dk$۴ah+SgO\M=CX˭6'Zcˆٌs΂5gօr%ʢSΚ.5&}d#v.S5@Ŕ(L 1eBC8[ ]YHN >@=3ƒJyV^MN|=B bizVY @帢 ^eI2&/8b}I WZ\h$th+ؒ#M_F\MH@ח"Ix E[IKgWPۭiJ̴CNE/AY*N%"6 ~N<z5α8t[Hcn~@DGܺD0=P8tԍI?|3kiF4Nc`?a 0` =0Q]!%0yO{$ I`@X!OIO1@BMD0~6A0My4M;tLUqr! A<[1:J,dk@ۡϤ 1FxNlz@w1 ٟ{ ;3A(xFL^SHjUfmPM*H8D[*B 1MRa,iZ,B,S&8WɊ-ѭv2'xh_P!8o"b<W?#-z+mEpZi4R)o盿$\zJ,4*9ÖZ,'׸ZjVaT ,5|ɤHӚraCHN0$i_\_3 pbf v*oY0v= &IK@-f圴b3p 㯹+hp 9q ѹN\1&oϰh2[@E[0*iu@)|I | nVD;1ki1 l) cH1 Wyd0Fi^*-e8P%w,B܃)STb*+{PwmEb܋!Swi{3FPܓ]؎n~Su7btST3-_-^x7؊:Ay} vev>LLG%/ ۰.^ L8n@ѿ∺aT?| +_+sT_x+" Ϟ|{qHH.Y?E3/UOe|;7M?\ÿ0@Y"4R۹,MNkЅG9_{dne}+1DJ..яK|Bΰ /ǛZ~r`FRϻz-*nXw:S-=,;_͎;g5ܿ:Ѽ~ݫ~|׹xZӐ%¤`{N.YC ,7wfϿ]87 ;Lbt[~9hu' K }|y65bnw"Zh`O+íl|){_nIr~ V@0BP]]sVY//D㚠 ɍEh$NJ#+i,AvٽcSR|ΐ)·o=B•AB!u#2|OLB䴾ٻ c&U͈KSMS/c-_||/ئxi}aKGN7/%9N! c^ R[c}ݪCwvA۞~?WgDhͅa\VZ;Tq ,n% Ϙ:$(3k|6b}K$GQ@3&y~glP*#Cbzuld})y"vaJ2 hqD|jƓй F{0V"Ozk@I; ==8D-Pҙ˲sx[{ۗOȲ'%kZZՇZ0_:Zo(E0V@ *`; rOOҁ)b*ea៊aW[fBa$ Dfvu!",V\C Ge^7 14P3,JW_GI 'FzZ@J,;m?j=M'*Η&A@5KJx.k'{0Lj܊AyO/X4EYtK$#7sK߀8`Q|&uМ';ae;(ޗEm:4~DV0N'xzk @.Y+)<1 Uo޶a(Z^j룿jsmoCDѶO<[Qkvɗ` jMb<y۵L DvsU{։m S3h9 j`tB`m`oLo.ٷo;ōhiq  O.!:1[CSd:Z,tG%)`c{uSy?@Ɔc?+: WuL@1SѺb Yv!m\zgU!X5Ћ#bɺ?.mm}TMEѥ)5 {Y$"V ;*E6zg7>jc1"hv"`$Drs F6 ,-v;E0k"* ȝ. +$![L'9:rQ}Gldff f2Pj}'hx͸^˒RD _mD)֡}V*ԏE_"+J"ƍxKW#*hVïv t㑆-& $zkq!X颈ӍoՏ Vw\Kڎl[S)d;g|DίzsWMxn\{˖ YOrvz[?rVt'VI6g此ImXRYeu3 1\֟F69CiSJ|SJsv 3B\.4IENDB`data/image/lightbeam_popup_blocked.png0000644000000000000000000000671512741721616017137 0ustar rootrootPNG  IHDRn<3PLTE>>>>>>>>>>>>>>>>ItRNS 0@P`p# 9IDATx EQc(HQV&1pSo9->ĘE~H=~Y9"qc\.M5#ssL^l%&{^eRY^j5?…"CVʾ C%W[џS/fi(w}T\lz/-"'#]`pTzgucA3߇`dLdLk\=sMբѾUA'TfuwlMBKa3Y;"uD+qh\HK"(./a>$U/P%o1fYч;Tϳ{|Wgk=W[>=N?QR.;z6V=*KSB Ub A^"先/ xs^]i\__iPwkFo e">!|]s9-Q"w; cKL~_ũ#vm7/NjNG3˳M#rPz `V-EziXojd4э-ɝm(j}7R v2' { zPLc ؾVCodFAm<`{;C3 =OWF,U6a:$}tEY_ِ$'Ǩ'M<:W:vGlXN !Y "7J1>E+:**5w%#`xzcSۃx*7 ]A[]gċ%[= 7o I^īF3||wq>kv ;zv *$ YQS'L=Rq'p?X~Rkg=hsM{Y5GeY>z,c:L?=*q>BlHӱ\L?OSe!  ; =pc66]C`P{<.Mg7vX;)cM.A `l=WEbN^cN6~`t YJg_MEnZ-b)Wj}"NS%=yxRسY 7=-^T|{< `ݔX/M`v6vih `ݺ&_ط,\+!, 5#BK?`)ar0[.Ei?0aF& 0#<pBXUfjyUx0[?``#vpsrI.)||i p,=|9'",-'^}ڷJ {Zޙ$ A%ARv vyB_oGx 0d(u7P~<,vEJd# pTzvyUwK&e^9 |fU{= ܩ{t8 whe׵ pI,SRO{ Aeu%nM{|v"ܣFr/G_8W0;q$A_J=_"1vuCAu7u~s҇jwzNg(A5$/7P Ƿ [&?/?ϳv%# "IiD;pYJHhApyKᖚ?$`.DZ D)6Q2aۊ,VNTt ^/*CJ֞Alz^2ߝJlxZnu8c7>, *Q- Вf4mpQ44pS6f5<0wUSDLb¼F\lѕC 31!f1an#a(vX` Z7OC?5Z p%w1 % 1o/\jC^q_⊆`PtE(/+a ;t5J&oO!G&`"ij0(S418i o{ #5~3Q]͝`ZX`3>6%Ft(2lPؠrh6n/ r 7Uk}yp=j ؃0d*aJڪ4T”*9vż7(9Ȳ^keYIqø[ɯ# %%2y4kV)KAf%ȟp F]'1- \8HF,~iIh!̝!Agp'-Z;' q:! C l@8/EP_{qK5t*-c? ߆8ޫďQ] 45~=޵0 DRa9`iPUm<} *S93*~UeK(|^?2zls|Y|ծ{\̽m?^rh֣ؔ<[UtqI^ϫKy8_XҔSԗo"4I<CԆ?ykL)<[fI%IENDB`data/image/lightbeam_logo-only.svg0000644000000000000000000002366312741721616016244 0ustar rootroot data/image/lightbeam_icon_website.png0000644000000000000000000000111012741721616016743 0ustar rootrootPNG  IHDR22)x3PLTEܤtRNS 0@P`p#IDATxŕn0!?"Vڬ4( H'>@jۍCLj{G/c\}v=tYmIJ`o#tXR=+&a q6r0 Ezc >(jYAaP8I%?Fb"_H k)NrCsL[ ROVPxp*%x<2 C.3I Xe]"S<0Ȭ..Iƭ(*ULnEWm%gJ#D Gql J$[c6Yq ՋDU2'a/2l$b﷭+j4$MDvQFۊ$ O!Țt%F|؆F.?AC<*;R?ixr&y(: >nٗ[wVIENDB`data/image/lightbeam_icon_sortby.png0000644000000000000000000000060212741721616016630 0ustar rootrootPNG  IHDR22)x0PLTEWtRNS 0@P`p|X IDATxK cFP慴˨5 ,~L}qJRQ}W*'bgڰw+{ ֜)'-lfa(4Q(ҵdR`2dJ 6xG'fumN-3v(^yI7 %"JJXY zZW)͢tQGX!EmߏU;T4U*IUt&:Z-p͋#C@_@e0PIENDB`data/image/lightbeam_icon_reset.png0000644000000000000000000000061612741721616016435 0ustar rootrootPNG  IHDR22)x3PLTEDEtRNS 0@P`p#IDATxڵ EVcLnLF=n[dИ'c+^ Vi 亸@< 8=@\6`9!&/l {xie-WRu}Hv$W)H5rq8Eu 4JS'L8yN)s6ܬX$'inq[F=.s(}$9,_q:]IY%QDBKEz-*?7>/ {@IENDB`data/image/lightbeam_icon_list_blue.png0000644000000000000000000000057412741721616017300 0ustar rootrootPNG  IHDR22)x3PLTEMMMMMMMMMMMMMMMM}4tRNS 0@P`p#IDATxn FQ@vWp&6Ȍ٘Mzd]THj"2gVtm3Wjoִ~|q{sٗA08TKGIn--u/"dprv{XtB\U"A%/Ɯf+{ q$!ElAWg߱{>UۼADtQڤ$J4 Psy\"Y%ഞU 8vC)IENDB`data/image/lightbeam_icon_list.png0000644000000000000000000000057412741721616016271 0ustar rootrootPNG  IHDR22)x3PLTEDEtRNS 0@P`p#IDATxn0 a;O6[f3j(p 7%&fl2A0?dvƼU]azw7YrTw'`e|>}XĪXttwiR t4Y" 6]w`nb'/ G')b4'!;yuE~"$1/V|N{4p!kG$&qOdC!C(*wD5QIENDB`data/image/lightbeam_icon_graph.png0000644000000000000000000000075312741721616016416 0ustar rootrootPNG  IHDR22)x3PLTEDEtRNS 0@P`p#WIDATxr0 1M $4r4 x=%Fb|EoJ4Z %q!+ex2C^rPUJ9 -I0^V,hTVJ0ho|+yHlBfxUW>J1K/}Ȭ8NP*:Unh_w%-N8}4-%ѣ3aܒ獽by whEv3t|9Pxil4 Peo>#){*ϕAw3Y; m( ʶuĦ75}Ï6+%{ s} G?IENDB`data/image/lightbeam_icon_feedback.png0000644000000000000000000000056012741721616017035 0ustar rootrootPNG  IHDR22)x3PLTEܤtRNS 0@P`p#IDATxڭn0 DцVQEBzg0HDs;VJņ$M4HeBB4 (4܎ȅoX(y2pϛ<+NHi%YTD %,o3딙Krj6Mf;92=\j-F-+S(.<kz3oS鼙]2 @S}Ǵ٠:F3rz1]S|mgb0qK z2=!y7sv!xlU߷1!͞}֎i.R?"uL1s)t]/Oww)̺lV-lN8fɔ~|`dla:ʔO"{-W%Z̖)[tΣ7dtoIS R^4YGڒ5 ks=pXmv͜2hC0ifP4fq"_$^h{%Cb TPV6KK-aeV%]bj%m~hʦC?'Mѓ8儶BfBUxe(*E# oeJYQuBiVd*uM{ilgJRU]4v#LaMUrG CΔRfSyd(xaZ\&9iQfIZ\ n9QU4O?L_0 ds L9s r0 (H&6vB e}) rGI3ZEޛGRB"h'V4h#WUj7SQΒXS;c\^KV1z3BjyT2yLI@'RuDv䆒TB9B8FɔӶ"P "aA&w1xA@Vi֓iuiFۙ@n1 㞒iՓ2p*]0E*Q6Bi E:>a): I2E ]_sX=Iט"ne})mN0I0mυ)c0y?>odLqJ:S8(f1"1(Lǧ3b_qǧsbS`@eGI&uLrL=Y( р1" 2 uL=SD!:Si`ʾL ezweoc+)#coyB1G N T ]plz0|﷑-LG/zԉ)+L)\8uHd=Siu-u':ԣ%dWn>cZ5j82ESMQːL}Pvd (_n Em(TꘚfJ'>ePLioq/>Ht"-L#G%^1 1<0R923}UďT Gc Е>jZx6XxVPi1E{ NA*[T^cpRbؘ d蘤/vIGaǝ t6_IN^F8z2h8 ØLH=U*+jQF*LE{nJ;]u=2-S{/0) 4$YGzKi]= SVa?FlP,TImL1Ҡ_3nc | Ԙ ԊJ[WPLE n` e"Cl()T)aj)s֋ilcxFZzbOA_Ե)gj֢LQ8"phGEw'sg9ȓϖ@ǣg-J-ecYpʓd&d޴({"!{`I:I#o?)IENDB`data/image/Lightbeam_radio_on.png0000644000000000000000000000570412741721616016040 0ustar rootrootPNG  IHDR44x IDATx՚oTJc*5o}a1+`0x73jx_@&j.di"IZ5jJBҴR$`Я;̹31 H?޹|s""xxsAq}{CQiqt=?lض{Fm.M+tºžo+j Z}^\5w4̚Hgk7=6!lX]4mgXJ/}`JGwT E>[*lZ%xfm(v0VdqWA ޫdWp`lBh(6 PMNWj*;ز`>=|WOvEDCbn_Qm\gy7!vX\V``a07^A.* . DAx_TK JATˋwYxPdUew ֦>'P|8EtR=i/M/R`Ҏ- TUAa@Q UbwZZ B. Ǻ)ODx>RbnPXTs׊xd[Cm*4-e"' ,ƪx xP$*C}KSkiJg&ePLucz[PCո3P`0+z' NT* M󠓩ATRπ~>ۆ%/ާW~KO %aˁIJ D*Fp7b,4О*/PEHբq, )iSj^|z/!JlT#*/Y {a%“w갧l5KL+K]WיpQ4 !֑| *Uxn݊ E=ppsׯϼ..*HP*[*m)MJv&3*yByڴj Ё#񟠷. [7oʍZ82J|z)lj]v8E%j WD`AXȾyܺE7Wn2?? ~vv:5& V;jtaxD{_ )`VVu$&iO'o ځv -2+_q{ ?:1bRDA9-?P$o@\{R1=|v^@?J`f_=] y H:m$HLLv p>āE$9D{whB[<@]qe8i ^I oSRǨg$ -W7B!eqܬosԏ[Y@ڿ H̶{(s '_[Fu ' Mzr8@@bNU4O!L~u>*M\Ryc5V@eP˞\tup PƬߜ}]p'sCc=gAaQ c5X+mP@`*˫1ʹ[:=In`W=<@-}yo'2rW ?Bzhá'MW~ń1U[g:xT/{+¶ũ7zնYu"s*^}Hfם0+J\Z#Tv=?GVɏ'<"uX{dWMk׮ :^#cnvܖrk@mT)(' +pijr>1zaU 0L ej) D:e% ;aS%ħÒ>+*Lw|'dfyV'{\7G@*o vgr}EseS!+ eM( R l`F)Tew_=`jx(+5+ p_ J%X }aPX`ڹ B 彄jX1IENDB`data/image/Lightbeam_radio_off.png0000644000000000000000000000136612741721616016176 0ustar rootrootPNG  IHDR44xIDATx;n@s:n$ dY#RPR7P")UR)t,]7/E;?K{ڛ}P~j[CK=,eKԞ (.&SJ돏YIu ڜ6絀a!8IË[l`padZjOhF;ѱ[vPb_鏯x Qƨ*u9AY}QP˨IJ0)لši04mٽ{gWʋ  mFnk #PU| @fx;:`n+ Ηk9ìk4粓14-,O5) +=81NƓmռ+p6Ѕ9v &>ᬗюETs.; 31ńˎn!(l -rh7<H@1Z`BCb (Urу$@^4$r@@$#%j&Bj!04䐥1Ij) H%fPhg!2 fI$a$aʂZ3$^kݬ*f 9S5 @3udlZ8$71iƂt33e HJIENDB`data/image/Lightbeam---Wordmark-Beta.png0000644000000000000000000000621412741721616016752 0ustar rootrootPNG  IHDRS)PLTEI缽I缽I缽I缽I缽I缽I缽I缽I缽I缽I缽I缽I缽I缽I缽I缽Dps.tRNS 000@@@PPP```ppp zIDATx<ƙLj*J.IdR{*\p{0`~NI":[7dzi>Y, 4vx5_m{˓9rWݮdLxwP>6+Lךi.כfڡf9D3RL;fZh:Ťئ{teY{7xcInț`/VyXט=fgڣ9dO rMAOt)j,Kܡn'˒Բ,=˘z;ɪZe=z]vMe:Gq;eQK\t\>;h- Rgo]~{m:6ƫCuGqdZo%`Lq_5a+ ]%Ӽw6sT<7庲4=FtYuC 5Sc6cwxɠSȻqZb)`!-J}Lg )qqu@;)vV ёP1E͔Χʇ6ct}oWQcI>G17qL *ӔQ!e3j܎1mZ_L؅o)|:@;j^e:E/K'yd# %&(ܑZL//.f-0`b`M!/}^{z,5Z(}E61rW>0ev+ЎuM"u:tTI{6=ҺosKUs9gƿ#im[AP2i17.O KYK(+F֯WLH^,QH[t\r߱^5뭶 Xa_PA1.Yd Ie~i)u)fLZ Z50e2TN)aWd\R2a]G4P3jmwEZp%\5 b$2XAS*,ULq6L*fqx8SxNe*LU_[-2Nf.a;0eŞ=i&3!B 2MZ0 ɢz;%ܰ%M,I?9h4! jK` 5& 4 &!dꪘҼP/]) Ste23EڈJЇ0|Qt4xʤ[ D7;)=9d3EQ[00a'bNS6xR$qO&SL°325ŰZ 8\)d57&1<rS3Y__L&[,7_d KBv3h2vL"b)A@fΧ+F ԤkH4+Sl)d )ƭfh_ioCf! gj^f?\wt &35LL5djL;l_0S!2Ea@)L=nL] )}-.b2- )܇ROx )}=i`H#'S ?\jj?l|/pL&7 )=X0;.ЙϬ2XHF)dgU~>tx `n)=g{`Oo)=oR[7B5{`k`J=0E7`7oU~wLg%%6\)*S#ٴx7 S懱Dt>Sl-wZ0|Og3圷b\)fIg6`j$*Yss'vCHRKM4C͔;NuSDAVc}18۞vLXc4)iRp)R2m=Y2P0E%ark8xz5GJg!}KȺX:PvÔ@,YuR$SL 58RÎd & M`h )mI)$V0LS3FB{SztT'h!dvE;P񿛿/EfsGki(d jɪxbawLoh$rџL7цeIjyRk729'CuKV~--------------oL}P /KU -F=V׽>YhwЪ2- =tDY7CKeٜiYg>3Oo-ܼd<5co>?' /^z͗o`L#AoQe_?D{'SlN{-i>4[2}\_~ǜ ?NaBoQ{6k Fޢ9}C+!EpIbs -|`+_UU}9ӻ$u=?zOQSSLOz*?jޢZJZ{gνխ"-IENDB`data/icons/0000755000000000000000000000000012751274675011620 5ustar rootrootdata/icons/lightbeam_icon_block.png0000644000000000000000000000075512741721616016442 0ustar rootrootPNG  IHDR22)xHPLTE>>>>>>>>>>>*J8VGbUnqA` tRNS 0@P`IIDATxڥᮃ FEq\es0Mg_'ںx~ɲj3Ʈө0:8Xz*P?uX1v6N@Kay CXCw!a($H[^46[ 6J-!*6mڲ1iWe(Tg qEH3kjFZkL~ 1 cdRk4:++E0je חTe, % F0P0,YeI5P nQ }xl(MSjzѨ*غp@_,N_I7ɗ~ˣՁD{WpoqďIENDB`data/icons/lightbeam_150x45.png0000644000000000000000000000762212741721616015206 0ustar rootrootPNG  IHDR-L TYIDATx \TuM|̐Zmmv{Hf#[ԵX]Kl̷##5u5[DdTC  \?ڼ9a~?班SL1SL_4ϼyzQ0NPV)fݴ@A^ \U$P0?{(.o}>XƩ^op 4TB6ky` \5I4z=t::^T}ǓM{ Fnז/ebp29NLG$\*p)fFes#c+ D u1d6^X]4+CF}l;0 :.j>3ڒ )=L`)E=mYYx2?fulaN Vɠ~n`qMK/s Vjs s2&kLZ%?<̧.3r8^' ; #Nf{A;Tdq2/ X /^8t&C \"ͷ qDNcU {kanq5b f-7eEZ,6!gqoI|/溎 mZ)#'1ݨMW aXAY ʅas.Lqsi6 r o=I8t'9U֑d֚viXXhؒrLye j0f;6H< "\jKΥޒL+ךa3 &MQ SCRqI*GeN%3EP6chr݀--ځ\aVUԲ*nAL~9DBp*E̹00t`i,Q9fS2ƺUm M? NnBZy7`Y H\KR2hyf6s(XhUڌ[ RR *!* -<Za7R8[1{V菑![O!#I5, Űؔ"sxo(T8&y7,{e[5荨 Ze-H ˫@qm(UW8M>A>eQTyԾWV鮈3pRIb~vhl;>L?N^-Pר'_%(AP2#3. h.8p[Q{Jвߍ͔G`Mk fJ^</Ⱦw)86~sF!tM^)9)_x0dgMf@J C!VQrLioAbMPg3+k:|B M_|~&MlbYn]a%$+Lj" :ApJ &P !Jh\ crզ,K~9I 9{*hs*|D_-ebEֳƜCC;@őA-Rf(Qmr͔WĒZ.DsD ~q'm;'dN#juva%yec+tkMpR$ JG)LYݛ^5IVц* |ndj`F_q<:I[ k&$6 J\E܃xGhl̰"l`{''-5t7c*tYYqAx+̡!.NyjHuLQ*hEY+'/̍ UU3?ğZ흷6>VuLox*t5*Ƹ8}@D\M܅xEQ"@ɔO(Hb^*l&\ q*i(`R J7څxW/E/g\{TJ+:6cV1*7T,&<=%-ly%j#'1\k/VAsƐ( q̛ op$&P4d9*FaA+ g% [fvǶjXe^9&N//&͑طї-5`7 |CHN Dj':Ai6RlzYeR0ɾpAL^VwfN9 k? mIENDB`data/icons/lightbeam_logo-wordmark_300x150.png0000644000000000000000000002052012741721616020114 0ustar rootrootPNG  IHDR,d[!IDATxڽjSaބ5MiľZIpAD_]"uwspqw'g onL|8TNW' #0JBzC6?Ԛj6GkĵS򘅙uBA&KmqhHBKRC< ճzZЪJ|4 2d5%(F_Vǰ8 ӟ2G\V0 hCO>5$h0/j5X2d i+ j7l« X6X%xYex}KoIԴB=K"5dS=/K)KvzX0UX*܉DphKLEh=e} DYR֛Ԑv!h5[Mu_έIߕ]z,iZZ5[{PR;K`4 KZҐyh j65@MRPgy9 )=[{<kX3ycƜspJ.Sw!l% -ՠϣ 㩡(K1]x؆4rHKT- XVz4XJЄXI:mNM};l9^9gIۖ^w%X1X +E'hT&Vc+*1P-=@n7Wxqp і-^ӒL <i!75,P0`i!-u'^H/Njm@,}~$jXJroOVW9 9Gz+K\fB1Ê^vH+EgjAPall &i0]m֣OmXvӾD2AʴzZ@ Y%D\ B=aAĴ,V_Wլ|2Q@j+Ғ.C*^`ѝ G. ߡzm>B,}L1{&z7~H4Z^|ߤA,zVVXE1Xu"X)VL8%D"~5s&yRGk[и vȁJAYBKL (ײôe㗿EK@J7_xғ}݈Rt/Xjg p)`8ΫRIcgV;? ͦ}SAP V.q TQIF^\"E5v$`,j#}WRBDB&sx|&3`Vp\̍AlJGY}YNB_R_Wu69ڍ(BhEW:/PhJ^Kw4oRzXu @ "XD'!<6]P}F nSlB3QE W]DR1IBTb+ \6hqpC+CK(ċQVZVv~.lSV,[ZȀE,U5:l3u\ dOU=9:]F*`0"O21)p{ns'qU~ vIħ@הl&[iW|&KChKAKL =Ze(fR鴐aZhch!~)GXX\FiD$I STaU,#JMFt\ HN=pk@OZ]Y/>Š*:\$w녤HHa1+QUmIC+ZQ}Z(NF h^ǒWcU+aXXȠƉ 0HuWH= X4RAEH »"4:dƧ!.)@B *=2T8'?O Xw0k|~ w!3ݺ#-+)ޓE\J%SDZQih&R]/sl飄Zzld`5t㋒j;QdUJIJHDrZ63z"{.{A^E8wڑ0Y<] /lzV#gzppQţВH Y-K1BB.UN!`>2\i2@Z XM*3µaJD&*̬^GE/FaJrq0_< Vmy}gn?>HWY-3zТGY6hEY B]c wqeZ-<gXM*MU e 2(&:NDUZRFNN(*A(0qwnnjOu71b\? ~r?DN/5T9Zfh(ղbK|DiKժl.XְRvN_ *e`5PLmVzO1ե(%P fh|5v0 C|[1Æ|H&ڠo Bjzbx2"y<%֮8Z*x *5̝4Jtz.\jE`a\:a0vL?[wLaӗc<&Mc&cf,S0<ҲԴb⩡D.Hk[̵+m)aj}X>/[^Du>2HG#FST54c0qdL2gcx[TPku'Q>s.O5zD 6#8zY1>=+ITK5׳xjRt>]`u%,7!.3Kg"c!y.<`"CBx1^o`)]t4* ) 5{lmrL?yXh)^?XԜo?aޜ=sL1' ʧb|^}Y!xjh,Xn)ePZIJ֕PӴ'8Z>t,HG)Gyvۓm@kXbZ`<-V-[ ka5xrJjr)_2"TxJC^񼽼G5Ye_'%JǀPmjZ=tm4TV+ `Ȓ%C*Cx;aYRbf!y>yVtcAh xsln`~&VF~$> iIRFZFtP_M %0^>H9*?f;оo/'vP7GgkZ̯x. A7IZ&t'(ӽ0<S..lF%8}KP!Cݮ99}: Gr՛&V^4QV5hpR5ȵY Pm֑ݸ[+Šٸ6o 7Ը8<Kqo7KU]q[ FPryVxrs ƮVo.Jʒٳg{g=عm;,Ƭ>#FYI)Ucdn>qV:`MjW-)q0,V{%PTT^"{R >E +SӁxuE%22-:_to*z5*}uHYR?>[FV ?:r/s>I0qlA~ن9ًEW]3e%"jhs5Ȓ‥y#mPbNfX?! %ۖ/,3O~7j< ?z*D*׼!ۤ5&EW)T0RAEVc;!|~j=OصXG2dy% >Ym 3#_( )xS _4!y X(|\I‰䟑sp•ӦLGi![4 vT0-#)*l;PU V!Xu{vm=ϭ{r`2d5Qڦ\K3[dX>;~FRg,H[SJv>;#ov0 ḯZ@)Liq촔%`gb4ML=3M5DKᅽ2;Ӑ6ݫznhlkƮjxXN[!i Lxm'GT1Ww&\U^X嚺UZo~Yfɠ]d,Z-Y]˰%g-s~-/.36 ?ռjd5Ճ /jcLm_}g$c $QEO_S6%?=L^W6}?ƱpV6wE U0mDO`&,S,,B[iz!(N)NCQgl d5?UVw5&>)Q].ixE{DQ^Ku%WIXUNw [^X~_Ak2AIe\z`,&~,NaUeʛ &+FԬޕ!|eV%Q_RZp4r*U[=Ba]턺t\VN$*L;u-|4T4'SnCQLTC&XJfXӾN:C;gW\ϒ۝6f&Aݽ脡} /m?}Weg7%zڵk|Cʿ~il$A{sTN.'|_k6O{QWH]Kٯi>iYv4]uy?KD)%ߝrӶ^S㖘r;[گi,8;).8cvR5Pǭ7* WIENDB`data/icons/lightbeam_logo-only_48x48.png0000644000000000000000000000162112741721616017127 0ustar rootrootPNG  IHDR00` GPLTEUUU@@@fffIII```MMf]]]UUUPP`ZZZUUUYYYUUURR\XXXXXXUU]WWWUU\UU\SSYSSYWW\SSXWW[SSXUUYTT\VVZVVZUUYTT[UU\TTZUU[TTZVVYUU[TTZVVYUUZTTZTTYVV[VV[UUZVV[UUZVVZUUYTT[TTZTTZUU[TTZTTZVVYUUZTTZVV[VV[UUZVVZUUZTTYTT[VVZUUYUUZUUYUUZUUZUUZUUZUUYUUZUUZUUYUUZUUZUUZUU[UUZUUZUU[UUZUUZUUZUUZUUZUUZUUZUUZUUZUUZUUZUUZUUZUUZUUZUUZUUZUUZUUZUUZUUZ4ltRNS  !#$'(./157<=>AEFNOQRSTU\]^dehikoqrs|AIDATxՖSP1+,}3l3[,KF'r;pa\.BˈJH*$x W26> X N# [EecH1j`cPW06aq83) 5 XP7geDL!LN%(xu!F1 AcŴ.XƁ=igơz JD+{kr_p7ū=m?x[5߅L<vޣ9ϗNᏈ:.:Y}蜿M=N%T2Յ@!trhL-d-&!b.'{YdkgEJ/<͇IENDB`data/icons/lightbeam_logo-only_32x32.png0000644000000000000000000000216212741721616017112 0ustar rootrootPNG  IHDR szz9IDATXíoUgv H"GZdatH4EzOJE42(@s(v>9\iwgf? ږ&iDD.!(M^oVD}$!1vwd)d7Q{iѴvt{<[O`}شXѩ3&Ӷ|Z.8ˁhgDhpQī3 ":bl? "ކB_ 5q5!!?süb !@> kFt) ٯ-}Jqb2ׯ ډgعN+AH753TzD_Ds *uQ[0{ò\CZLUiЀCvyxfC]DTsa>/oSޜn^nцAtd&Uf'Fna{T ߫ ܵd{49̼b'2}CPwloy;I'ƭA\aoov4+4 vp{v-?e)?\Df ;!;vA?1ۈ!pѢAI<1NV:ym{p ;\%PLEԠÔR:*}O x+6v""Р%S>Fv6DOI{QDi| CEY'zzs-9BZ 2g~|?#1UX{{hzsIENDB`data/icons/lightbeam_icon_hide.png0000644000000000000000000000204412741721616016252 0ustar rootrootPNG  IHDR22)x8PLTEq:q:q:q:q:q:q:q:q:q:q:q:q:q:q:%')*,01347h}8:[iV>𔔔\=_=hIe<|Yh;xOk;҆`uDn:ޒlq:zFⳛS_kx즄ʵ"nUtRNS 0@P`p#IDATxڍkW0Smk"t*Qp^☂Sp@6M;%I^e4s hCSߤif)J,,+Y-3,(p 5ytTƱ_#3ބC HX@p8r\D@N!R̤v- ";f:s +`h# *<~R5C7,.Hi4zq10He`X.eMnlWdAvK_”n__x5,+K|қ6jh҅WuLc[SBrt"B+K֢00nFD Ly^s,17PM^2tS+}nEbl2n̠˯d _xX^"ξ`|:-ܽ `bXyF~J]>? @(CޱnU+9l">el`A"^` P}\SƟdjr^R!@%ȃ`p:de`P9u 1 KV&ʦB̨~b0r)_墎nD˙%_4*286%晪vIENDB`data/icons/lightbeam_icon_empty_list.png0000644000000000000000000000051012741721616017526 0ustar rootrootPNG  IHDR22)x3PLTEktRNS 0@P`p#IDATx #O5t$ߙfVmy-XY(LmV?$<sX{+i5-0!y:шPDB%7N$R\+ ]銘M lI&d=]%! 8C8pXpPz7SOIENDB`META-INF/0000755000000000000000000000000012751274675010734 5ustar rootrootMETA-INF/mozilla.sf0000644000000000000000000000017112742016652012721 0ustar rootrootSignature-Version: 1.0 MD5-Digest-Manifest: lES9XjDRxeEskpYptGTbEQ== SHA1-Digest-Manifest: fnGmU3M2AGmTDYdiO/owQeOLfU4= META-INF/manifest.mf0000644000000000000000000002365312742016652013064 0ustar rootrootManifest-Version: 1.0 Name: install.rdf Digest-Algorithms: MD5 SHA1 MD5-Digest: j4H6IQWBSpRdDQ6jsgbfnA== SHA1-Digest: iEuWCGc5jRcYA8pPxjmC0KK4vuk= Name: bootstrap.js Digest-Algorithms: MD5 SHA1 MD5-Digest: 7bXGjs354WXP08z1LJmaUQ== SHA1-Digest: hBvzrYsRacNpTOCOH+GOhYBn2mE= Name: package.json Digest-Algorithms: MD5 SHA1 MD5-Digest: Qyeb/FH9XAFEm2tf5YpsHQ== SHA1-Digest: bpOATF0xCkY8FVDP4Mjxj4A1nXE= Name: data/aggregate.js Digest-Algorithms: MD5 SHA1 MD5-Digest: 7Qh1QQSHKCi7RT9PFx6jEw== SHA1-Digest: 1suCRjUKU66g/V7R68DldaOh1eo= Name: data/content-script.js Digest-Algorithms: MD5 SHA1 MD5-Digest: /XnlNCIXe669PXq4vG8ukg== SHA1-Digest: lBnOUSSS2Z8hkuQPj5qmBGuJtdU= Name: data/dialog.js Digest-Algorithms: MD5 SHA1 MD5-Digest: HZacRv89pmZ/2u73t1jLfA== SHA1-Digest: BHEIwKqo4t13yJ3XUMUL17Zj1aI= Name: data/events.js Digest-Algorithms: MD5 SHA1 MD5-Digest: U8iHfXeQsdYUAUR06KicOA== SHA1-Digest: 2bKYOzVDFSYOXi+zKvDoBSpt574= Name: data/first-run.css Digest-Algorithms: MD5 SHA1 MD5-Digest: X2wdSRbS+9ncX7Pe1CkZcQ== SHA1-Digest: stnwGdhZR4NjOUC38yGYdSx8MB8= Name: data/first-run.html Digest-Algorithms: MD5 SHA1 MD5-Digest: HxdtRPUMwUQQQqSeFMbkPg== SHA1-Digest: 3oMytMsU6eGjTK+vKACawFp7+p8= Name: data/font-awesome.css Digest-Algorithms: MD5 SHA1 MD5-Digest: YpJTdfW0//R5giWoy9n+Og== SHA1-Digest: ZI/TPuI0ZpB3INSLyrBibLUqsPI= Name: data/graph.js Digest-Algorithms: MD5 SHA1 MD5-Digest: YxBUJ1DfFXPF9/xk0I7o1Q== SHA1-Digest: LHzq7uaZTVT2k1SzkIVFIogWeg8= Name: data/index.html Digest-Algorithms: MD5 SHA1 MD5-Digest: DXP3Ae/3aN15Gawb1a+jUA== SHA1-Digest: zWpcawG39i7ad0RjHfvsAMxqOjY= Name: data/infobar.js Digest-Algorithms: MD5 SHA1 MD5-Digest: 34MDbpAnrGzOqA5iS0BjRQ== SHA1-Digest: UWvSnDV57TTYiwTNM1cO3fMP5eI= Name: data/initialPage.js Digest-Algorithms: MD5 SHA1 MD5-Digest: R6OfJLv05hD+zWqjxDP5Sg== SHA1-Digest: 2HA3AoxbryoLLVnqbdatlDUmZyA= Name: data/lightbeam.js Digest-Algorithms: MD5 SHA1 MD5-Digest: 5DO7QocmG2aWpG/Unv70WQ== SHA1-Digest: y27DQQl236vLKxnL5jWxK1Vr8as= Name: data/list.js Digest-Algorithms: MD5 SHA1 MD5-Digest: B/ZJJ90FVyvzEJ0A17Yfcg== SHA1-Digest: ofvaTitsObnHXEVg6MUeKcRZy/s= Name: data/map.svg Digest-Algorithms: MD5 SHA1 MD5-Digest: M/NaeHe1SDa2LYuL+y1XlA== SHA1-Digest: AFCjn24b46gsBGIfKSwngJteL/o= Name: data/OpenSans.css Digest-Algorithms: MD5 SHA1 MD5-Digest: MZaWGyvm3JBCXU9IirM/cA== SHA1-Digest: n572xPpke/DBhPxSouxq4724LPc= Name: data/parseuri.js Digest-Algorithms: MD5 SHA1 MD5-Digest: nhQeQYrMRfTe/RshKXrkTA== SHA1-Digest: yf9ZOZz1W/ttQLvazJMbXC2fYAE= Name: data/style.css Digest-Algorithms: MD5 SHA1 MD5-Digest: kluOe9Tb/Gp5Eor05K9F4g== SHA1-Digest: yWOoDPBTH31wI8W/FazXh8zPNks= Name: data/svgdataset.js Digest-Algorithms: MD5 SHA1 MD5-Digest: 4dQGZw3ap6X8E26Kg6TI2A== SHA1-Digest: YIZ2TlwtNTGcvHQV46TVlLv6J+4= Name: data/tooltip.js Digest-Algorithms: MD5 SHA1 MD5-Digest: Qiq/9tOm1ji+t/fpIXPfyg== SHA1-Digest: QZZYuD2jlmScyMc68HtUomkViyM= Name: data/ui.js Digest-Algorithms: MD5 SHA1 MD5-Digest: 9gXCLNcqS5Uyn5qYGZwukw== SHA1-Digest: g//Wa281tkMdY1dPYz0tV1+XapE= Name: data/upgrade.html Digest-Algorithms: MD5 SHA1 MD5-Digest: j397lEHCZgdxNybaVbeG3A== SHA1-Digest: EJM7XyM0MCB5LcHEWTfC/a2PiwQ= Name: data/d3/d3.js Digest-Algorithms: MD5 SHA1 MD5-Digest: wcl+JjfuM+kVuaJg7BkOGA== SHA1-Digest: QZ5KxQLWwGdWh4fc7Ejg+WzI7gQ= Name: data/d3/LICENSE Digest-Algorithms: MD5 SHA1 MD5-Digest: XkTmO3SR7EsY9z0bODNKBA== SHA1-Digest: WaLdafKXr9AGkxOtH20o+oNB4rk= Name: data/font/fontawesome-webfont.woff Digest-Algorithms: MD5 SHA1 MD5-Digest: IfIS+UqdtqDjhHySGEKqGQ== SHA1-Digest: HwvcWKpZq5VM54qU5NDqlKtDZVQ= Name: data/font/LICENSE Digest-Algorithms: MD5 SHA1 MD5-Digest: O4Pvljh/FGVfyFTdw8a9Vw== SHA1-Digest: K4uBUimqimHkg/tLoFiLi2xJGJA= Name: data/font/OpenSans-Bold.ttf Digest-Algorithms: MD5 SHA1 MD5-Digest: UBRWhQQrTfB6H9GZVydbgQ== SHA1-Digest: wWkegWiyWWr4oAFiusYNvmBenjY= Name: data/font/OpenSans-Light.ttf Digest-Algorithms: MD5 SHA1 MD5-Digest: G/cb4REYnnaYeku5sxFctw== SHA1-Digest: QEQsGJVoGEtubCeiXWnxTZG2UDk= Name: data/font/OpenSans-LightItalic.ttf Digest-Algorithms: MD5 SHA1 MD5-Digest: aUP7b9QgDz0HNGkyXGrNyQ== SHA1-Digest: c8RzCJTGeQDgt716SRxxfUojazA= Name: data/icons/lightbeam_150x45.png Digest-Algorithms: MD5 SHA1 MD5-Digest: KzSEm01NWAaDZPHsiv4FZA== SHA1-Digest: /zkDSlSkMA7kqC6/HOnUoQGw+J4= Name: data/icons/lightbeam_icon_block.png Digest-Algorithms: MD5 SHA1 MD5-Digest: L15JyU6lYBP5YxatV2Ersg== SHA1-Digest: WGA3793tH2McHPtnFaNv3M2Mcd8= Name: data/icons/lightbeam_icon_empty_list.png Digest-Algorithms: MD5 SHA1 MD5-Digest: gDBbbeJeoYH7vp/fJLW6zA== SHA1-Digest: PZSAiubUCCVqRSyOFvyYcO2eSuo= Name: data/icons/lightbeam_icon_hide.png Digest-Algorithms: MD5 SHA1 MD5-Digest: C9V9ADEfXnfT0TSz+b3uKA== SHA1-Digest: 3z8Kwl+/B0ZwsrFDTIi2uTnQSEo= Name: data/icons/lightbeam_icon_unblock.png Digest-Algorithms: MD5 SHA1 MD5-Digest: btqRiN6Jcx8X6KqllZa7Jw== SHA1-Digest: GXqmc9vfPFSqIFShmCniEoytsD8= Name: data/icons/lightbeam_icon_watch.png Digest-Algorithms: MD5 SHA1 MD5-Digest: Nl4vc+ooNZm+RXjWLIOjFA== SHA1-Digest: OpU3KySoVJIMlnetYcAHATLxOgo= Name: data/icons/lightbeam_logo-only_16x16.png Digest-Algorithms: MD5 SHA1 MD5-Digest: hLpc4bHT9VxSj9N/0+T3Yg== SHA1-Digest: HUOKVZADwrDp2Redh8lFLMOzNd0= Name: data/icons/lightbeam_logo-only_32x32.png Digest-Algorithms: MD5 SHA1 MD5-Digest: SDfHyltTmUrEbqutUpf+VQ== SHA1-Digest: xIKHYuyRCcEK7BFxoHY4pOdneOs= Name: data/icons/lightbeam_logo-only_48x48.png Digest-Algorithms: MD5 SHA1 MD5-Digest: 8aUV1SxQhWSkN3txqTmoUA== SHA1-Digest: 0dBuGQOC8HTM9gvbF3jUbfacZaE= Name: data/icons/lightbeam_logo-wordmark_300x150.png Digest-Algorithms: MD5 SHA1 MD5-Digest: 29a3D/GUjR5N+i09QjaNVg== SHA1-Digest: Ayex+YrYZZqxaqFPwcYaNX/XqlU= Name: data/image/Lightbeam---Wordmark-Beta.png Digest-Algorithms: MD5 SHA1 MD5-Digest: QGQcX9ZIeL/nyn3HGVpKAw== SHA1-Digest: x1qLPRAv5tvIwTRx3Hy0nG+ThmI= Name: data/image/lightbeam__wordmark_temp.png Digest-Algorithms: MD5 SHA1 MD5-Digest: YjbfG3v664KN/5qIC22agQ== SHA1-Digest: 5Fp2fGu1mjDFxMdhtVMh+sggbcI= Name: data/image/lightbeam_icon_about.png Digest-Algorithms: MD5 SHA1 MD5-Digest: cn4kQDxBangO/x5nWZNh+g== SHA1-Digest: ASvXROHWMEbpk1jFZrs6CtqWQkM= Name: data/image/lightbeam_icon_checkbox.png Digest-Algorithms: MD5 SHA1 MD5-Digest: 2qYQ0cY1sOdn2L2Ufc9NfQ== SHA1-Digest: Su6OeBvLHyefaPGwQXjL9X0KZjU= Name: data/image/lightbeam_icon_download2.png Digest-Algorithms: MD5 SHA1 MD5-Digest: UedcbaApavtzPK5Po6uYDQ== SHA1-Digest: NwfqF6oGJJUAflxJZkRveKnVEn8= Name: data/image/lightbeam_icon_feedback.png Digest-Algorithms: MD5 SHA1 MD5-Digest: deIqVoERHtxmtkjRIjWLyA== SHA1-Digest: IMlkiSSDj+tQx5QahAlv3U4fwKo= Name: data/image/lightbeam_icon_graph.png Digest-Algorithms: MD5 SHA1 MD5-Digest: 1yslXnx774SYaa55jpE0+Q== SHA1-Digest: BjfYHGpQfnPmUbe16pSyxseJ6mQ= Name: data/image/lightbeam_icon_help.png Digest-Algorithms: MD5 SHA1 MD5-Digest: l5apgR8yYSJsVNNMidz9/g== SHA1-Digest: gl40eGgclW7mpa37qlitZqwncvI= Name: data/image/lightbeam_icon_list.png Digest-Algorithms: MD5 SHA1 MD5-Digest: SqX8Ero+fHJp7qk/snXPEQ== SHA1-Digest: O/lEn9ayt7o1lhQ24Xo8Ww0Bks4= Name: data/image/lightbeam_icon_list_blue.png Digest-Algorithms: MD5 SHA1 MD5-Digest: cEKX9ustR/fD9hjxB0MvPA== SHA1-Digest: JOJ7OJJKzB7tA7L2qTC87cjUATI= Name: data/image/lightbeam_icon_reset.png Digest-Algorithms: MD5 SHA1 MD5-Digest: U5KIS46dVWBRPNUcEXaKOg== SHA1-Digest: vqJFWkYisnWc2CKRZC6W/7AqFOk= Name: data/image/lightbeam_icon_sortby.png Digest-Algorithms: MD5 SHA1 MD5-Digest: 5VwOMV7Fb0oyXIPJOwSauQ== SHA1-Digest: XuiZOOSfeA3NeHEyeApAmQP6n0E= Name: data/image/lightbeam_icon_website.png Digest-Algorithms: MD5 SHA1 MD5-Digest: knssW35KUQsNBKyRDXJhnA== SHA1-Digest: x+GRGSrFXvB6+w8QEhGatpa5pBc= Name: data/image/lightbeam_logo-only.svg Digest-Algorithms: MD5 SHA1 MD5-Digest: ZLvNTC0vZbjU6kx3lWSJ+w== SHA1-Digest: SAD3SQKsD0xWdEY0S3uv1kWjT5w= Name: data/image/lightbeam_popup_blocked.png Digest-Algorithms: MD5 SHA1 MD5-Digest: YJQE8pJY1u4Qd8BRMPfS2Q== SHA1-Digest: xxq4y2l9PWMkIP0pWIeVgg+mB64= Name: data/image/lightbeam_popup_hidden.png Digest-Algorithms: MD5 SHA1 MD5-Digest: YAtkkwiGabUJjEgZGrLSkA== SHA1-Digest: fX4zawopBtu+1y9uD4OGR9WwsHc= Name: data/image/lightbeam_popup_privacy.png Digest-Algorithms: MD5 SHA1 MD5-Digest: NRjIurviXdCaAIaDgBO1Aw== SHA1-Digest: 773ht5ptzqjSPoZe91pkDverLaE= Name: data/image/lightbeam_popup_warningreset.png Digest-Algorithms: MD5 SHA1 MD5-Digest: Snu4O/5vaNcX3KSsixxniw== SHA1-Digest: 7+dO5/iXS6vGQxvLjzEvvv414Bw= Name: data/image/Lightbeam_radio_off.png Digest-Algorithms: MD5 SHA1 MD5-Digest: zYZ5RxhqQoqnM57dlN9HYw== SHA1-Digest: N/nshp7fM/Sd4CeRAYjvxpv30O4= Name: data/image/Lightbeam_radio_on.png Digest-Algorithms: MD5 SHA1 MD5-Digest: kn8bR+9YZU9hrvb8JdF2Rg== SHA1-Digest: ZPdWFzQT7e58nVcar3RXKfPX6Z4= Name: data/picoModal/LICENSE.md Digest-Algorithms: MD5 SHA1 MD5-Digest: 7oUKjC73Of+TuNJbS5DP9Q== SHA1-Digest: Rz7pkrjdBOoJNtuOXvKBvWnHsfQ= Name: data/picoModal/picoModal.js Digest-Algorithms: MD5 SHA1 MD5-Digest: 4/JumKKWsP/WiI3qTf4wnQ== SHA1-Digest: fK4GE1z8zdHD7862is/JggENCkw= Name: lib/connection.js Digest-Algorithms: MD5 SHA1 MD5-Digest: +7x7ga7oi4zQLHU4s0kXSg== SHA1-Digest: PyroCUc9TacApHoxqdnKVR9OctU= Name: lib/main.js Digest-Algorithms: MD5 SHA1 MD5-Digest: 6yc1Sofjjh3y3cCm1Cs92w== SHA1-Digest: KSznPbWHEgaJ4eEB7rIEWlJFvcM= Name: lib/persist.js Digest-Algorithms: MD5 SHA1 MD5-Digest: lD+HXBuQ8wlUFqGIyC6h2A== SHA1-Digest: 4vhb1MbIg91aowUZkFWe0ykgDkk= Name: lib/ui.js Digest-Algorithms: MD5 SHA1 MD5-Digest: WKggwFae/IHLiuJBlosuqw== SHA1-Digest: j/ZMojqDtgdZj3Tw4IqRYXVEee0= Name: lib/shared/menuitems.js Digest-Algorithms: MD5 SHA1 MD5-Digest: tg5U30VT7gB3B4kz/wawVg== SHA1-Digest: wPjwLn+Pm0T+CWjU5YzI4+hW96c= Name: lib/shared/policy.js Digest-Algorithms: MD5 SHA1 MD5-Digest: YN7sLz8oUGrk2KinwuKaiA== SHA1-Digest: nQhppBYwX5ioXHv4SecHueIvAz0= Name: lib/shared/unload+.js Digest-Algorithms: MD5 SHA1 MD5-Digest: MNRpqFaw1RJvmUDNcfqRvQ== SHA1-Digest: xt7dGaVXRrGad0t2QYvuy+h5NLU= Name: lib/tab/events.js Digest-Algorithms: MD5 SHA1 MD5-Digest: j+TKVuIwwZ4wlZUpqDXOhw== SHA1-Digest: dE1PmywHfWgY0uFBNar40tBN1z8= Name: lib/tab/utils.js Digest-Algorithms: MD5 SHA1 MD5-Digest: A2CIt4TPF3XuWARLp/uRvQ== SHA1-Digest: UBDqb2jCZgLqh3xw0YE5R6ibuZs= META-INF/mozilla.rsa0000644000000000000000000001013012742016652013072 0ustar rootroot0T *H E0A1 0 +0  *H  00Uwk0  *H  01 0 UUS10U Mozilla Corporation1/0-U &Mozilla AMO Production Signing Service110/U(production-signing-ca.addons.mozilla.org1402 *H  %services-ops+addonsigning@mozilla.com0 160714223921Z 210713223921Z0~10U  Production1 0 UUS10U Mountain View10 U Addons1 0 UCA1$0"Ujid1-F9UJ2thwoAm5gQ@jetpack0"0  *H 0 n.ႄ0:[Z[sOd3.*0<]4La\Fv\NU`7KZi܊KgLˌ`QwNRR{,(ōVV\FZqF{y}bG]Ӫb)gѰ_eöۏzpI@t1 ō4^W gDv5uzkr1bկ 3c1NG`~ZiU^APo -|rX荌2 >y3H0s9,rvZ\@Jdo7o 5.PBPځ[j1^w߆8R.qY6=uF0:\BjN-?R'fa#a+Вh_Fc}LޠTkFA>Ъ퉭5iOa%^GDN *q\8 Ýn@:Gp^A4P6(x 0  *H  k[wE/Rǭ9n,4<޽P/KB#j^>l1VgDs K[fnrCc-;sVɔ{ŢݙKsa`ɒ?S-W"@E|dD!NR!aH[:6p ` .|L3l9{.!R fpmN g랗C&[`ah*~vCm!ںE6{:EEBbu9VsnUFNF̋Mk7oԤ"GLnT8Y8c(:TwE`w\ ,w A\SQ4*=V=o>d?`+qZOd\@hwK4#1P`(Xt|K&zzVV`,[z(g!8BL:DCcF`쨡LG000  *H  0}1 0 UUS10U Mozilla Corporation1/0-U &Mozilla AMO Production Signing Service10Uroot-ca-production-amo0 150317235242Z 250314235242Z01 0 UUS10U Mozilla Corporation1/0-U &Mozilla AMO Production Signing Service110/U(production-signing-ca.addons.mozilla.org1402 *H  %services-ops+addonsigning@mozilla.com0"0  *H 0 3ٶ #cSs)gH~̼Ƚf4߹jM`NwM+" ۏBa< 7&J²<v1e)+o# T!b]wzHKUǛko< xȎ򉾢~,SLgncV}hDg4u2/{^HloR& .)'ޅIe)@HQ~,p5+8fF $>Vg#zYzOXv!Ѐ/ѸʙXeiPU=t_7-{+5z)r#6rՆpeJf_,}{"G :};fYH/KPqK7w;`|c> .oq[_4?,}-U]xinTσu!0޹>5}-_;NUq1W0S001 0 UUS10U Mozilla Corporation1/0-U &Mozilla AMO Production Signing Service110/U(production-signing-ca.addons.mozilla.org1402 *H  %services-ops+addonsigning@mozilla.comUwk0 +]0 *H  1  *H 0 *H  1 160714223921Z0# *H  1s_#7;G/rLax0  *H I /Wl4aF 4bCfzͧ4/ p5?V?f#{I@ƅHit},^\Gb1L7OR?&d8!-'PnMཛྷ״APni>tdK}8E'/qN(K8R;a8&Žw&3, <ލ`~o,Y0qҮ.Xԣ7fmt"mv lFk{U)zO2g cEU7`B/T!ih !J͍ҕhz|(KdV=̲u6q&n B_G.l5=iJTyJ(V]D9gX(Sլetٞyg[OX͕ 7t)uʔ=㆑>/:Rѱ&ܷi%Eo6_͞BBvsvNλ T_梧t{X i2G/Z