/******************************************************************************* uBlock Origin - a Chromium browser extension to black/white list requests. Copyright (C) 2015-2017 Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see {http://www.gnu.org/licenses/}. Home: https://github.com/gorhill/uBlock */ /* global punycode */ /* jshint bitwise: false */ 'use strict'; /******************************************************************************/ µBlock.HnSwitches = (function() { /******************************************************************************/ var HnSwitches = function() { this.reset(); }; /******************************************************************************/ var switchBitOffsets = { 'no-strict-blocking': 0, 'no-popups': 2, 'no-cosmetic-filtering': 4, 'no-remote-fonts': 6, 'no-large-media': 8 }; var fromLegacySwitchNames = { 'dontBlockDoc': 'no-strict-blocking', 'doBlockAllPopups': 'no-popups', 'noStrictBlocking': 'no-strict-blocking', 'noPopups': 'no-popups', 'noCosmeticFiltering': 'no-cosmetic-filtering' }; var switchStateToNameMap = { '1': 'true', '2': 'false' }; var nameToSwitchStateMap = { 'true': 1, 'false': 2, 'on': 1, 'off': 2 }; /******************************************************************************/ // For performance purpose, as simple tests as possible var reHostnameVeryCoarse = /[g-z_-]/; var reIPv4VeryCoarse = /\.\d+$/; // http://tools.ietf.org/html/rfc5952 // 4.3: "MUST be represented in lowercase" // Also: http://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers var isIPAddress = function(hostname) { if ( reHostnameVeryCoarse.test(hostname) ) { return false; } if ( reIPv4VeryCoarse.test(hostname) ) { return true; } return hostname.startsWith('['); }; var toBroaderHostname = function(hostname) { var pos = hostname.indexOf('.'); if ( pos !== -1 ) { return hostname.slice(pos + 1); } return hostname !== '*' && hostname !== '' ? '*' : ''; }; var toBroaderIPAddress = function(ipaddress) { return ipaddress !== '*' && ipaddress !== '' ? '*' : ''; }; var selectHostnameBroadener = function(hostname) { return isIPAddress(hostname) ? toBroaderIPAddress : toBroaderHostname; }; /******************************************************************************/ HnSwitches.prototype.reset = function() { this.switches = {}; this.n = ''; this.z = ''; this.r = 0; }; /******************************************************************************/ // If value is undefined, the switch is removed HnSwitches.prototype.toggle = function(switchName, hostname, newVal) { var bitOffset = switchBitOffsets[switchName]; if ( bitOffset === undefined ) { return false; } if ( newVal === this.evaluate(switchName, hostname) ) { return false; } var bits = this.switches[hostname] || 0; bits &= ~(3 << bitOffset); bits |= newVal << bitOffset; if ( bits === 0 ) { delete this.switches[hostname]; } else { this.switches[hostname] = bits; } return true; }; /******************************************************************************/ HnSwitches.prototype.toggleOneZ = function(switchName, hostname, newState) { var bitOffset = switchBitOffsets[switchName]; if ( bitOffset === undefined ) { return false; } var state = this.evaluateZ(switchName, hostname); if ( newState === state ) { return false; } if ( newState === undefined ) { newState = !state; } var bits = this.switches[hostname] || 0; bits &= ~(3 << bitOffset); if ( bits === 0 ) { delete this.switches[hostname]; } else { this.switches[hostname] = bits; } state = this.evaluateZ(switchName, hostname); if ( state === newState ) { return true; } this.switches[hostname] = bits | ((newState ? 1 : 2) << bitOffset); return true; }; /******************************************************************************/ HnSwitches.prototype.toggleBranchZ = function(switchName, targetHostname, newState) { var changed = this.toggleOneZ(switchName, targetHostname, newState); var targetLen = targetHostname.length; // Turn off all descendant switches, they will inherit the state of the // branch's origin. for ( var hostname in this.switches ) { if ( this.switches.hasOwnProperty(hostname) === false ) { continue; } if ( hostname === targetHostname ) { continue; } if ( hostname.length <= targetLen ) { continue; } if ( hostname.endsWith(targetHostname) === false ) { continue; } if ( hostname.charAt(hostname.length - targetLen - 1) !== '.' ) { continue; } changed = this.toggle(switchName, hostname, 0) || changed; } return changed; }; /******************************************************************************/ HnSwitches.prototype.toggleZ = function(switchName, hostname, deep, newState) { if ( deep === true ) { return this.toggleBranchZ(switchName, hostname, newState); } return this.toggleOneZ(switchName, hostname, newState); }; /******************************************************************************/ // 0 = inherit from broader scope, up to default state // 1 = non-default state // 2 = forced default state (to override a broader non-default state) HnSwitches.prototype.evaluate = function(switchName, hostname) { var bits = this.switches[hostname] || 0; if ( bits === 0 ) { return 0; } var bitOffset = switchBitOffsets[switchName]; if ( bitOffset === undefined ) { return 0; } return (bits >>> bitOffset) & 3; }; /******************************************************************************/ HnSwitches.prototype.evaluateZ = function(switchName, hostname) { var bitOffset = switchBitOffsets[switchName]; if ( bitOffset === undefined ) { this.r = 0; return false; } this.n = switchName; var bits, hn = hostname, broadenSource = selectHostnameBroadener(hn); for (;;) { bits = this.switches[hn] || 0; if ( bits !== 0 ) { bits = bits >>> bitOffset & 3; if ( bits !== 0 ) { this.z = hn; this.r = bits; return bits === 1; } } hn = broadenSource(hn); if ( hn === '' ) { break; } } this.r = 0; return false; }; /******************************************************************************/ HnSwitches.prototype.toResultString = function() { return this.r !== 1 ? '' : 'ub:' + this.n + ': ' + this.z + ' true'; }; /******************************************************************************/ HnSwitches.prototype.toString = function() { var out = [], switchName, val, hostname, toUnicode = punycode.toUnicode; for ( hostname in this.switches ) { if ( this.switches.hasOwnProperty(hostname) === false ) { continue; } for ( switchName in switchBitOffsets ) { if ( switchBitOffsets.hasOwnProperty(switchName) === false ) { continue; } val = this.evaluate(switchName, hostname); if ( val === 0 ) { continue; } if ( hostname.indexOf('xn--') !== -1 ) { hostname = toUnicode(hostname); } out.push(switchName + ': ' + hostname + ' ' + switchStateToNameMap[val]); } } return out.join('\n'); }; /******************************************************************************/ HnSwitches.prototype.fromString = function(text) { var lineIter = new µBlock.LineIterator(text), line, pos, fields, switchName, hostname, state, reNotASCII = /[^\x20-\x7F]/, toASCII = punycode.toASCII; this.reset(); while ( lineIter.eot() === false ) { line = lineIter.next().trim(); pos = line.indexOf('# '); if ( pos !== -1 ) { line = line.slice(0, pos).trim(); } if ( line === '' ) { continue; } fields = line.split(/\s+/); if ( fields.length !== 3 ) { continue; } switchName = fields[0]; pos = switchName.indexOf(':'); if ( pos === -1 ) { continue; } switchName = switchName.slice(0, pos); switchName = fromLegacySwitchNames[switchName] || switchName; if ( switchBitOffsets.hasOwnProperty(switchName) === false ) { continue; } // Performance: avoid punycoding if hostname is made only of // ASCII characters. hostname = fields[1]; if ( reNotASCII.test(hostname) ) { hostname = toASCII(hostname); } state = fields[2]; if ( nameToSwitchStateMap.hasOwnProperty(state) === false ) { continue; } this.toggle(switchName, hostname, nameToSwitchStateMap[state]); } }; /******************************************************************************/ return HnSwitches; /******************************************************************************/ })(); /******************************************************************************/ µBlock.hnSwitches = new µBlock.HnSwitches(); /******************************************************************************/