diff --git a/README.md b/README.md index fb85c0e..80cb5d4 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,9 @@ HTTP Switchboard (FOSS) put you in FULL control of where your browser is allowed You can blacklist/whitelist a single cell, an entire row, a group of rows, an entire column, or the whole matrix with just one click. -HTTP Switchboard matrix uses precedence logic to evaluate what is blocked/allowed according to which cells are blacklisted/whitelisted. For example, this allows the user to whitelist a whole page with one click, without having to repeatedly whitelist whatever new data appear on the page. +HTTP Switchboard matrix uses precedence logic to evaluate what is blocked/allowed according to which cells are blacklisted/whitelisted. For example, this allows you to whitelist a whole page with one click, without having to repeatedly whitelist whatever new data appear on the page. -You can also create scopes for your whitelist/blacklist rules. For example, this allow you to whitelist `facebook.com` ONLY when visiting Facebook web site. +You can also create scopes for your whitelist/blacklist rules. For example, this allows you to whitelist `facebook.com` ONLY when visiting Facebook web site. The goal of this extension is to make the allowing or blocking of web sites, wholly or partly, as straightforward as possible, so as to not discourage those users who give up easily on good security and privacy habits. diff --git a/css/fonts.css b/css/fonts.css index a6ef215..4b52aee 100644 --- a/css/fonts.css +++ b/css/fonts.css @@ -34,4 +34,12 @@ font-weight: normal; font-style: normal; } +.fa { + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + vertical-align: baseline; + display: inline-block; + } diff --git a/css/popup.css b/css/popup.css index 4c4ea8b..49dafae 100644 --- a/css/popup.css +++ b/css/popup.css @@ -1,12 +1,3 @@ -.fa { - font-family: FontAwesome; - font-style: normal; - font-weight: normal; - line-height: 1; - vertical-align: baseline; - display: inline-block; - } - body { margin: 0; border: 0; @@ -16,53 +7,117 @@ body { min-width: 32em; min-height: 15em; } +*:focus { + outline: none; + } .paneHead { padding: 2px; position: fixed; top: 0; + height: 5.5em; left:0; right: 0; background-color: white; z-index: 10; } +.paneContent { + padding-top: 5.5em; + } -#toolbar { - padding-bottom:1.5em; +.toolbar { + padding-bottom: 0.5em; + display: inline-block; + vertical-align: top; } -#toolbar button { +.toolbar button { margin: 0; border: 0; padding: 4px; - font-size: 1.75em; - min-width: 1.3em; - opacity: 0.7; + font: inherit; + background-color: white; + opacity: 0.9; + cursor: pointer; } -#toolbar button:hover { +.toolbar button:hover { + background-color: #eee; opacity: 1; } -.btn-group.pull-right > .btn { - float: left; +.toolbar button.fa { + font: 1.75em 'FontAwesome'; + min-width: 1.3em; } -body #scopePersist { +.pullright { + float: right; + } + +.dropdown-menu { + margin: 0; + border: 0; + padding: 3px 0 0 0; + position: absolute; + z-index: 20; + font-size: 110%; + display: none; + } +.dropdown-menu.pullright { + right: 0; + } +.dropdown-menu > ul { + margin: 0; + border: 0; + border: 1px solid #ccc; + border-radius: 4px; + padding: 3px 0; + background-color: white; + list-style-type: none; + } +.dropdown-menu > ul > li.dropdown-menu-entry { + margin: 0; + border: 0; + padding: 4px 0.5em; + color: black; + cursor: pointer; + } +.dropdown-menu > ul > li.dropdown-menu-entry:hover { + background: #eee; + } +.dropdown-menu > ul > li.dropdown-menu-entry-divider { + margin: 0.5em 0; + border-top: 1px solid #ccc; + } +.dropdown-menu > ul > li.dropdown-menu-entry > .fa { + margin-right: 0.5em; + font-size: 110%; + color: #aaa; + } +.dropdown-menu-button ~ .dropdown-menu { + display: none; + } +.dropdown-menu-button:focus ~ .dropdown-menu { + display: block; + } + +body #buttonRevert { margin-right: 1em; } -body.tScopeGlobal #scopePersist { +body.tScopeGlobal #buttonPersist { color: #000; } -body.tScopeDomain #scopePersist { +body.tScopeDomain #buttonPersist { color: #24c; } -body.tScopeSite #scopePersist { +body.tScopeSite #buttonPersist { color: #48c; } -body.powerOff #scopePersist { +body.powerOff #buttonPersist { visibility: hidden; } body.powerOff #buttonRevert { visibility: hidden; } + .dropdown-menu > li > a > i { padding: 0 6px; font-size: 1.2em; @@ -74,23 +129,25 @@ body.powerOff #buttonPower i { color: #c00; } -body #scopeMenu { - margin: 0 0 1em 0; +body #toolbarScope { + margin: 0; border: 0; padding: 0; display: inline-block; box-sizing: content-box; position: relative; } -body.powerOff #scopeMenu { +body.powerOff #toolbarScope { visibility: hidden; } -body #scopeCell { + +body .toolbar button#scopeCell { margin: 0; border: 1px dotted rgba(0,0,0,0.2); padding: 6px 1px 3px 1px; box-sizing: content-box; width: 18em; + height: 1.5em; white-space: nowrap; text-align: right; line-height: 100%; @@ -116,43 +173,14 @@ body.pScopeDomain #scopeCell { body.pScopeSite #scopeCell { background-image: url('/img/permanent-scope-site.png'); } + body #scopeKeys { - position: absolute; - z-index: 20; - display: none; - } -body #scopeKeys > div { - margin: 0; - border: 0; - padding-top: 3px; - } -body #scopeKeys > div > div { - margin: 0; - border: 0; - border: 1px solid #ccc; - border-radius: 4px; - padding: 3px 0; - background-color: white; - } -body #scopeKeys > div > div > div { - margin: 0; - border: 0; padding: 6px 1px 3px 1px; - box-sizing: content-box; - width: 18em; + left: 0; + right: 0; text-align: right; - cursor: pointer; - } -body #scopeKeys > div > div > div:hover { - background: #eee; - } -body #scopeCell:focus ~ #scopeKeys { - display: block; } -.paneContent { - padding-top: 5em; - } .matrix { text-align: left; } @@ -171,6 +199,11 @@ body #scopeCell:focus ~ #scopeKeys { line-height: 100%; position: relative; } + +.paneHead #matHead { + position: absolute; + bottom: 3px; + } .paneHead .matCell:nth-child(2) { letter-spacing: -0.3px; } @@ -233,7 +266,7 @@ body #scopeCell:focus ~ #scopeKeys { margin: 0; padding: 0; border: 0; - height: 9px; + height: 3px; background: url('/img/matrix-group-hide.png') no-repeat center top 0, url('/img/matrix-group-hline.png') repeat-x center top 3px; opacity: 0.2; diff --git a/css/rulemanager.css b/css/rulemanager.css new file mode 100644 index 0000000..0d821f8 --- /dev/null +++ b/css/rulemanager.css @@ -0,0 +1,245 @@ +body { + margin: 0; + padding: 0; + font: 15px httpsb,sans-serif; + width: 100%; + } +h1:first-child { + margin: 0 0 1em 0; + } +#navi-bar { + border-bottom: 1px solid #ccc; + padding: 1em; + position: fixed; + background-color: white; + width: 100%; + height: 8em; + z-index: 100; + } +#navi-bar + div { + padding: 10em 1em 1em 1em; + } +h2 { + margin: 0.5em 0; + padding: 0.5em 0; + } +h2 + *, h1 + * { + margin: 0 1em; + } +table { + border: 0; + border-collapse: collapse; + } +table td { + margin: 0; + border: 0; + padding: 4px 0; + vertical-align: top; + } +table td h2 { + margin: 0 0 0.75em 0; + } +textarea { + box-sizing: border-box; + width: 99%; + height: 20em; + } +button { + margin: 0 1em 0 0; + border: 1px solid rgba(0,0,0,0.2); + border-radius: 5px; + padding: 0.5em 1em; + font: inherit; + font-size: 110%; + background-color: #eee; + cursor: pointer; + } +button .fa { + margin-right: 0.5em; + font-size: larger; + } + +#recipeDecode, #recipeImport, #recipeEncode, #recipeExport { + cursor: pointer; + opacity: 0.5; + } +#recipeImport, #recipeExport { + padding-top: 32px; + display: inline-block; + } +#recipeDecode, #recipeEncode { + margin: 0 4px; + padding-top: 28px; + display: inline-block; + height: 28px; + } +#recipeDecode { + margin-bottom: 1em; + background: url('/img/decode.png') no-repeat center top; + } +#recipeEncode { + margin-top: 1em; + background: url('/img/encode.png') no-repeat center top; + } +#recipeImport { + background: url('/img/import.png') no-repeat center top; + } +#recipeExport { + background: url('/img/export.png') no-repeat center top; + } +#recipeDecode:hover, #recipeImport:hover, #recipeEncode:hover, #recipeExport:hover { + opacity: 1; + } +.recipe { + margin: 2px; + border: 1px solid #ddd; + padding: 1px; + font-family: monospace; + font-size: smaller; + line-height: 110%; + color: #888; + background-color: #f2f2f2; + white-space: pre; + overflow-x: hidden; + } +.scopes ul { + margin: 0; + padding-left: 1em; + list-style: none; + } +.scopes > ul { + padding: 0; + } +.scopes > ul > li.scope { + margin: 4px; + border: 1px dotted #ccc; + display: inline-block; + vertical-align: top; + } +.scopes > ul > li.scope:hover { + border: 1px solid #ccc; + } +.scopes > ul > li.scope.todelete { + background-color: #fee; + text-decoration: line-through; + } +.scopes > ul > li.scope > div:first-child { + margin: 0; + border: 0; + padding: 2px; + } +.scopes > ul > li.scope > div:first-child .scopeName { + cursor: pointer; + } +.scopes > ul > li.scope > div:first-child .state { + padding: 3px 3px 0 3px; + float: right; + font-size: 120%; + } +.scopes > ul > li.scope > div:first-child .state::before { + content: '\f09c'; + } +.scopes > ul > li.scope.permanent > div:first-child .state::before { + content: '\f023'; + opacity: 0.25; + } +.scopes > ul > li.scope.todelete > div:first-child .state::before { + content: '\f00d'; + opacity: 1; + color: red; + } +#global.scopes > ul > li.scope > div:first-child { + color: #000; + background-color: #eee; + } +#global.scopes > ul > li.scope > ul > li > ul { + min-width: 12em; + } +#perdomain.scopes > ul > li.scope > *:first-child { + color: #24c; + background-color: #eee; + } +#persite.scopes > ul > li.scope > *:first-child { + color: #48c; + background-color: #f4f4f4; + } +#global.scopes > ul > li.scope > ul > li { + margin-left: 2em; + display: inline-block; + vertical-align: top; + } +#global.scopes > ul > li.scope > ul > li:first-child { + margin-left: 0; + } +.scopes > ul > li.scope > .recipe { + margin: 1em 2px 2px 2px; + border: 1px solid #ddd; + padding: 1px; + font-size: 12px; + line-height: 110%; + display: inline-block; + vertical-align: bottom; + width: 20em; + height: 2em; + color: #888; + background-color: #f2f2f2; + white-space: pre; + overflow: hidden; + opacity: 0.25; + } +.scopes > ul > li.scope .recipe:hover { + opacity: 1.0; + } +.scopes > ul > li.scope > ul > li.white { + color: #080; + } +.scopes > ul > li.scope > ul > li.black { + color: #c00; + } +.scopes > ul > li.scope > ul > li.gray { + color: #aaa; + } +.scopes > ul > li.scope > ul > li > ul > li { + cursor: pointer; + } +.scopes > ul > li.scope > ul > li > ul > li:hover { + background-color: #eef; + } +.scopes > ul > li.scope > ul > li > ul > li > span.state { + padding: 4px 4px 0 4px; + display: inline-block; + float: right; + visibility: hidden; + } +.scopes > ul > li.scope.permanent > ul > li > ul > li > span.state { + visibility: visible; + } +.scopes > ul > li.scope.permanent > ul > li > ul > li > span.state::before { + content: '\f09c'; + } +.scopes > ul > li.scope.permanent > ul > li > ul > li.permanent > span.state::before { + content: '\f023'; + opacity: 0.25; + } +.scopes > ul > li.scope.todelete > ul > li > ul > li.rule { + background-color: #fee; + text-decoration: line-through; + } +.scopes > ul > li.scope > ul > li > ul > li.rule.todelete { + background-color: #fee; + text-decoration: line-through; + } +.scopes > ul > li.scope > ul > li > ul > li.rule.todelete > span.state::before { + content: '\f00d'; + opacity: 1; + color: red; + } +.scopes > ul > li.scope.todelete > ul > li > ul > li.rule > span.state::before { + content: '\f00d'; + opacity: 1; + color: red; + } +.bad { + background-color: #fdd; + } + diff --git a/js/background.js b/js/background.js index b7641c5..2cf1042 100644 --- a/js/background.js +++ b/js/background.js @@ -74,8 +74,6 @@ var HTTPSB = { temporaryScopes: null, permanentScopes: null, - temporaryScopeJunkyard: {}, - // Current entries from remote blacklists -- // just hostnames, '*/' is implied, this saves significantly on memory. blacklistReadonly: {}, diff --git a/js/httpsb.js b/js/httpsb.js index bc1e33a..b47c039 100644 --- a/js/httpsb.js +++ b/js/httpsb.js @@ -38,100 +38,6 @@ HTTPSB.normalizeScopeURL = function(url) { /******************************************************************************/ -HTTPSB.createPageScopeIfNotExists = function(url) { - if ( url && url === '*' ) { - return true; - } - if ( !url ) { - return false; - } - url = uriTools.rootURLFromURI(url); - var tscope = this.temporaryScopes.scopes[url]; - var pscope = this.permanentScopes.scopes[url]; - if ( !tscope !== !pscope ) { - throw 'HTTP Switchboard.createPageScopeIfNotExists(): corrupted internal state'; - } - // Skip everything if scopes exist and are switched on - if ( tscope && !tscope.off && pscope && !pscope.off ) { - return false; - } - // Create temporary scope or switch it on - if ( !tscope ) { - tscope = new PermissionScope(); - tscope.whitelist('main_frame', '*'); - tscope.whitelist('stylesheet', '*'); - tscope.whitelist('image', '*'); - this.temporaryScopes.scopes[url] = tscope; - } else { - tscope.off = false; - } - // Create permanent scope or switch it on - if ( !pscope ) { - pscope = new PermissionScope(); - pscope.whitelist('main_frame', '*'); - pscope.whitelist('stylesheet', '*'); - pscope.whitelist('image', '*'); - this.permanentScopes.scopes[url] = pscope; - } else { - pscope.off = false; - } - - // Page-scoped permissions are always persisted, so that the - // entry is present, in order to be sure at least '*|main_frame' is - // persisted. - this.savePermissions(); - - return true; -}; - -/******************************************************************************/ - -HTTPSB.destroyPageScopeIfExists = function(url) { - if ( !url || url === '*' ) { - return false; - } - url = uriTools.rootURLFromURI(url); - var tscope = this.temporaryScopes.scopes[url]; - var pscope = this.permanentScopes.scopes[url]; - if ( !tscope !== !pscope ) { - throw 'HTTP Switchboard.destroyPageScopeIfExists(): corrupted internal state'; - } - if ( !tscope && !pscope ) { - return false; - } - if ( tscope.off && pscope.off ) { - return false; - } - tscope.off = true; - pscope.off = true; - - // Flush out the page permissions from storage. - this.savePermissions(); - - return true; -}; - -/******************************************************************************/ - -HTTPSB.scopePageExists = function(url) { - if ( !url ) { - return false; - } - // Global scope always exists - if ( url === '*' ) { - return true; - } - url = uriTools.rootURLFromURI(url); - var tscope = this.temporaryScopes.scopes[url]; - var pscope = this.permanentScopes.scopes[url]; - if ( !tscope !== !pscope ) { - throw 'HTTP Switchboard.scopePageExists(): corrupted internal state'; - } - return tscope && !tscope.off && pscope && !pscope.off; -}; - -/******************************************************************************/ - HTTPSB.globalScopeKey = function() { return '*'; }; @@ -183,14 +89,16 @@ HTTPSB.isValidScopeKey = function(scopeKey) { HTTPSB.createTemporaryGlobalScope = function(url) { var scopeKey, scope; scopeKey = this.siteScopeKeyFromURL(url); - scope = this.removeTemporaryScope(scopeKey); - if ( scope ) { - this.temporaryScopeJunkyard[scopeKey] = scope; + this.removeTemporaryScopeFromScopeKey(scopeKey); + if ( scopeKey.indexOf('https:') === 0 ) { + scopeKey = 'http:' + scopeKey.slice(6); + this.removeTemporaryScopeFromScopeKey(scopeKey); } scopeKey = this.domainScopeKeyFromURL(url); - scope = this.removeTemporaryScope(scopeKey); - if ( scope ) { - this.temporaryScopeJunkyard[scopeKey] = scope; + this.removeTemporaryScopeFromScopeKey(scopeKey); + if ( scopeKey.indexOf('https:') === 0 ) { + scopeKey = 'http:' + scopeKey.slice(6); + this.removeTemporaryScopeFromScopeKey(scopeKey); } }; @@ -198,15 +106,25 @@ HTTPSB.createPermanentGlobalScope = function(url) { var changed = false; // Remove potentially occulting domain/site scopes. var scopeKey = this.siteScopeKeyFromURL(url); - var scope = this.removePermanentScope(scopeKey); - if ( scope ) { + if ( this.removePermanentScopeFromScopeKey(scopeKey) ) { changed = true; } + if ( scopeKey.indexOf('https:') === 0 ) { + scopeKey = 'http:' + scopeKey.slice(6); + if ( this.removeTemporaryScopeFromScopeKey(scopeKey) ) { + changed = true; + } + } scopeKey = this.domainScopeKeyFromURL(url); - scope = this.removePermanentScope(scopeKey); - if ( scope ) { + if ( this.removePermanentScopeFromScopeKey(scopeKey) ) { changed = true; } + if ( scopeKey.indexOf('https:') === 0 ) { + scopeKey = 'http:' + scopeKey.slice(6); + if ( this.removeTemporaryScopeFromScopeKey(scopeKey) ) { + changed = true; + } + } if ( changed ) { this.savePermissions(); } @@ -216,27 +134,23 @@ HTTPSB.createPermanentGlobalScope = function(url) { /******************************************************************************/ HTTPSB.createTemporaryDomainScope = function(url) { - var scopeKey, scope; - // Already created? - scopeKey = this.domainScopeKeyFromURL(url); - if ( !this.temporaryScopes.scopes[scopeKey] ) { - // See if there is a match in junkyard - scope = this.temporaryScopeJunkyard[scopeKey]; - if ( !scope ) { - scope = new PermissionScope(); - scope.whitelist('main_frame', '*'); - } else { - delete this.temporaryScopeJunkyard[scopeKey]; - } + var scopeKey = this.domainScopeKeyFromURL(url); + var scope = this.temporaryScopes.scopes[scopeKey]; + if ( !scope ) { + scope = new PermissionScope(); + scope.whitelist('main_frame', '*'); this.temporaryScopes.scopes[scopeKey] = scope; + } else if ( scope.off ) { + scope.off = false; } // Remove potentially occulting site scope. scopeKey = this.siteScopeKeyFromURL(url); - scope = this.removeTemporaryScope(scopeKey); - if ( scope ) { - this.temporaryScopeJunkyard[scopeKey] = scope; + this.removeTemporaryScopeFromScopeKey(scopeKey); + if ( scopeKey.indexOf('https:') === 0 ) { + scopeKey = 'http:' + scopeKey.slice(6); + this.removeTemporaryScopeFromScopeKey(scopeKey); } }; @@ -253,10 +167,15 @@ HTTPSB.createPermanentDomainScope = function(url) { // Remove potentially existing site scope: it would occlude domain scope. scopeKey = this.siteScopeKeyFromURL(url); - scope = this.removePermanentScope(scopeKey); - if ( scope ) { + if ( this.removePermanentScopeFromScopeKey(scopeKey) ) { changed = true; } + if ( scopeKey.indexOf('https:') === 0 ) { + scopeKey = 'http:' + scopeKey.slice(6); + if ( this.removeTemporaryScopeFromScopeKey(scopeKey) ) { + changed = true; + } + } if ( changed ) { this.savePermissions(); @@ -267,24 +186,16 @@ HTTPSB.createPermanentDomainScope = function(url) { /******************************************************************************/ HTTPSB.createTemporarySiteScope = function(url) { - var scopeKey, scope; - // Already created? - scopeKey = this.siteScopeKeyFromURL(url); - if ( this.temporaryScopes.scopes[scopeKey] ) { - return false; - } - - // See if there is a match in junkyard - scope = this.temporaryScopeJunkyard[scopeKey]; + var scopeKey = this.siteScopeKeyFromURL(url); + var scope = this.temporaryScopes.scopes[scopeKey]; if ( !scope ) { scope = new PermissionScope(); scope.whitelist('main_frame', '*'); + this.temporaryScopes.scopes[scopeKey] = scope; } else { - delete this.temporaryScopeJunkyard[scopeKey]; + scope.off = false; } - this.temporaryScopes.scopes[scopeKey] = scope; - return true; }; HTTPSB.createPermanentSiteScope = function(url) { @@ -302,31 +213,44 @@ HTTPSB.createPermanentSiteScope = function(url) { /******************************************************************************/ -HTTPSB.removeTemporaryScope = function(scopeKey) { +HTTPSB.createTemporaryScopeFromScopeKey = function(scopeKey, empty) { var scope = this.temporaryScopes.scopes[scopeKey]; - if ( scope ) { - delete this.temporaryScopes.scopes[scopeKey]; + if ( !scope ) { + scope = new PermissionScope(); + scope.whitelist('main_frame', '*'); + this.temporaryScopes.scopes[scopeKey] = scope; + } else if ( scope.off ) { + scope.off = false; } return scope; }; -HTTPSB.removePermanentScope = function(scopeKey) { - var scope = this.permanentScopes.scopes[scopeKey]; +/******************************************************************************/ + +HTTPSB.removeTemporaryScopeFromScopeKey = function(scopeKey) { + if ( scopeKey === '*' ) { + return null; + } + var scope = this.temporaryScopes.scopes[scopeKey]; if ( scope ) { - delete this.permanentScopes.scopes[scopeKey]; + scope.off = true; } return scope; }; -/******************************************************************************/ - -HTTPSB.removePermanentScope = function(scopeKey) { - var scope = this.permanentScopes.scopes[scopeKey]; - if ( !scope ) { +HTTPSB.removePermanentScopeFromScopeKey = function(scopeKey, persist) { + // Can't remove global scope + if ( scopeKey === '*' ) { return null; } - delete this.permanentScopes.scopes[scopeKey]; - return scope; + var pscope = this.permanentScopes.scopes[scopeKey]; + if ( pscope ) { + delete this.permanentScopes.scopes[scopeKey]; + if ( persist ) { + this.savePermissions(); + } + } + return pscope; }; /******************************************************************************/ @@ -377,6 +301,16 @@ HTTPSB.transposeType = function(type, url) { /******************************************************************************/ +HTTPSB.addRuleTemporarily = function(scopeKey, list, type, hostname) { + this.temporaryScopes.addRule(scopeKey, list, type, hostname); +}; + +HTTPSB.removeRuleTemporarily = function(scopeKey, list, type, hostname) { + this.temporaryScopes.removeRule(scopeKey, list, type, hostname); +}; + +/******************************************************************************/ + // Whitelist something HTTPSB.whitelistTemporarily = function(scopeKey, type, hostname) { @@ -496,6 +430,17 @@ HTTPSB.getPermanentColor = function(scopeKey, type, hostname) { /******************************************************************************/ +// Commit temporary permissions. + +HTTPSB.commitPermissions = function(persist) { + this.permanentScopes.assign(this.temporaryScopes); + if ( persist ) { + this.savePermissions(); + } +}; + +/******************************************************************************/ + // Reset permission lists to their default state. HTTPSB.revertPermissions = function() { diff --git a/js/lists.js b/js/lists.js index 59d0ef3..15cd4cf 100644 --- a/js/lists.js +++ b/js/lists.js @@ -188,6 +188,7 @@ PermissionScope.prototype.assign = function(other) { this.white.assign(other.white); this.black.assign(other.black); this.gray.assign(other.gray); + this.off = other.off; }; /******************************************************************************/ @@ -380,6 +381,24 @@ PermissionScope.prototype.evaluate = function(type, hostname) { /******************************************************************************/ +PermissionScope.prototype.addRule = function(list, type, hostname) { + var list = this[list]; + if ( !list ) { + throw new Error('PermissionScope.addRule() > invalid list name'); + } + return list.addOne(type + '|' + hostname); +}; + +PermissionScope.prototype.removeRule = function(list, type, hostname) { + var list = this[list]; + if ( !list ) { + throw new Error('PermissionScope.removeRule() > invalid list name'); + } + return list.removeOne(type + '|' + hostname); +}; + +/******************************************************************************/ + PermissionScope.prototype.whitelist = function(type, hostname) { var key = type + '|' + hostname; var changed = false; @@ -470,6 +489,7 @@ PermissionScopes.prototype.fromString = function(s) { PermissionScopes.prototype.assign = function(other) { var scopeKeys, i, scopeKey; + var thisScope, otherScope; // Remove scopes found here but not found in other // Overwrite scopes found both here and in other @@ -477,10 +497,14 @@ PermissionScopes.prototype.assign = function(other) { i = scopeKeys.length; while ( i-- ) { scopeKey = scopeKeys[i]; - if ( !other.scopes[scopeKey] ) { + otherScope = other.scopes[scopeKey]; + if ( otherScope && otherScope.off ) { + otherScope = null; + } + if ( !otherScope ) { delete this.scopes[scopeKey]; } else { - this.scopes[scopeKey].assign(other.scopes[scopeKey]); + this.scopes[scopeKey].assign(otherScope); } } @@ -489,7 +513,15 @@ PermissionScopes.prototype.assign = function(other) { i = scopeKeys.length; while ( i-- ) { scopeKey = scopeKeys[i]; - if ( !this.scopes[scopeKey] ) { + otherScope = other.scopes[scopeKey]; + if ( otherScope.off ) { + continue; + } + thisScope = this.scopes[scopeKey]; + if ( thisScope && thisScope.off ) { + thisScope = null; + } + if ( !thisScope ) { this.scopes[scopeKey] = new PermissionScope(); this.scopes[scopeKey].assign(other.scopes[scopeKey]); } @@ -514,13 +546,19 @@ PermissionScopes.prototype.scopeKeyFromPageURL = function(url) { // From narrowest scope to broadest scope // Try site scope var scopeKey = scheme + '://' + hostname; - if ( this.scopes[scopeKey] ) { + var scope = this.scopes[scopeKey]; + if ( scope && !scope.off ) { return scopeKey; } var secure = scheme === 'https'; + // If the connection is encrypted, it is then acceptable to apply + // rules from unencrypted connection if any, because it is assumed the + // rules for unencrypted connection are more restrictive. + // The reverse is not true however. if ( secure ) { scopeKey = 'http://' + hostname; - if ( this.scopes[scopeKey] ) { + scope = this.scopes[scopeKey]; + if ( scope && !scope.off ) { return scopeKey; } } @@ -530,12 +568,14 @@ PermissionScopes.prototype.scopeKeyFromPageURL = function(url) { return '*'; } scopeKey = scheme + '://*.' + domain; - if ( this.scopes[scopeKey] ) { + scope = this.scopes[scopeKey]; + if ( scope && !scope.off ) { return scopeKey; } if ( secure ) { scopeKey = 'http://*.' + domain; - if ( this.scopes[scopeKey] ) { + scope = this.scopes[scopeKey]; + if ( scope && !scope.off ) { return scopeKey; } } @@ -563,6 +603,14 @@ PermissionScopes.prototype.evaluate = function(scopeKey, type, hostname) { /******************************************************************************/ +PermissionScopes.prototype.addRule = function(scopeKey, list, type, hostname) { + return this.scopeFromScopeKey(scopeKey).addRule(list, type, hostname); +}; + +PermissionScopes.prototype.removeRule = function(scopeKey, list, type, hostname) { + return this.scopeFromScopeKey(scopeKey).removeRule(list, type, hostname); +}; + PermissionScopes.prototype.whitelist = function(scopeKey, type, hostname) { return this.scopeFromScopeKey(scopeKey).whitelist(type, hostname); }; diff --git a/js/popup.js b/js/popup.js index e7fe2e8..43d8031 100644 --- a/js/popup.js +++ b/js/popup.js @@ -1362,8 +1362,8 @@ function bindToTabHandler(tabs) { if ( !HTTPSBPopup.matrixHasRows ) { $('#no-traffic').css('display', ''); $('#matHead').remove(); - $('#scopeMenu').remove(); - $('#scopePersist').remove(); + $('#toolbarScope').remove(); + $('#buttonPersist').remove(); $('#buttonReload').remove(); $('#buttonRevert').remove(); } @@ -1413,6 +1413,18 @@ function gotoExtensionURL() { /******************************************************************************/ +function gotoExternalURL() { + var url = $(this).data('externalUrl'); + if ( url ) { + chrome.runtime.sendMessage({ + what: 'gotoURL', + url: url + }); + } +} + +/******************************************************************************/ + // make menu only when popup html is fully loaded function initAll() { @@ -1454,7 +1466,7 @@ function initAll() { $('body') .on('mouseenter', '.matCell', mouseenterMatrixCellHandler) .on('mouseleave', '.matCell', mouseleaveMatrixCellHandler); - $('#scopePersist').on('click', persistScope); + $('#buttonPersist').on('click', persistScope); $('#scopeKeyGlobal').on('mousedown', createGlobalScope); $('#scopeKeyDomain').on('mousedown', createDomainScope); $('#scopeKeySite').on('mousedown', createSiteScope); @@ -1466,8 +1478,9 @@ function initAll() { $('#buttonRuleManager span').text(chrome.i18n.getMessage('ruleManagerPageName')); $('#buttonInfo span').text(chrome.i18n.getMessage('statsPageName')); $('#buttonSettings span').text(chrome.i18n.getMessage('settingsPageName')); - $('.extensionURL').on('click', gotoExtensionURL); - $('#buttonPower').on('click', togglePower); + $('.extensionURL').on('mousedown', gotoExtensionURL); + $('.externalURL').on('mousedown', gotoExternalURL); + $('#buttonPower').on('mousedown', togglePower); $('#matList').on('click', '.g3Meta', function() { var separator = $(this); separator.toggleClass('g3Collapsed'); diff --git a/js/rulemanager.js b/js/rulemanager.js index 2ecec39..253baf7 100644 --- a/js/rulemanager.js +++ b/js/rulemanager.js @@ -19,9 +19,6 @@ Home: https://github.com/gorhill/httpswitchboard */ - -// TODO: cleanup - /******************************************************************************/ (function() { @@ -31,26 +28,14 @@ var recipeWidth = 40; /******************************************************************************/ var friendlyTypeNames = { - '*': '*', - 'cookie': 'cookies', + '*': '\u2217', + 'cookie': 'cookie', 'stylesheet': 'css', - 'image': 'images', - 'object': 'plugins', - 'script': 'scripts', - 'xmlhttprequest': 'XMLHttpRequests', - 'sub_frame': 'frames', - 'other': 'other' -}; - -var hostileTypeNames = { - '*': '*', - 'cookies': 'cookie', - 'css': 'stylesheet', - 'images': 'image', - 'plugins': 'object', - 'scripts': 'script', - 'XMLHttpRequests': 'xmlhttprequest', - 'frames': 'sub_frame', + 'image': 'img', + 'object': 'plugin', + 'script': 'script', + 'xmlhttprequest': 'XHR', + 'sub_frame': 'frame', 'other': 'other' }; @@ -119,7 +104,7 @@ function renderRecipeStringToListKey(recipe) { if ( !parts ) { return false; } - return parts[1]; + return parts[1].replace('list', ''); } /******************************************************************************/ @@ -164,27 +149,6 @@ function renderAllScopesToRecipeString(scopes) { /******************************************************************************/ -function renderRuleToHTML(rule) { - // part[0] = type - // part[1] = hostname - var parts = rule.split('|'); - return document.createTextNode(friendlyTypeNames[parts[0]] + ' ' + (parts[1] === '*' ? '*' : parts[1])); -} - -/******************************************************************************/ - -function renderScopeKeyToHTML(scopeKey) { - if ( scopeKey === '*' ) { - return $('*'); - } - return $('', { - href: scopeKey, - text: scopeKey - }); -} - -/******************************************************************************/ - function uglifyRecipe(recipe) { recipe = encodeURIComponent(recipe.replace(/ /g, '\t')); var s = ''; @@ -219,11 +183,13 @@ function beautifyRecipe(recipe) { /******************************************************************************/ -function getPermanentColor(scopeKey, rule) { - // part[0] = type - // part[1] = hostname - var parts = rule.split('|'); - return getHTTPSB().getPermanentColor(scopeKey, parts[0], parts[1]); +function renderRuleKeyToHTML(rule) { + var pos = rule.indexOf('|'); + return document.createTextNode( + friendlyTypeNames[rule.slice(0, pos)] + + ' ' + + rule.slice(pos + 1).replace('*', '\u2217') + ); } /******************************************************************************/ @@ -251,49 +217,123 @@ function compareRules(a, b) { /******************************************************************************/ +function renderScopeKeyToHTML(scopeKey) { + var div = $('
'); + var scopeNameElement = $('', { + 'text': scopeKey.replace('*', '\u2217'), + 'class': 'scopeName' + }); + div.append(scopeNameElement); + div.append($('', { + 'class': 'fa state' + }) + ); + return div; +} + +/******************************************************************************/ + +function strToId(s) { + return s.replace(/[ 0123456789*./:|-]/g, function(c) { + return 'GHIJKLMNOPQRSTUVWXYZ'.charAt(' 0123456789*./:|-'.indexOf(c)); + }); +} + +function IdToStr(id) { + return id.replace(/[G-Z]/g, function(c) { + return ' 0123456789*./:|-'.charAt('GHIJKLMNOPQRSTUVWXYZ'.indexOf(c)); + }); +} + +/******************************************************************************/ + +function liScopeFromScopeKey(scopeKey) { + var liScope = $('.' + strToId(scopeKey)); + return liScope.length ? liScope : null; +} + +/******************************************************************************/ + +function liListFromScopeKey(scopeKey, listKey) { + var liList = $('.' + strToId(scopeKey) + ' .' + strToId(listKey)); + return liList.length ? liList : null; +} + +/******************************************************************************/ + +function liRuleFromRuleKey(scopeKey, listKey, ruleKey) { + var liRule = $('.' + strToId(scopeKey) + ' .' + strToId(listKey) + ' .' + strToId(ruleKey)); + return liRule.length ? liRule : null; +} + +/******************************************************************************/ + function renderScopeToHTML(scopeKey) { - var lists = ['gray', 'black', 'white']; - var httpsb = getHTTPSB(); - var scope = httpsb.temporaryScopes.scopes[scopeKey]; var liScope = $('
  • ', { - 'class': 'scope' + 'class': 'scope ' + strToId(scopeKey) }); + liScope.prop('scopeKey', scopeKey) liScope.append(renderScopeKeyToHTML(scopeKey)); - var ulScope = $('