├── .gitignore ├── images ├── wrench.png ├── stats-256.png ├── carrot-19px.png ├── promo-image.png ├── hamburger-19px.png ├── hamburger-128px.png ├── carrot-inactive-19px.png ├── hamburger-inactive-19px.png └── promo-image.svg ├── background.html ├── popup.css ├── package.sh ├── TODO ├── stats.css ├── manifest.json ├── LICENSE ├── stats.html ├── popup.html ├── popup.js ├── README.md ├── options.css ├── flot ├── jquery.flot.selection.min.js └── jquery.min.js ├── options.html ├── lib.js ├── dimmer.js ├── stats.js ├── options.js └── background.js /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | crackbook.zip 4 | -------------------------------------------------------------------------------- /images/wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gintas/crackbook/HEAD/images/wrench.png -------------------------------------------------------------------------------- /images/stats-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gintas/crackbook/HEAD/images/stats-256.png -------------------------------------------------------------------------------- /images/carrot-19px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gintas/crackbook/HEAD/images/carrot-19px.png -------------------------------------------------------------------------------- /images/promo-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gintas/crackbook/HEAD/images/promo-image.png -------------------------------------------------------------------------------- /images/hamburger-19px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gintas/crackbook/HEAD/images/hamburger-19px.png -------------------------------------------------------------------------------- /images/hamburger-128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gintas/crackbook/HEAD/images/hamburger-128px.png -------------------------------------------------------------------------------- /images/carrot-inactive-19px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gintas/crackbook/HEAD/images/carrot-inactive-19px.png -------------------------------------------------------------------------------- /images/hamburger-inactive-19px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gintas/crackbook/HEAD/images/hamburger-inactive-19px.png -------------------------------------------------------------------------------- /background.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-width: 300px; 3 | overflow-x:hidden; 4 | font-family: Georgia; 5 | white-space: nowrap; 6 | } 7 | 8 | img.icon { 9 | width: 16px; 10 | height: 16px; 11 | border: 0; 12 | } 13 | 14 | span#current_domain { 15 | font-weight: bold; 16 | } 17 | -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git --no-pager grep console.log | grep -v package.sh 4 | git --no-pager grep TODO | grep -v package.sh 5 | git --no-pager grep XXX | grep -v package.sh 6 | 7 | rm -f crackbook.zip 8 | zip crackbook.zip manifest.json LICENSE README.md *.html *.js *.css images/*.png flot/*.js 9 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * Blog post 2 | * Whitelist 3 | . Personal statistics 4 | ========= 5 | * Cooldown period for the ban screen 6 | + Option for non-translucent background. 7 | * Configurable "Back to work" interval 8 | * Configurable origin tracking 9 | * More robust origin tracking 10 | * History scanning for statistics 11 | -------------------------------------------------------------------------------- /stats.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 1em; 3 | font-family: Verdana, sans-serif; 4 | } 5 | 6 | h1, h2 { 7 | color: #225; 8 | font-weight: bold; 9 | } 10 | 11 | #logPlot-container { 12 | padding-bottom: 2em; 13 | } 14 | 15 | .zoomout { 16 | padding: 1em; 17 | font-size: small; 18 | color: #999; 19 | } 20 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Crackbook", 3 | "version": "0.2.9", 4 | "manifest_version": 2, 5 | "description": "Limit junk sites in your daily infodiet.", 6 | "icons": { 7 | "128": "images/hamburger-128px.png" 8 | }, 9 | "browser_action": { 10 | "default_icon": "images/carrot-19px.png", 11 | "default_title": "Crackbook", 12 | "default_popup": "popup.html" 13 | }, 14 | "background": { 15 | "page": "background.html" 16 | }, 17 | "options_page": "options.html", 18 | "content_scripts": [ 19 | { 20 | "matches": ["*://*/*"], 21 | "js": ["dimmer.js"], 22 | "run_at": "document_start" 23 | } 24 | ], 25 | "content_security_policy": "script-src 'self' https://ssl.google-analytics.com; object-src 'self'", 26 | "permissions": [ 27 | "history", 28 | "notifications", 29 | "tabs", 30 | "*://*/*" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Gintautas Miliauskas26 | 30 |
31 | 32 |
33 |
34 |
35 | Configure...
36 |
37 |
40 |
41 |
42 | Statistics
43 |
44 |
80 | 81 |
82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /lib.js: -------------------------------------------------------------------------------- 1 | var DAY_STARTING_HOUR = 6; // AM 2 | 3 | // 4 | // Helper functions. 5 | // 6 | 7 | function trimProtocol(url) { 8 | var p = url.indexOf('://'); 9 | if (p > -1) { 10 | url = url.slice(p + '://'.length); 11 | } 12 | return url; 13 | } 14 | 15 | function trimWWW(url) { 16 | if (url.slice(0, 4) == 'www.') 17 | url = url.slice(4); 18 | return url; 19 | } 20 | 21 | function trimPath(url) { 22 | p = url.indexOf('/'); 23 | if (p > -1) 24 | domain = url.slice(0, p); 25 | else 26 | domain = url; 27 | return domain; 28 | } 29 | 30 | // Returns today's date as an ISO8601 string (e.g., 2011-03-04) 31 | function todayAsString() { 32 | var now = new Date(); 33 | var dt = new Date(now - DAY_STARTING_HOUR * 3600 * 1000); 34 | return dt.getFullYear() + "-" + lzero(dt.getMonth() + 1) + "-" + lzero(dt.getDate()); 35 | } 36 | 37 | // 38 | // Local storage functions. 39 | // 40 | 41 | var_defaults = { 42 | junkDomains: '[]', 43 | reporting: 'true', 44 | checkActiveTab: 'true', 45 | user_id: '0', 46 | dimmerThreshold: '0', 47 | dimmerDelay: '3.0', 48 | dimmerDelayIncrement: '0.05', 49 | redirectProbability: '2', 50 | startTime: '0', 51 | endTime: '' + 24*60-1, 52 | weekdays: '"12345"', 53 | day: '""', 54 | dayHits: '0', 55 | hitLogKeys: '[]', 56 | reset_daily_flag: 'false', 57 | base_delay: '10.0' 58 | }; 59 | 60 | function getLocal(varname) { 61 | if (!(varname in localStorage)) { 62 | localStorage.setItem(varname, var_defaults[varname]); 63 | } 64 | var s = localStorage.getItem(varname); 65 | return JSON.parse(s); 66 | } 67 | 68 | function setLocal(varname, value) { 69 | localStorage.setItem(varname, JSON.stringify(value)); 70 | } 71 | 72 | function storeHit(domain, blocked, active) { 73 | var dt = new Date(); 74 | 75 | // Create an entry. 76 | var timestamp = Math.round(dt.getTime() / 1000); 77 | var entry = JSON.stringify({ domain: domain, blocked: blocked, timestamp: timestamp, active: active }); 78 | 79 | var key = 'hitLogKeys-' + (dt.getYear() - 100) + "-" + (dt.getMonth() + 1); 80 | 81 | // Append the entry to the right collection. 82 | var hitLogKeys = getLocal('hitLogKeys'); 83 | var row = null; 84 | if (hitLogKeys.indexOf(key) == -1) { 85 | hitLogKeys.push(key); 86 | setLocal('hitLogKeys', hitLogKeys); 87 | row = entry; 88 | } else { 89 | row = localStorage.getItem(key) + "," + entry; 90 | } 91 | // Access localStorage directly to avoid double stringification. 92 | localStorage.setItem(key, row); 93 | } 94 | 95 | function loadHits(key) { 96 | return JSON.parse('[' + localStorage.getItem(key) + ']'); 97 | } 98 | 99 | function getTodaysHits() { 100 | if (getLocal('day') == todayAsString()) { 101 | return getLocal('dayHits'); 102 | } else { 103 | return 0; 104 | } 105 | } 106 | 107 | function lookupJunkDomain(url) { 108 | var junkDomains = getLocal('junkDomains'); 109 | var normalized_url = trimWWW(trimProtocol(url.trim())); 110 | for (var i = 0; i < junkDomains.length; i++) { 111 | if (normalized_url.indexOf(junkDomains[i]) == 0) { 112 | return junkDomains[i]; 113 | } 114 | } 115 | return null; 116 | } 117 | 118 | function bgPage() { 119 | return chrome.extension.getBackgroundPage(); 120 | } 121 | 122 | function lzero(x) { 123 | // Add a leading zero to a number. 124 | return x > 9 ? x : '0' + x; 125 | } 126 | 127 | function renderTime(minutes) { 128 | return lzero((Math.floor(minutes / 60))) + ':' + lzero(minutes % 60); 129 | } 130 | 131 | function parseTime(s) { 132 | var components = s.split(':'); 133 | var hr = parseInt(components[0], 10) | 0; 134 | if (hr < 0 || hr >= 24) hr = 0; 135 | var min = parseInt(components[1], 10) | 0; 136 | if (min < 0 || min >= 60) hr = 0; 137 | var r = hr * 60 + min; 138 | if (r >= 60*24) 139 | r = 0; 140 | return r; 141 | } 142 | 143 | function pickRandomItem(items) { 144 | return items[Math.floor(Math.random() * items.length)]; 145 | } 146 | -------------------------------------------------------------------------------- /dimmer.js: -------------------------------------------------------------------------------- 1 | var BODY_POLL_MS = 30; 2 | var DIMMER_DIV_ID = '_crackbook_dimmer_'; 3 | var DIMMER_TEXT = "Wait %d seconds for the content to appear."; 4 | var DIMMER_SWITCH_TEXT = "The timer restarts if you switch away from this tab."; 5 | 6 | var TRACING = false; 7 | 8 | var original_url = null; 9 | var dimmer_delay = null; 10 | var dimmer_appearance = null; 11 | 12 | function clearDimTimer(dimmer) { 13 | // Clear old timer. 14 | var timerIdInput = document.getElementById(DIMMER_DIV_ID + "timerId"); 15 | if (timerIdInput) { 16 | timerId = parseInt(timerIdInput.value); 17 | clearTimeout(timerId); 18 | dimmer.removeChild(timerIdInput); 19 | } 20 | } 21 | 22 | function setDimTimer(dimmer, delay) { 23 | // Clear old timer. 24 | var timerIdInput = document.getElementById(DIMMER_DIV_ID + "timerId"); 25 | if (timerIdInput) { 26 | timerId = parseInt(timerIdInput.value); 27 | clearTimeout(timerId); 28 | } 29 | 30 | var timeoutFn = function() { 31 | var dimmer = document.getElementById(DIMMER_DIV_ID); 32 | dimmer.style.display = "none"; 33 | // Disable the dimmer. This page may be dimmed again if the URL changes. 34 | }; 35 | 36 | // Set timer. 37 | var timerId = setTimeout(timeoutFn, Math.round(delay * 1000)); 38 | 39 | // Store timer ID. 40 | if (!timerIdInput) { 41 | var timerIdInput = document.createElement("input"); 42 | timerIdInput.id = DIMMER_DIV_ID + "timerId"; 43 | timerIdInput.type = "hidden"; 44 | dimmer.appendChild(timerIdInput); 45 | } 46 | timerIdInput.value = timerId; 47 | } 48 | 49 | function addDimmer(delay, appearance) { 50 | var dimmer = document.createElement('div'); 51 | dimmer.id = DIMMER_DIV_ID; 52 | 53 | // TODO: add a picture 54 | 55 | // Message 56 | dimmer.style.color = "#ffffff"; 57 | dimmer.style.paddingTop = window.innerHeight / 2 - 30 + "px"; 58 | dimmer.style.fontSize = '36px'; 59 | dimmer.style.fontFamily = 'Georgia'; 60 | dimmer.style.fontVariant = 'normal'; 61 | 62 | var text = document.createElement("div"); 63 | text.innerHTML = DIMMER_TEXT.replace('%d', Math.round(delay)); 64 | text.style.textAlign = "center"; 65 | text.style.paddingTop = "50px"; 66 | text.style.fontSize = "20px"; 67 | dimmer.appendChild(text); 68 | 69 | var switch_text = document.createElement("div"); 70 | switch_text.innerHTML = DIMMER_SWITCH_TEXT; 71 | switch_text.id = DIMMER_DIV_ID + 'stayput'; 72 | switch_text.style.display = "none"; 73 | switch_text.style.textAlign = "center"; 74 | switch_text.style.paddingTop = "10px"; 75 | switch_text.style.fontSize = "14px"; 76 | switch_text.style.color = "#aaaaaa"; 77 | dimmer.appendChild(switch_text); 78 | 79 | // Positioning. 80 | dimmer.style.position = "fixed"; 81 | dimmer.style.top = "0px"; 82 | dimmer.style.left = "0px"; 83 | dimmer.style.width = "100%"; 84 | dimmer.style.height = "100%"; 85 | 86 | // Background. 87 | dimmer.style.background = "#001000"; 88 | if (appearance && appearance.transparent) { 89 | dimmer.style.opacity = "0.95"; 90 | } 91 | dimmer.style.zIndex = "99999"; 92 | 93 | document.body.insertBefore(dimmer, document.body.firstChild); 94 | 95 | // Install URL change watcher. 96 | original_url = document.URL; 97 | setInterval(watchUrlChanges, 1000); 98 | // TODO(gintas): Disable watcher when the tab is not active. 99 | 100 | return dimmer; 101 | } 102 | 103 | /* Watches for URL changes and reshows the dimmer if a change is detected. */ 104 | function watchUrlChanges() { 105 | if (document.URL != original_url) { 106 | original_url = document.URL; 107 | dim('reshow'); 108 | } 109 | } 110 | 111 | // Actions 112 | 113 | function create(dimmer_el, delay, appearance) { 114 | if (!dimmer_el) { 115 | var dimmer = addDimmer(delay, appearance); 116 | setDimTimer(dimmer, delay); 117 | } 118 | } 119 | 120 | function create_suspended(dimmer_el, delay, appearance) { 121 | if (!dimmer_el) { 122 | var dimmer = addDimmer(delay, appearance); 123 | } 124 | } 125 | 126 | function suspend(dimmer_el, delay) { 127 | if (dimmer_el) { 128 | clearDimTimer(dimmer_el); 129 | } 130 | } 131 | 132 | function resume(dimmer_el, delay) { 133 | if (dimmer_el && dimmer_el.style.display != "none") { 134 | setDimTimer(dimmer_el, delay); 135 | 136 | var switch_text = document.getElementById(DIMMER_DIV_ID + 'stayput'); 137 | switch_text.style.display = "block"; 138 | } 139 | } 140 | 141 | function reshow(dimmer_el, delay) { 142 | if (dimmer_el) { 143 | dimmer_el.style.display = "block"; 144 | setDimTimer(dimmer_el, delay); 145 | // TODO(gintas): Do not assume that this tab is currently active. 146 | } 147 | } 148 | 149 | /* Dims the current page for a given time in seconds 150 | 151 | 'action' is one of the following: 152 | - "create": a dimmer is created on the page if it is not already there and a timer is started 153 | - "create_suspended": a dimmer is created on the page if it is not already there, no timer is started 154 | - "suspend": the countdown is suspended if there is a dimmer on the page, no-op otherwise 155 | - "resume": the countdown is resumed if there is a dimmer on the page, no-op otherwise 156 | 157 | */ 158 | function dim(action) { 159 | // Dispatch by action name. 160 | var action_fns = { 161 | create: create, 162 | suspend: suspend, 163 | resume: resume, 164 | create_suspended: create_suspended, 165 | reshow: reshow 166 | }; 167 | 168 | if (TRACING) { 169 | console.log("action: " + action); 170 | } 171 | 172 | var action_fn = action_fns[action]; 173 | 174 | var dimmer_el = document.getElementById(DIMMER_DIV_ID); 175 | action_fn(dimmer_el, dimmer_delay, dimmer_appearance); 176 | } 177 | 178 | /* Forwarder function for calls using executeScript() */ 179 | function invoke_dimmer(action) { 180 | dim(action); 181 | } 182 | 183 | // On initial load, check with the extension whether action needs to be taken. 184 | chrome.extension.sendRequest({}, function(response) { 185 | if (response.redirectUrl) { 186 | window.location.href = response.redirectUrl; 187 | } else if (response.dimmerAction) { 188 | // Save dimmer parameters. 189 | dimmer_delay = response.delay; 190 | dimmer_appearance = response.appearance; 191 | function delayedDimmerFn() { 192 | if (document.body != null) { 193 | // The body of the document has started loading, the dimmer can be shown. 194 | invoke_dimmer(response.dimmerAction); 195 | } else { 196 | // The body is not yet available. 197 | setTimeout(delayedDimmerFn, BODY_POLL_MS); 198 | } 199 | } 200 | // Start polling. 201 | delayedDimmerFn(); 202 | } 203 | }); 204 | -------------------------------------------------------------------------------- /stats.js: -------------------------------------------------------------------------------- 1 | MIN_HISTORY_LENGTH = 4 * 24 * 3600 * 1000; // 4 days 2 | 3 | window.onload = function() { 4 | if (enoughLogData()) { 5 | drawLogPlot(); 6 | } else { 7 | $('#logPlot-container').html( 8 | "Crackbook has less than 4 days of activity logs. Please come back later."); 9 | } 10 | 11 | $('#show-browser-history-visits').click(function() { 12 | $('#browser-history-visits').show(); 13 | $('#browser-hist').hide(); 14 | // TODO: Show "Loading..." 15 | var histPlot = createPlot($('#histPlot')); 16 | collectHistoryData(histPlot); 17 | return true; 18 | }); 19 | }; 20 | 21 | /** 22 | * Draws a plot. 23 | */ 24 | function createPlot(el) { 25 | 26 | // Helper for returning the weekends in a period 27 | // Copied from flot samples. 28 | function weekendAreas(axes) { 29 | var markings = []; 30 | var d = new Date(axes.xaxis.min); 31 | // go to the first Saturday 32 | d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 1) % 7)); 33 | d.setUTCSeconds(0); 34 | d.setUTCMinutes(0); 35 | d.setUTCHours(0); 36 | var i = d.getTime(); 37 | do { 38 | // when we don't set yaxis, the rectangle automatically 39 | // extends to infinity upwards and downwards 40 | markings.push({ xaxis: { from: i, to: i + 2 * 24 * 60 * 60 * 1000 } }); 41 | i += 7 * 24 * 60 * 60 * 1000; 42 | } while (i < axes.xaxis.max); 43 | 44 | return markings; 45 | } 46 | 47 | var options = { 48 | xaxis: { mode: "time", timeformat: "%b %d", minTickSize: [1, "day"] }, 49 | selection: { mode: "x" }, 50 | grid: { markings: weekendAreas }, 51 | legend: { position: "nw" } 52 | }; 53 | 54 | var plot = $.plot(el, [], options); 55 | 56 | el.bind("plotselected", function (event, ranges) { 57 | // Zoom into selected area. 58 | var options = plot.getOptions(); 59 | options.xaxes[0].min = ranges.xaxis.from; 60 | options.xaxes[0].max = ranges.xaxis.to; 61 | plot.clearSelection(); 62 | plot.setupGrid(); 63 | plot.draw(); 64 | }); 65 | 66 | $(el).next("div.zoomout").click(function(e) { 67 | // Zoom out to view the entire dataset. 68 | e.preventDefault(); 69 | var options = plot.getOptions(); 70 | options.xaxes[0].min = options.xaxes[0].datamin; 71 | options.xaxes[0].max = options.xaxes[0].datamax; 72 | plot.setupGrid(); 73 | plot.draw(); 74 | }); 75 | 76 | return plot; 77 | } 78 | 79 | /** 80 | * Redraws a given plot with the given data. 81 | */ 82 | function redrawPlot(histPlot, plotData) { 83 | histPlot.setData(plotData); 84 | histPlot.setupGrid(); 85 | histPlot.draw(); 86 | } 87 | 88 | /** 89 | * Adds an entry to the plot, given a dictionary of hits by date. 90 | */ 91 | function addPlotRow(histPlot, plotData, domain, hitsByDate) { 92 | addMissingZeroes(hitsByDate); 93 | var row = mapToPairList(hitsByDate); 94 | 95 | // TODO: ensure order stability of entries 96 | plotData.push({ 97 | label: domain, 98 | data: row 99 | }); 100 | redrawPlot(histPlot, plotData); 101 | } 102 | 103 | /** 104 | * Adds zeroes for days where no hits have been logged. 105 | * 106 | *Modifies the list in place. 107 | * 108 | * @param hitsByDate a map of date -> hitcount. 109 | */ 110 | function addMissingZeroes(hitsByDate) { 111 | var min = null; 112 | for (var k in hitsByDate) { 113 | if (hitsByDate.hasOwnProperty(k)) { 114 | var t = parseInt(k, 10); 115 | if (min === null || t < min) { 116 | min = t; 117 | } 118 | } 119 | } 120 | 121 | var max = new Date().getTime(); // the current moment is the upper bound 122 | var dt = new Date(min); 123 | while (dt.getTime() < max) { 124 | if (!hitsByDate.hasOwnProperty(dt.getTime())) { 125 | hitsByDate[dt.getTime()] = 0; 126 | } 127 | dt.setDate(dt.getDate() + 1); 128 | } 129 | } 130 | 131 | /** 132 | * Convert a dictionary (object) to a list of pairs (in ascending key order). 133 | */ 134 | function mapToPairList(row) { 135 | var keys = []; 136 | for (var k in row) { 137 | if (row.hasOwnProperty(k)) { 138 | keys.push(k); 139 | } 140 | } 141 | var sorted_keys = keys.sort(); 142 | var d = []; 143 | for (var i = 0; i < sorted_keys.length; i++) { 144 | d.push([sorted_keys[i], row[sorted_keys[i]]]); 145 | } 146 | return d; 147 | } 148 | 149 | function markHit(timestamp, hitsByDate) { 150 | var dt = new Date(timestamp); 151 | clearTime(dt); 152 | var histKey = dt.getTime(); 153 | if (!hitsByDate.hasOwnProperty(histKey)) { 154 | hitsByDate[histKey] = 0; 155 | } 156 | hitsByDate[histKey] += 1; 157 | } 158 | 159 | /** 160 | * Collects history data asynchronously and shows it on the given plot. 161 | */ 162 | function collectHistoryData(histPlot) { 163 | var plotData = []; 164 | var junkDomains = getLocal('junkDomains'); 165 | junkDomains.forEach(function(domain) { 166 | findDirectHits(domain, function(visitItems) { 167 | var hitsByDate = {}; 168 | visitItems.forEach(function(item) { 169 | markHit(item.visitTime, hitsByDate); 170 | }); 171 | addPlotRow(histPlot, plotData, domain, hitsByDate); 172 | }); 173 | }); 174 | } 175 | 176 | /** 177 | * Draws plot of hits, according to the internal Crackbook log. 178 | */ 179 | function drawLogPlot() { 180 | var logPlot = createPlot($('#logPlot')); 181 | 182 | var domainStats = {}; 183 | // Initialize domainStats. 184 | var junkDomains = getLocal('junkDomains'); 185 | junkDomains.forEach(function(domain) { 186 | domainStats[domain] = {}; 187 | }); 188 | 189 | var hitLogKeys = getLocal('hitLogKeys'); 190 | hitLogKeys.forEach(function(key) { 191 | var entries = loadHits(key); 192 | entries.forEach(function(entry) { 193 | var hitsByDate = domainStats[entry.domain]; 194 | markHit(entry.timestamp * 1000, hitsByDate); 195 | }); 196 | }); 197 | 198 | var plotData = []; 199 | junkDomains.forEach(function(domain) { 200 | addPlotRow(logPlot, plotData, domain, domainStats[domain]); 201 | }); 202 | } 203 | 204 | /** 205 | * Returns true if there is enough internal log data to show a decent plot. 206 | */ 207 | function enoughLogData() { 208 | return (new Date() - earliestLogTimestamp()) > MIN_HISTORY_LENGTH; 209 | } 210 | 211 | function earliestLogTimestamp() { 212 | var timestamp = null; 213 | var hitLogKeys = getLocal('hitLogKeys'); 214 | hitLogKeys.forEach(function(key) { 215 | var entries = loadHits(key); 216 | entries.forEach(function(entry) { 217 | if (timestamp === null || entry.timestamp < timestamp) { 218 | timestamp = entry.timestamp; 219 | } 220 | }); 221 | }); 222 | return (timestamp !== null) ? new Date(timestamp * 1000) : new Date(); 223 | } 224 | 225 | function clearTime(dt) { 226 | dt.setUTCMilliseconds(0); 227 | dt.setUTCSeconds(0); 228 | dt.setUTCMinutes(0); 229 | dt.setUTCHours(0); 230 | } 231 | 232 | 233 | /** 234 | * Finds hits to a given domain. 235 | * 236 | *
Finds only direct hits (usually caused by user typing in a domain). Checks common variants of
237 | * the domain.
238 | */
239 | function findDirectHits(domain, visitsHandler) {
240 | var variants = domainVariants(domain);
241 | var outstandingRequests = variants.length;
242 | var cumulativeVisits = [];
243 | variants.forEach(function(url) {
244 | chrome.history.getVisits({ url: url }, function(visits) {
245 | cumulativeVisits = cumulativeVisits.concat(visits);
246 | outstandingRequests--;
247 | if (outstandingRequests === 0) {
248 | visitsHandler(cumulativeVisits);
249 | }
250 | });
251 | });
252 | }
253 |
254 | /**
255 | * Returns variants of a given domain with.
256 | */
257 | function domainVariants(domain) {
258 | return [
259 | 'http://' + domain,
260 | 'http://www.' + domain,
261 | 'https://' + domain,
262 | 'https://www.' + domain
263 | ];
264 | }
265 |
--------------------------------------------------------------------------------
/options.js:
--------------------------------------------------------------------------------
1 | var TOP_DOMAINS_NUM = 6;
2 | var MSG_SAVED_DELAY = 2*1000;
3 |
4 | function getTopDomains(historyItems) {
5 | var typedCounts = {};
6 | // Get URL data.
7 | var domains = [];
8 | for (var i = 0; i < historyItems.length; i++) {
9 | var h = historyItems[i];
10 | if (h.url && h.typedCount) {
11 | if (h.url.slice(0, 5) == 'https')
12 | continue; // https URLs are probably not junk
13 |
14 | var url = trimWWW(trimProtocol(h.url.trim()));
15 | var domain = trimPath(url);
16 |
17 | // Special case for Google Reader.
18 | // TODO: test me
19 | var components = url.split('/');
20 | if (components.length > 1 && components[0].indexOf('google.') != -1 && components[1] == 'reader') {
21 | domain = components[0] + '/' + components[1];
22 | }
23 |
24 | if (domains.indexOf(domain) == -1) {
25 | domains.push(domain);
26 | typedCounts[domain] = h.typedCount;
27 | } else {
28 | typedCounts[domain] += h.typedCount;
29 | }
30 | }
31 | }
32 | // Sort by typed count (descending).
33 | domains.sort(function(a, b) { return typedCounts[b] - typedCounts[a]; });
34 | // Take top N.
35 | var topUrls = domains.slice(0, TOP_DOMAINS_NUM);
36 | return topUrls;
37 | } // getTopDomains
38 |
39 | function addUrlField(ulId, value) {
40 | var checkbox = document.createElement('input');
41 | checkbox.setAttribute('type', 'checkbox');
42 | checkbox.setAttribute('checked', 'checked');
43 |
44 | var input = document.createElement('input');
45 | input.setAttribute('type', 'text');
46 | input.setAttribute('class', 'domain');
47 | input.value = value;
48 |
49 | var li = document.createElement('li');
50 | li.setAttribute('class', 'domain');
51 | li.appendChild(checkbox);
52 | li.appendChild(input);
53 |
54 | var ul = document.getElementById(ulId);
55 | var button_li = ul.children[ul.children.length - 1];
56 | // Insert field before the "add domain" button.
57 | ul.insertBefore(li, button_li);
58 | }
59 |
60 | function clearDomainsFromPage(ulId) {
61 | var ul = document.getElementById(ulId);
62 | for (var i = ul.children.length-1; i >= 0; i--) {
63 | var li = ul.children[i];
64 | if (li.getAttribute('class') == 'domain')
65 | ul.removeChild(li);
66 | }
67 | }
68 |
69 | function putDomainsOnPage(ulId, placeholderId, domains) {
70 | // Put on page.
71 | for (var i = 0; i < domains.length; i++) {
72 | addUrlField(ulId, domains[i]);
73 | }
74 |
75 | // Remove placeholder, if any.
76 | var placeholder = document.getElementById(placeholderId);
77 | if (placeholder) {
78 | document.getElementById(ulId).removeChild(placeholder);
79 | }
80 | } // putDomainsOnPage
81 |
82 |
83 | function collectInputs(ulId) {
84 | var ul = document.getElementById(ulId);
85 | var domains = [];
86 | for (var i = 0; i < ul.childNodes.length; i++) {
87 | if (ul.childNodes[i].nodeName == "LI") {
88 | var li = ul.childNodes[i];
89 | var checkbox = li.childNodes[0];
90 | var input = li.childNodes[1];
91 | if (checkbox.checked && input.value.indexOf('.') != -1) {
92 | domains.push(input.value.trim());
93 | }
94 | }
95 | }
96 | return domains;
97 | }
98 |
99 |
100 | // Collect the most frequently visited domains and prepopulate the blacklist.
101 | function loadTopUrls() {
102 | var weekAgo = new Date().getTime() - 1000*3600*24*7;
103 | chrome.history.search({ text: "", startTime: weekAgo,
104 | maxResults: 1000 },
105 | function(historyItems) {
106 | var topUrls = getTopDomains(historyItems);
107 | setLocal('junkDomains', topUrls);
108 | // Not calling submitConfigChange here to give the chance for the
109 | // user to clean the domain list.
110 | putDomainsOnPage("siteBlacklist", "blacklistPlaceholder", topUrls);
111 | }
112 | );
113 | } // loadTopUrls()
114 |
115 |
116 | function addJunkDomain() {
117 | addUrlField('siteBlacklist', '');
118 | }
119 |
120 | function bindControlHandlers() {
121 | document.getElementById('add_junk_domain_button').onclick = addJunkDomain;
122 | document.getElementById('save_button').onclick = saveSettings;
123 | document.getElementById('crackbookLink').onclick = function() {
124 | chrome.tabs.create({url: 'http://crackbook.info'});
125 | }
126 | }
127 |
128 |
129 | function showSettings() {
130 | // Threshold & delay
131 | document.getElementById("dimmerThreshold").value = getLocal('dimmerThreshold');
132 | document.getElementById("dimmerDelay").value = getLocal('dimmerDelay').toFixed(2);
133 | document.getElementById("dimmerDelayIncrement").value = getLocal('dimmerDelayIncrement').toFixed(2);
134 | document.getElementById("reset_daily_flag").checked = getLocal('reset_daily_flag');
135 | document.getElementById("base_delay").value = getLocal('base_delay');
136 |
137 | document.getElementById("checkActiveTab").checked = getLocal('checkActiveTab');
138 |
139 | // Junk domains.
140 | clearDomainsFromPage('siteBlacklist');
141 | if (getLocal('junkDomains').length > 0) {
142 | putDomainsOnPage("siteBlacklist", "blacklistPlaceholder", getLocal("junkDomains"));
143 | } else {
144 | loadTopUrls();
145 | }
146 |
147 | // Schedule
148 | document.getElementById("startTime").value = renderTime(getLocal('startTime'));
149 | document.getElementById("endTime").value = renderTime(getLocal('endTime'));
150 |
151 | var currentWeekdays = getLocal('weekdays');
152 | for (var i = 0; i < 7; i++) {
153 | document.getElementById("weekday-" + i).checked = currentWeekdays.indexOf("" + i) != -1;
154 | }
155 |
156 | // Reporting
157 | if (getLocal('reporting'))
158 | document.getElementById("upload_stats").checked = true;
159 | }
160 |
161 |
162 | function saveSettings() {
163 | /* Save settings from submitted form. */
164 |
165 | var junkDomains = collectInputs("siteBlacklist");
166 | for (var i = 0; i < junkDomains.length; i++) {
167 | junkDomains[i] = trimWWW(trimProtocol(junkDomains[i]));
168 | }
169 |
170 | var dimmerThreshold = parseInt(document.getElementById("dimmerThreshold").value, 10);
171 | if (isNaN(dimmerThreshold) || dimmerThreshold < 0) {
172 | dimmerThreshold = getLocal('dimmerThreshold');
173 | }
174 |
175 | var dimmerDelay = parseFloat(document.getElementById("dimmerDelay").value);
176 | if (isNaN(dimmerDelay) || dimmerDelay < 0) {
177 | dimmerDelay = getLocal('dimmerDelay');
178 | }
179 |
180 | var dimmerDelayIncrement = parseFloat(document.getElementById("dimmerDelayIncrement").value);
181 | if (isNaN(dimmerDelayIncrement) || dimmerDelayIncrement < 0) {
182 | dimmerDelayIncrement = getLocal('dimmerDelayIncrement');
183 | }
184 |
185 | var base_delay = parseFloat(document.getElementById("base_delay").value);
186 | if (isNaN(base_delay) || base_delay < 0) {
187 | base_delay = getLocal('base_delay');
188 | }
189 |
190 | var reset_daily_flag = !!document.getElementById('reset_daily_flag').checked;
191 |
192 | var checkActiveTab = document.getElementById("checkActiveTab").checked;
193 |
194 | // TODO: better validation
195 | var startTime = parseTime(document.getElementById("startTime").value);
196 | var endTime = parseTime(document.getElementById("endTime").value);
197 |
198 | var weekdays = "";
199 | for (i = 0; i < 7; i++) {
200 | if (document.getElementById("weekday-" + i).checked)
201 | weekdays += i;
202 | }
203 |
204 | var reporting = document.getElementById("upload_stats").checked;
205 |
206 | // Write settings to storage.
207 | setLocal('reporting', reporting);
208 | setLocal('dimmerThreshold', dimmerThreshold);
209 | setLocal('dimmerDelay', dimmerDelay);
210 | setLocal('dimmerDelayIncrement', dimmerDelayIncrement);
211 | setLocal('reset_daily_flag', reset_daily_flag);
212 | setLocal('base_delay', base_delay);
213 |
214 | setLocal('checkActiveTab', checkActiveTab);
215 | setLocal('junkDomains', junkDomains);
216 | setLocal('startTime', startTime);
217 | setLocal('endTime', endTime);
218 | setLocal('weekdays', weekdays);
219 |
220 | bgPage().submitConfigChange();
221 | bgPage().updateIcon(null, true);
222 |
223 | // Re-render saved settings so that invalid settings can be seen.
224 | showSettings();
225 |
226 | // Show status message.
227 | document.getElementById('save_button').style.display = 'none';
228 | var msg = document.getElementById('saved_message');
229 | msg.style.display = 'inline'; // show the message
230 | var opacityValue = 1.0;
231 | var timeoutFn = function() {
232 | opacityValue -= 1 / 20.0;
233 | msg.style.opacity = opacityValue;
234 | if (opacityValue < 0.1) {
235 | window.close();
236 | } else {
237 | setTimeout(timeoutFn, MSG_SAVED_DELAY / 20.0);
238 | }
239 | };
240 | setTimeout(timeoutFn, MSG_SAVED_DELAY / 20.0);
241 | } // saveSettings
242 |
243 |
244 |
245 | window.onload = function() {
246 | bindControlHandlers();
247 | showSettings();
248 | };
249 |
250 |
251 | // Google Analytics
252 | var _gaq = _gaq || [];
253 | _gaq.push(['_setAccount', 'UA-6080477-5']);
254 | _gaq.push(['_trackPageview']);
255 |
256 | (function() {
257 | var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
258 | ga.src = 'https://ssl.google-analytics.com/ga.js';
259 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
260 | })();
261 |
--------------------------------------------------------------------------------
/background.js:
--------------------------------------------------------------------------------
1 | var HITNUM_FONT = '12px Arial Bold';
2 | var HITNUM_COLOR = "rgb(255,255,255)";
3 | var HITNUM_POS_X = 3;
4 | var HITNUM_POS_Y = 12;
5 | var NOTIFICATION_TEXT = 'Time to get back to work!';
6 | var API_URL = 'http://crackbook.info/api/';
7 |
8 | // TODO: the following should be configurable
9 |
10 | var NOTIFICATION_THRESHOLD = 5;
11 | var NOTIFICATION_HIT_INTERVAL = 5;
12 |
13 | var TRACING = false;
14 |
15 | function drawIcon(img_name) {
16 | img_path = "images/" + img_name;
17 | chrome.browserAction.setIcon({ path: img_path });
18 | } // drawIcon
19 |
20 | function drawTextOnBg(canvas, image, value) {
21 | var ctx = canvas.getContext('2d');
22 |
23 | ctx.drawImage(image, 0, 0);
24 |
25 | ctx.font = HITNUM_FONT;
26 | ctx.fillStyle = HITNUM_COLOR;
27 | ctx.fillText("" + value, HITNUM_POS_X, HITNUM_POS_Y);
28 |
29 | var imageData = ctx.getImageData(0, 0, 19, 19);
30 | chrome.browserAction.setIcon({imageData: imageData});
31 | } // drawTextOnBg
32 |
33 | var iconState = null;
34 |
35 | function updateIcon(active, inJunk) {
36 | if (active === null) // null or undefined
37 | active = extensionActive();
38 | if (inJunk === null) { // null or undefined
39 | chrome.tabs.getSelected(null, function(selectedTab) {
40 | var junkDomain = lookupJunkDomain(selectedTab.url);
41 | updateIcon(active, !!junkDomain);
42 | });
43 | return;
44 | }
45 |
46 | var newIcon = null;
47 |
48 | newIcon = inJunk ? 'hamburger' : 'carrot';
49 | if (!active)
50 | newIcon += '-inactive';
51 | newIcon += '-19px.png';
52 |
53 | if (iconState != newIcon) {
54 | iconState = newIcon;
55 | drawIcon(newIcon);
56 | }
57 | }
58 |
59 | function extensionActive() {
60 | var now = new Date();
61 | // Check weekday.
62 | if (getLocal('weekdays').indexOf("" + now.getDay()) == -1)
63 | return false;
64 | // Check time.
65 | var nowMins = parseTime(now.getHours() + ":" + now.getMinutes());
66 | var startTime = getLocal('startTime');
67 | var endTime = getLocal('endTime');
68 | if (startTime <= endTime) {
69 | return (startTime <= nowMins) && (nowMins <= endTime);
70 | } else {
71 | // Handle the case when, e.g. the end time is in the night (14:00-3:00).
72 | return (startTime <= nowMins) || (nowMins <= endTime);
73 | }
74 | }
75 |
76 | function shouldDimPage() {
77 | return getTodaysHits() > getLocal('dimmerThreshold');
78 | }
79 |
80 | function toQueryString(obj) {
81 | // Convert an object to a query string.
82 | var components = [];
83 | for (var k in obj) {
84 | var v = obj[k];
85 | components.push(k + '=' + encodeURIComponent(v));
86 | }
87 | return components.join('&');
88 | }
89 |
90 | function ajaxPost(url, fields) {
91 | fields['user_id'] = getLocal('user_id');
92 | fields['timestamp'] = new Date().valueOf();
93 | var xhr = new XMLHttpRequest();
94 | xhr.open("POST", url, true);
95 | xhr.send(toQueryString(fields));
96 | }
97 |
98 | function registerHit(domain, blocked, active) {
99 | var params = {domain: domain, blocked: blocked, active: active};
100 | if (getLocal('reporting')) {
101 | ajaxPost(API_URL + 'register_hit', params);
102 | }
103 | storeHit(domain, blocked, active);
104 | }
105 |
106 | function submitConfigChange() {
107 | if (getLocal('reporting'))
108 | ajaxPost(API_URL + 'register_configuration', {
109 | domains: JSON.stringify(getLocal('junkDomains')),
110 | dimmer_threshold: getLocal('dimmerThreshold'),
111 | dimmer_delay: getLocal('dimmerDelay'),
112 | dimmer_delay_increment: getLocal('dimmerDelayIncrement'),
113 | dimmer_delay_growth: '-1', // TODO: remove
114 | redirect_probability: '-1', // TODO: remove
115 | start_time: getLocal('startTime'),
116 | end_time: getLocal('endTime'),
117 | weekdays: getLocal('weekdays')
118 | });
119 | }
120 |
121 | // Returns true if the URL looks normal.
122 | // Used to avoid trying to dim special-purpose tabs.
123 | function isNormalUrl(s) {
124 | return s && ((s.indexOf('http://') === 0) || (s.indexOf('https://') === 0));
125 | }
126 |
127 | /*
128 | * Dimmer state transitions for junk pages
129 | *
130 | * handleNewPage:
131 | * - tab active --> enable dimmer
132 | * - tab inactive --> enable dimmer, suspend timer
133 | *
134 | * tabSelectionChangedHandler:
135 | * - suspend timer on previous tab
136 | * - restart timer on new tab
137 | *
138 | * windowFocusChangedHandler:
139 | * - suspend timer on previous tab
140 | * - restart timer on active tab
141 | *
142 | */
143 |
144 | var lastDimmedTabId = null;
145 |
146 | function handleNewPage(newTab, selectedTab, sendResponse) {
147 | // Every code path in this function should call sendResponse.
148 | // Collect data.
149 | var junkDomain = lookupJunkDomain(newTab.url);
150 | var active = extensionActive();
151 | var shouldDim = shouldDimPage();
152 | if (!junkDomain && getLocal('checkActiveTab')) {
153 | junkDomain = lookupJunkDomain(selectedTab.url);
154 | // TODO: This works for "open in background tab", but not for "open in
155 | // foreground tab" or "open in new window". Cover these cases by checking
156 | // the last seen tab, not just the active tab, and whether the switch was
157 | // recent.
158 | // TODO: This is easy to circumvent by immediately reloading a page. One
159 | // solution is to add a temporary blacklist of pages / domains.
160 | }
161 |
162 | // Send response.
163 | if (!(junkDomain && active && shouldDim)) {
164 | sendResponse({}); // do nothing
165 | } else {
166 | var tabIsActive = (newTab.id == selectedTab.id);
167 | sendResponse({dimmerAction: tabIsActive ? "create" : "create_suspended",
168 | delay: getLocal('dimmerDelay'),
169 | appearance: {transparent: false}});
170 | if (tabIsActive) {
171 | lastDimmedTabId = newTab.id;
172 | }
173 | }
174 |
175 | // Tracking and logging.
176 | updateIcon(null, !!junkDomain);
177 | if (junkDomain) {
178 | if (active) {
179 | incrementJunkCounter(junkDomain);
180 | increaseDimmerDelay();
181 | }
182 | registerHit(junkDomain, shouldDim, active);
183 | }
184 | }
185 |
186 | function increaseDimmerDelay() {
187 | var newDelay = getLocal('dimmerDelay') + getLocal('dimmerDelayIncrement');
188 | setLocal('dimmerDelay', newDelay);
189 | }
190 |
191 | function tabSelectionChangedHandler(tabId, selectInfo) {
192 | if (lastDimmedTabId) {
193 | invokeDimmer(lastDimmedTabId, "suspend");
194 | lastDimmedTabId = null;
195 | }
196 |
197 | chrome.tabs.get(tabId, function(tab) {
198 | if (isNormalUrl(tab.url)) {
199 | // If the page was opened from a junk page, the following check will not
200 | // indicate that this page is junk. Only the icon is affected though.
201 | var junkDomain = lookupJunkDomain(tab.url);
202 | updateIcon(null, !!junkDomain);
203 | invokeDimmer(tabId, "resume");
204 | lastDimmedTabId = tabId;
205 | }
206 | });
207 | }
208 |
209 | function windowFocusChangedHandler(windowId) {
210 | if (lastDimmedTabId) {
211 | // TODO: What if that tab does not exist any more?
212 | invokeDimmer(lastDimmedTabId, "suspend");
213 | lastDimmedTabId = null;
214 | }
215 |
216 | if (windowId != chrome.windows.WINDOW_ID_NONE) {
217 | chrome.tabs.getSelected(windowId, function(tab) {
218 | if (isNormalUrl(tab.url)) {
219 | var junkDomain = lookupJunkDomain(tab.url);
220 | updateIcon(null, !!junkDomain);
221 | if (junkDomain && shouldDimPage()) {
222 | invokeDimmer(tab.id, "resume");
223 | lastDimmedTabId = tab.id;
224 | }
225 | }
226 | });
227 | }
228 | }
229 |
230 | // A wrapper function that also figures out the selected tab.
231 | function newPageHandler(request, sender, sendResponse) {
232 | chrome.tabs.getSelected(null, function(selectedTab) {
233 | handleNewPage(sender.tab, selectedTab, sendResponse);
234 | });
235 | }
236 |
237 | function showNotification() {
238 | var notification_obj = webkitNotifications.createNotification(
239 | 'images/hamburger-128px.png',
240 | NOTIFICATION_TEXT,
241 | "");
242 | notification_obj.show();
243 | window.setTimeout(function() { notification_obj.cancel(); }, 3000);
244 | }
245 |
246 | function incrementJunkCounter(domain) {
247 | var today = todayAsString();
248 | var day = getLocal('day');
249 | var hits = getLocal('dayHits');
250 | if (day == today) {
251 | hits += 1;
252 | } else {
253 | setLocal('day', today);
254 | hits = 1;
255 | }
256 | setLocal('dayHits', hits);
257 |
258 | // Also, if the day changed and reset_daily_flag is set, reset.
259 | if (day != today && getLocal('reset_daily_flag')) {
260 | setLocal('dimmerDelay', getLocal('base_delay'));
261 | }
262 |
263 | chrome.browserAction.setBadgeText({text: "" + hits});
264 | setTimeout(function() { chrome.browserAction.setBadgeText({text: ''}); },
265 | 3000);
266 |
267 | // Show notification if needed.
268 | if (hits > NOTIFICATION_THRESHOLD && (hits % NOTIFICATION_HIT_INTERVAL === 0))
269 | // If hits >= dimmerThreshold, the notification is not needed any
270 | // more as the dimmer kicks in.
271 | if (hits < getLocal('dimmerThreshold'))
272 | showNotification();
273 | }
274 |
275 | function invokeDimmer(tabId, dimmerAction) {
276 | // Dim the page and start (or restart) the timer.
277 | //
278 | // Actions:
279 | // - "create": a dimmer is created on the page if it is not already there and a timer is started
280 | // - "create_suspended": a dimmer is created on the page if it is not already there, no timer is started
281 | // - "suspend": the countdown is suspended if there is a dimmer on the page
282 | // - "resume": the countdown is resumed if there is a dimmer on the page
283 | if (TRACING) {
284 | console.log("Invoking action: " + tabId + " -> " + dimmerAction);
285 | }
286 | var primer_code = "if (window.invoke_dimmer) { invoke_dimmer('" + dimmerAction + "'); }";
287 | chrome.tabs.executeScript(tabId, { code: primer_code });
288 | }
289 |
290 | function initIcon() {
291 | updateIcon(null, false);
292 | }
293 |
294 | function initUserID() {
295 | var user_id = getLocal('user_id');
296 | if (user_id === 0)
297 | setLocal('user_id', Math.floor(Math.random() * 256*256*256*127));
298 | }
299 |
300 | function initExtension() {
301 | initUserID();
302 | chrome.extension.onRequest.addListener(newPageHandler);
303 | chrome.tabs.onSelectionChanged.addListener(tabSelectionChangedHandler);
304 | chrome.windows.onFocusChanged.addListener(windowFocusChangedHandler);
305 | initIcon();
306 |
307 | if (getLocal('junkDomains').length === 0)
308 | chrome.tabs.create({ url: "options.html" });
309 | }
310 |
311 | initExtension();
312 |
313 | // Google Analytics
314 | var _gaq = _gaq || [];
315 | _gaq.push(['_setAccount', 'UA-6080477-5']);
316 | _gaq.push(['_trackPageview']);
317 |
318 | (function() {
319 | var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
320 | ga.src = 'https://ssl.google-analytics.com/ga.js';
321 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
322 | })();
323 |
--------------------------------------------------------------------------------
/images/promo-image.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
439 |
--------------------------------------------------------------------------------
/flot/jquery.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery JavaScript Library v1.5.1
3 | * http://jquery.com/
4 | *
5 | * Copyright 2011, John Resig
6 | * Dual licensed under the MIT or GPL Version 2 licenses.
7 | * http://jquery.org/license
8 | *
9 | * Includes Sizzle.js
10 | * http://sizzlejs.com/
11 | * Copyright 2011, The Dojo Foundation
12 | * Released under the MIT, BSD, and GPL Licenses.
13 | *
14 | * Date: Wed Feb 23 13:55:29 2011 -0500
15 | */
16 | (function(aY,H){var al=aY.document;var a=(function(){var bn=function(bI,bJ){return new bn.fn.init(bI,bJ,bl)},bD=aY.jQuery,bp=aY.$,bl,bH=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,bv=/\S/,br=/^\s+/,bm=/\s+$/,bq=/\d/,bj=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,bw=/^[\],:{}\s]*$/,bF=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,by=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,bs=/(?:^|:|,)(?:\s*\[)+/g,bh=/(webkit)[ \/]([\w.]+)/,bA=/(opera)(?:.*version)?[ \/]([\w.]+)/,bz=/(msie) ([\w.]+)/,bB=/(mozilla)(?:.*? rv:([\w.]+))?/,bG=navigator.userAgent,bE,bC=false,bk,e="then done fail isResolved isRejected promise".split(" "),bd,bu=Object.prototype.toString,bo=Object.prototype.hasOwnProperty,bi=Array.prototype.push,bt=Array.prototype.slice,bx=String.prototype.trim,be=Array.prototype.indexOf,bg={};bn.fn=bn.prototype={constructor:bn,init:function(bI,bM,bL){var bK,bN,bJ,bO;if(!bI){return this}if(bI.nodeType){this.context=this[0]=bI;this.length=1;return this}if(bI==="body"&&!bM&&al.body){this.context=al;this[0]=al.body;this.selector="body";this.length=1;return this}if(typeof bI==="string"){bK=bH.exec(bI);if(bK&&(bK[1]||!bM)){if(bK[1]){bM=bM instanceof bn?bM[0]:bM;bO=(bM?bM.ownerDocument||bM:al);bJ=bj.exec(bI);if(bJ){if(bn.isPlainObject(bM)){bI=[al.createElement(bJ[1])];bn.fn.attr.call(bI,bM,true)}else{bI=[bO.createElement(bJ[1])]}}else{bJ=bn.buildFragment([bK[1]],[bO]);bI=(bJ.cacheable?bn.clone(bJ.fragment):bJ.fragment).childNodes}return bn.merge(this,bI)}else{bN=al.getElementById(bK[2]);if(bN&&bN.parentNode){if(bN.id!==bK[2]){return bL.find(bI)}this.length=1;this[0]=bN}this.context=al;this.selector=bI;return this}}else{if(!bM||bM.jquery){return(bM||bL).find(bI)}else{return this.constructor(bM).find(bI)}}}else{if(bn.isFunction(bI)){return bL.ready(bI)}}if(bI.selector!==H){this.selector=bI.selector;this.context=bI.context}return bn.makeArray(bI,this)},selector:"",jquery:"1.5.1",length:0,size:function(){return this.length},toArray:function(){return bt.call(this,0)},get:function(bI){return bI==null?this.toArray():(bI<0?this[this.length+bI]:this[bI])},pushStack:function(bJ,bL,bI){var bK=this.constructor();if(bn.isArray(bJ)){bi.apply(bK,bJ)}else{bn.merge(bK,bJ)}bK.prevObject=this;bK.context=this.context;if(bL==="find"){bK.selector=this.selector+(this.selector?" ":"")+bI}else{if(bL){bK.selector=this.selector+"."+bL+"("+bI+")"}}return bK},each:function(bJ,bI){return bn.each(this,bJ,bI)},ready:function(bI){bn.bindReady();bk.done(bI);return this},eq:function(bI){return bI===-1?this.slice(bI):this.slice(bI,+bI+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(bt.apply(this,arguments),"slice",bt.call(arguments).join(","))},map:function(bI){return this.pushStack(bn.map(this,function(bK,bJ){return bI.call(bK,bJ,bK)}))},end:function(){return this.prevObject||this.constructor(null)},push:bi,sort:[].sort,splice:[].splice};bn.fn.init.prototype=bn.fn;bn.extend=bn.fn.extend=function(){var bR,bK,bI,bJ,bO,bP,bN=arguments[0]||{},bM=1,bL=arguments.length,bQ=false;if(typeof bN==="boolean"){bQ=bN;bN=arguments[1]||{};bM=2}if(typeof bN!=="object"&&!bn.isFunction(bN)){bN={}}if(bL===bM){bN=this;--bM}for(;bMa";var bm=bd.getElementsByTagName("*"),bk=bd.getElementsByTagName("a")[0],bl=al.createElement("select"),be=bl.appendChild(al.createElement("option")),bj=bd.getElementsByTagName("input")[0];if(!bm||!bm.length||!bk){return}a.support={leadingWhitespace:bd.firstChild.nodeType===3,tbody:!bd.getElementsByTagName("tbody").length,htmlSerialize:!!bd.getElementsByTagName("link").length,style:/red/.test(bk.getAttribute("style")),hrefNormalized:bk.getAttribute("href")==="/a",opacity:/^0.55$/.test(bk.style.opacity),cssFloat:!!bk.style.cssFloat,checkOn:bj.value==="on",optSelected:be.selected,deleteExpando:true,optDisabled:false,checkClone:false,noCloneEvent:true,noCloneChecked:true,boxModel:null,inlineBlockNeedsLayout:false,shrinkWrapBlocks:false,reliableHiddenOffsets:true};bj.checked=true;a.support.noCloneChecked=bj.cloneNode(true).checked;bl.disabled=true;a.support.optDisabled=!be.disabled;var bf=null;a.support.scriptEval=function(){if(bf===null){var bo=al.documentElement,bp=al.createElement("script"),br="script"+a.now();try{bp.appendChild(al.createTextNode("window."+br+"=1;"))}catch(bq){}bo.insertBefore(bp,bo.firstChild);if(aY[br]){bf=true;delete aY[br]}else{bf=false}bo.removeChild(bp);bo=bp=br=null}return bf};try{delete bd.test}catch(bh){a.support.deleteExpando=false}if(!bd.addEventListener&&bd.attachEvent&&bd.fireEvent){bd.attachEvent("onclick",function bn(){a.support.noCloneEvent=false;bd.detachEvent("onclick",bn)});bd.cloneNode(true).fireEvent("onclick")}bd=al.createElement("div");bd.innerHTML="";var bg=al.createDocumentFragment();bg.appendChild(bd.firstChild);a.support.checkClone=bg.cloneNode(true).cloneNode(true).lastChild.checked;a(function(){var bp=al.createElement("div"),e=al.getElementsByTagName("body")[0];if(!e){return}bp.style.width=bp.style.paddingLeft="1px";e.appendChild(bp);a.boxModel=a.support.boxModel=bp.offsetWidth===2;if("zoom" in bp.style){bp.style.display="inline";bp.style.zoom=1;a.support.inlineBlockNeedsLayout=bp.offsetWidth===2;bp.style.display="";bp.innerHTML="";a.support.shrinkWrapBlocks=bp.offsetWidth!==2}bp.innerHTML="
";var bo=bp.getElementsByTagName("td");a.support.reliableHiddenOffsets=bo[0].offsetHeight===0;bo[0].style.display="";bo[1].style.display="none";a.support.reliableHiddenOffsets=a.support.reliableHiddenOffsets&&bo[0].offsetHeight===0;bp.innerHTML="";e.removeChild(bp).style.display="none";bp=bo=null});var bi=function(e){var bp=al.createElement("div");e="on"+e;if(!bp.attachEvent){return true}var bo=(e in bp);if(!bo){bp.setAttribute(e,"return;");bo=typeof bp[e]==="function"}bp=null;return bo};a.support.submitBubbles=bi("submit");a.support.changeBubbles=bi("change");bd=bm=bk=null})();var aE=/^(?:\{.*\}|\[.*\])$/;a.extend({cache:{},uuid:0,expando:"jQuery"+(a.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:true,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:true},hasData:function(e){e=e.nodeType?a.cache[e[a.expando]]:e[a.expando];return !!e&&!P(e)},data:function(bf,bd,bh,bg){if(!a.acceptData(bf)){return}var bk=a.expando,bj=typeof bd==="string",bi,bl=bf.nodeType,e=bl?a.cache:bf,be=bl?bf[a.expando]:bf[a.expando]&&a.expando;if((!be||(bg&&be&&!e[be][bk]))&&bj&&bh===H){return}if(!be){if(bl){bf[a.expando]=be=++a.uuid}else{be=a.expando}}if(!e[be]){e[be]={};if(!bl){e[be].toJSON=a.noop}}if(typeof bd==="object"||typeof bd==="function"){if(bg){e[be][bk]=a.extend(e[be][bk],bd)}else{e[be]=a.extend(e[be],bd)}}bi=e[be];if(bg){if(!bi[bk]){bi[bk]={}}bi=bi[bk]}if(bh!==H){bi[bd]=bh}if(bd==="events"&&!bi[bd]){return bi[bk]&&bi[bk].events}return bj?bi[bd]:bi},removeData:function(bg,be,bh){if(!a.acceptData(bg)){return}var bj=a.expando,bk=bg.nodeType,bd=bk?a.cache:bg,bf=bk?bg[a.expando]:a.expando;if(!bd[bf]){return}if(be){var bi=bh?bd[bf][bj]:bd[bf];if(bi){delete bi[be];if(!P(bi)){return}}}if(bh){delete bd[bf][bj];if(!P(bd[bf])){return}}var e=bd[bf][bj];if(a.support.deleteExpando||bd!=aY){delete bd[bf]}else{bd[bf]=null}if(e){bd[bf]={};if(!bk){bd[bf].toJSON=a.noop}bd[bf][bj]=e}else{if(bk){if(a.support.deleteExpando){delete bg[a.expando]}else{if(bg.removeAttribute){bg.removeAttribute(a.expando)}else{bg[a.expando]=null}}}}},_data:function(bd,e,be){return a.data(bd,e,be,true)},acceptData:function(bd){if(bd.nodeName){var e=a.noData[bd.nodeName.toLowerCase()];if(e){return !(e===true||bd.getAttribute("classid")!==e)}}return true}});a.fn.extend({data:function(bg,bi){var bh=null;if(typeof bg==="undefined"){if(this.length){bh=a.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,be;for(var bf=0,bd=e.length;bft ","
"],tr:[2,"","
"],td:[3,"
"],col:[2,"","
"],area:[1,""],_default:[0,"",""]};an.optgroup=an.option;an.tbody=an.tfoot=an.colgroup=an.caption=an.thead;an.th=an.td;if(!a.support.htmlSerialize){an._default=[1,"div"&&!e?bd.childNodes:[];for(var bj=bk.length-1;bj>=0;--bj){if(a.nodeName(bk[bj],"tbody")&&!bk[bj].childNodes.length){bk[bj].parentNode.removeChild(bk[bj])}}}if(!a.support.leadingWhitespace&&aj.test(bh)){bd.insertBefore(bg.createTextNode(aj.exec(bh)[0]),bd.firstChild)}bh=bd.childNodes}}if(bh.nodeType){bo.push(bh)}else{bo=a.merge(bo,bh)}}if(bn){for(bm=0;bo[bm];bm++){if(bi&&a.nodeName(bo[bm],"script")&&(!bo[bm].type||bo[bm].type.toLowerCase()==="text/javascript")){bi.push(bo[bm].parentNode?bo[bm].parentNode.removeChild(bo[bm]):bo[bm])}else{if(bo[bm].nodeType===1){bo.splice.apply(bo,[bm+1,0].concat(a.makeArray(bo[bm].getElementsByTagName("script"))))}bn.appendChild(bo[bm])}}}return bo},cleanData:function(bd){var bg,be,e=a.cache,bl=a.expando,bj=a.event.special,bi=a.support.deleteExpando;for(var bh=0,bf;(bf=bd[bh])!=null;bh++){if(bf.nodeName&&a.noData[bf.nodeName.toLowerCase()]){continue}be=bf[a.expando];if(be){bg=e[be]&&e[be][bl];if(bg&&bg.events){for(var bk in bg.events){if(bj[bk]){a.event.remove(bf,bk)}else{a.removeEvent(bf,bk,bg.handle)}}if(bg.handle){bg.handle.elem=null}}if(bi){delete bf[a.expando]}else{if(bf.removeAttribute){bf.removeAttribute(a.expando)}}delete e[be]}}}});function a8(e,bd){if(bd.src){a.ajax({url:bd.src,async:false,dataType:"script"})}else{a.globalEval(bd.text||bd.textContent||bd.innerHTML||"")}if(bd.parentNode){bd.parentNode.removeChild(bd)}}var ae=/alpha\([^)]*\)/i,ak=/opacity=([^)]*)/,aM=/-([a-z])/ig,y=/([A-Z])/g,aZ=/^-?\d+(?:px)?$/i,a7=/^-?\d/,aV={position:"absolute",visibility:"hidden",display:"block"},ag=["Left","Right"],aR=["Top","Bottom"],U,ay,aL,l=function(e,bd){return bd.toUpperCase()};a.fn.css=function(e,bd){if(arguments.length===2&&bd===H){return this}return a.access(this,e,bd,true,function(bf,be,bg){return bg!==H?a.style(bf,be,bg):a.css(bf,be)})};a.extend({cssHooks:{opacity:{get:function(be,bd){if(bd){var e=U(be,"opacity","opacity");return e===""?"1":e}else{return be.style.opacity}}}},cssNumber:{zIndex:true,fontWeight:true,opacity:true,zoom:true,lineHeight:true},cssProps:{"float":a.support.cssFloat?"cssFloat":"styleFloat"},style:function(bf,be,bk,bg){if(!bf||bf.nodeType===3||bf.nodeType===8||!bf.style){return}var bj,bh=a.camelCase(be),bd=bf.style,bl=a.cssHooks[bh];be=a.cssProps[bh]||bh;if(bk!==H){if(typeof bk==="number"&&isNaN(bk)||bk==null){return}if(typeof bk==="number"&&!a.cssNumber[bh]){bk+="px"}if(!bl||!("set" in bl)||(bk=bl.set(bf,bk))!==H){try{bd[be]=bk}catch(bi){}}}else{if(bl&&"get" in bl&&(bj=bl.get(bf,false,bg))!==H){return bj}return bd[be]}},css:function(bh,bg,bd){var bf,be=a.camelCase(bg),e=a.cssHooks[be];bg=a.cssProps[be]||be;if(e&&"get" in e&&(bf=e.get(bh,true,bd))!==H){return bf}else{if(U){return U(bh,bg,be)}}},swap:function(bf,be,bg){var e={};for(var bd in be){e[bd]=bf.style[bd];bf.style[bd]=be[bd]}bg.call(bf);for(bd in be){bf.style[bd]=e[bd]}},camelCase:function(e){return e.replace(aM,l)}});a.curCSS=a.css;a.each(["height","width"],function(bd,e){a.cssHooks[e]={get:function(bg,bf,be){var bh;if(bf){if(bg.offsetWidth!==0){bh=o(bg,e,be)}else{a.swap(bg,aV,function(){bh=o(bg,e,be)})}if(bh<=0){bh=U(bg,e,e);if(bh==="0px"&&aL){bh=aL(bg,e,e)}if(bh!=null){return bh===""||bh==="auto"?"0px":bh}}if(bh<0||bh==null){bh=bg.style[e];return bh===""||bh==="auto"?"0px":bh}return typeof bh==="string"?bh:bh+"px"}},set:function(be,bf){if(aZ.test(bf)){bf=parseFloat(bf);if(bf>=0){return bf+"px"}}else{return bf}}}});if(!a.support.opacity){a.cssHooks.opacity={get:function(bd,e){return ak.test((e&&bd.currentStyle?bd.currentStyle.filter:bd.style.filter)||"")?(parseFloat(RegExp.$1)/100)+"":e?"1":""},set:function(bf,bg){var be=bf.style;be.zoom=1;var e=a.isNaN(bg)?"":"alpha(opacity="+bg*100+")",bd=be.filter||"";be.filter=ae.test(bd)?bd.replace(ae,e):be.filter+" "+e}}}if(al.defaultView&&al.defaultView.getComputedStyle){ay=function(bh,e,bf){var be,bg,bd;bf=bf.replace(y,"-$1").toLowerCase();if(!(bg=bh.ownerDocument.defaultView)){return H}if((bd=bg.getComputedStyle(bh,null))){be=bd.getPropertyValue(bf);if(be===""&&!a.contains(bh.ownerDocument.documentElement,bh)){be=a.style(bh,bf)}}return be}}if(al.documentElement.currentStyle){aL=function(bg,be){var bh,bd=bg.currentStyle&&bg.currentStyle[be],e=bg.runtimeStyle&&bg.runtimeStyle[be],bf=bg.style;if(!aZ.test(bd)&&a7.test(bd)){bh=bf.left;if(e){bg.runtimeStyle.left=bg.currentStyle.left}bf.left=be==="fontSize"?"1em":(bd||0);bd=bf.pixelLeft+"px";bf.left=bh;if(e){bg.runtimeStyle.left=e}}return bd===""?"auto":bd}}U=ay||aL;function o(be,bd,e){var bg=bd==="width"?ag:aR,bf=bd==="width"?be.offsetWidth:be.offsetHeight;if(e==="border"){return bf}a.each(bg,function(){if(!e){bf-=parseFloat(a.css(be,"padding"+this))||0}if(e==="margin"){bf+=parseFloat(a.css(be,"margin"+this))||0}else{bf-=parseFloat(a.css(be,"border"+this+"Width"))||0}});return bf}if(a.expr&&a.expr.filters){a.expr.filters.hidden=function(be){var bd=be.offsetWidth,e=be.offsetHeight;return(bd===0&&e===0)||(!a.support.reliableHiddenOffsets&&(be.style.display||a.css(be,"display"))==="none")};a.expr.filters.visible=function(e){return !a.expr.filters.hidden(e)}}var i=/%20/g,ah=/\[\]$/,bc=/\r?\n/g,ba=/#.*$/,ar=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,aO=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,aB=/(?:^file|^widget|\-extension):$/,aD=/^(?:GET|HEAD)$/,b=/^\/\//,I=/\?/,aU=/