package.json 0000644 0000000 0000000 00000002527 12741721616 012057 0 ustar root root {
"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.rdf 0000644 0000000 0000000 00000002451 12741721616 011730 0 ustar root root
jid1-F9UJ2thwoAm5gQ@jetpack2truefalse1.3.1LightbeamLightbeam 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 Foundationdata:text/xml,<placeholder/>2true{ec8030f7-c20a-464f-9b0e-13a3a9e97384}38.0a143.0
bootstrap.js 0000644 0000000 0000000 00000001121 12741721616 012131 0 ustar root root /* 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/ 0000755 0000000 0000000 00000000000 12751274675 010342 5 ustar root root lib/tab/ 0000755 0000000 0000000 00000000000 12741721616 011077 5 ustar root root lib/tab/utils.js 0000644 0000000 0000000 00000011327 12741721616 012601 0 ustar root root /* 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.js 0000644 0000000 0000000 00000001061 12741721616 012737 0 ustar root root /* 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/ 0000755 0000000 0000000 00000000000 12741721616 011577 5 ustar root root lib/shared/unload+.js 0000644 0000000 0000000 00000006222 12741721616 013474 0 ustar root root /* 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.js 0000644 0000000 0000000 00000010367 12741721616 013443 0 ustar root root /* 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.js 0000644 0000000 0000000 00000014565 12741721616 014156 0 ustar root root /* 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.js 0000644 0000000 0000000 00000017736 12741721616 011322 0 ustar root root /* 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.js 0000644 0000000 0000000 00000004473 12741721616 012370 0 ustar root root /* 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.js 0000644 0000000 0000000 00000004452 12741721616 011620 0 ustar root root /* 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.js 0000644 0000000 0000000 00000022702 12741721616 013031 0 ustar root root /* 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/ 0000755 0000000 0000000 00000000000 12751274675 010505 5 ustar root root data/list.js 0000644 0000000 0000000 00000051530 12741721616 012011 0 ustar root root /* 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.js 0000644 0000000 0000000 00000014365 12741721616 012777 0 ustar root root /* 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.js 0000644 0000000 0000000 00000000324 12741721616 013257 0 ustar root root var openLightbeamButton = document.querySelector('button#openLightbeam');
if (openLightbeamButton) {
openLightbeamButton.addEventListener('click', function () {
self.port.emit('openLightbeam', {});
});
}
data/infobar.js 0000644 0000000 0000000 00000031261 12741721616 012455 0 ustar root root /* 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.html 0000644 0000000 0000000 00000103266 12741721616 012501 0 ustar root root
Lightbeam
DATA GATHERED SINCE
YOU HAVE VISITED
YOU HAVE CONNECTED WITH
TRACKING PROTECTION
Graph View
data/graph.js 0000644 0000000 0000000 00000030603 12741721616 012135 0 ustar root root /* 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.css 0000644 0000000 0000000 00000051575 12741721616 013627 0 ustar root root /*!
* 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.html 0000644 0000000 0000000 00000004146 12741721616 013320 0 ustar root root
Welcome to Lightbeam!
Thanks for downloading Lightbeam!
Open the Lightbeam tab by clicking the logo in the Add-on bar.
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
Don't forget to leave a review on our Mozilla Addons page!
data/first-run.css 0000644 0000000 0000000 00000002473 12741721616 013145 0 ustar root root /* 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.js 0000644 0000000 0000000 00000003001 12741721616 012330 0 ustar root root /* 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.js 0000644 0000000 0000000 00000020176 12741721616 012277 0 ustar root root /* 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 += "
";
}
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.
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_blocked.png"
}, callback);
}
data/content-script.js 0000644 0000000 0000000 00000003275 12741721616 014015 0 ustar root root /* 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.js 0000644 0000000 0000000 00000040553 12741721616 012767 0 ustar root root /* 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.css 0000644 0000000 0000000 00000001174 12741721616 012737 0 ustar root root /* 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/ 0000755 0000000 0000000 00000000000 12741721616 012403 5 ustar root root data/picoModal/picoModal.js 0000644 0000000 0000000 00000030443 12741721616 014654 0 ustar root root /**
* 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.md 0000644 0000000 0000000 00000002116 12741721616 014007 0 ustar root root The 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/ 0000755 0000000 0000000 00000000000 12751274675 011453 5 ustar root root data/d3/ 0000755 0000000 0000000 00000000000 12751274675 011013 5 ustar root root data/upgrade.html 0000644 0000000 0000000 00000003767 12741721616 013026 0 ustar root root
Welcome to Lightbeam!
Thanks for upgrading Lightbeam!
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.
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