From b10ac0020da120d158481e657dce3f7ec78d0aec Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sun, 11 Mar 2018 10:59:39 -0400 Subject: [PATCH] make use of CodeMirror in "My rules" pane --- src/about.html | 1 + src/css/codemirror.css | 11 +- src/css/dyna-rules.css | 144 ++++-------- src/dyna-rules.html | 40 ++-- src/js/dyna-rules.js | 382 +++++++++++++++++++------------- src/js/dynamic-net-filtering.js | 287 +++++++++++------------- src/js/hnswitches.js | 143 ++++++------ src/js/messaging.js | 109 +++++---- src/js/url-net-filtering.js | 83 ++++--- 9 files changed, 593 insertions(+), 607 deletions(-) diff --git a/src/about.html b/src/about.html index cfa4819e4..e929c5542 100644 --- a/src/about.html +++ b/src/about.html @@ -24,6 +24,7 @@
  • Punycode.js by Mathias Bynens
  • Font Awesome by Dave Gandy
  • CodeMirror by Marijn Haverbeke +
  • An implementation of Myers' diff algorithm by Arpad Borsos diff --git a/src/css/codemirror.css b/src/css/codemirror.css index 772a91140..4a53399a2 100644 --- a/src/css/codemirror.css +++ b/src/css/codemirror.css @@ -12,7 +12,7 @@ } /* For when panels are used */ -.codeMirrorContainer > div:not(.CodeMirror) { +.codeMirrorContainer > div:not([class^="CodeMirror"]) { display: flex; flex-direction: column; height: 100%; @@ -63,3 +63,12 @@ .cm-search-widget .cm-search-widget-button:hover { color: #000; } + +.CodeMirror-merge-l-deleted { + background-image: none; + font-weight: bold; + } +.CodeMirror-merge-l-inserted { + background-image: none; + font-weight: bold; + } diff --git a/src/css/dyna-rules.css b/src/css/dyna-rules.css index 033faf9ea..4f0f7207b 100644 --- a/src/css/dyna-rules.css +++ b/src/css/dyna-rules.css @@ -1,38 +1,40 @@ -div > p:first-child { - margin-top: 0; +body { + bottom: 0; + display: flex; + left: 0; + position: absolute; + right: 0; + top: 0; + flex-direction: column; } -div > p:last-child { - margin-bottom: 0; - } -code { - background-color: #eee; - font: 11px monospace; - padding: 2px 4px; -} #diff { border: 0; border-top: 1px solid #eee; + flex-grow: 1; margin: 0; - padding: 0.5em 0 0 0; + padding: 0; white-space: nowrap; } -#diff .pane { +#diff .tools > * { + margin-bottom: 0.5em; + } +#diff .tools .fa { + font-size: large; + } +#diff .ruleActions { border: 0; box-sizing: border-box; display: inline-block; - font: 90%/180% "Noto Mono",monospace; - margin: 0; padding: 0; - position: relative; - white-space: normal; + text-align: center; + vertical-align: top; width: 50%; } -#diff .pane .rulesContainer { - position: relative; - min-height: 150px; /* too short is confusing */ +#diff .ruleActions h3 { + font-weight: normal; + margin: 0.5em 0; } -#diff .ruleActions { - padding: 0 0 1em 0; +#diff .ruleFilter { text-align: center; } body[dir="ltr"] #revertButton:after { @@ -73,97 +75,33 @@ body[dir="rtl"] #commitButton:before { } #revertButton, #commitButton, -#diff.edit #editEnterButton { +#diff.editing #exportButton, +#diff.editing #importButton, +#editSaveButton { opacity: 0.25; pointer-events: none; } -#editStopButton, -#editCancelButton { - display: none; - } -#diff.dirty:not(.edit) #revertButton, -#diff.dirty:not(.edit) #commitButton { +#diff.dirty:not(.editing) #revertButton, +#diff.dirty:not(.editing) #commitButton, +#diff.editing #editSaveButton { opacity: 1; pointer-events: auto; } -#diff.edit #editStopButton, -#diff.edit #editCancelButton { - display: initial; + +.codeMirrorContainer { + height: 60vh; } -#diff.edit #importButton, -#diff.edit #exportButton { +.CodeMirror-merge, .CodeMirror-merge-pane, .CodeMirror-merge .CodeMirror { + box-sizing: border-box; + height: 100%; + } +#diff.editing .CodeMirror-merge-copy, +#diff.editing .CodeMirror-merge-copy-reverse { display: none; } -#diff ul { - border: 0; - border-top: 1px solid #eee; - list-style-type: none; - margin: 0; - overflow: hidden; - padding: 1em 0 0 0; - } -#diff.edit .right ul { - visibility: hidden; - } -#diff .left { - border-right: 1px solid #eee; - } -#diff .right > ul { +#diff.editing .CodeMirror-merge-left .CodeMirror { color: #888; } -#diff li { - background-color: #ddd; - direction: ltr; - padding: 0; - text-align: left; - white-space: nowrap; - padding-left: 3px; /* a bit of padding; must also be in textarea */ - } -#diff li:nth-child(even) { - background-color: #eee; - } -#diff .right li { - opacity: 0.5; - } -#diff .right li:hover { - } -#diff .right li.notLeft { - color: #000; - opacity: 1; - } -#diff .right li.notRight { - color: #000; - } -#diff .right li.toRemove { - color: #000; - text-decoration: line-through; - opacity: 1; - } -#diff textarea { - background-color: #f8f8ff; - border: 0; - border-top: 1px solid #eee; - box-sizing: border-box; - color: black; - direction: ltr; - font: inherit; - height: 100%; - left: 0; - margin: 0; - overflow: hidden; - overflow-y: auto; - padding: 1em 0 0 3px; /* same left and top padding as ul/li */ - position: absolute; - resize: none; - top: 0; - visibility: hidden; - white-space: pre; /* this implies nowrap; break only on \n and
    . - nowrap doesn't consistently - respect \n's (example: Safari) per the CSS spec: - http://www.w3.org/wiki/CSS/Properties/white-space */ - width: 100%; - word-wrap: normal; - } -#diff.edit textarea { - visibility: visible; +#diff.editing .CodeMirror-merge-editor .CodeMirror { + background-color: #ffe; } diff --git a/src/dyna-rules.html b/src/dyna-rules.html index 66eb7b7c9..b9e156ce3 100644 --- a/src/dyna-rules.html +++ b/src/dyna-rules.html @@ -4,51 +4,49 @@ uBlock — Dynamic filtering rules + + + + +
    -

    -

    +

    -
    +
    -

    +

    -
    -
      -
      -
      -
      -

      +

      - - - +
      -
      - -
        -
        +
        +
        +
        +
        + + + + + diff --git a/src/js/dyna-rules.js b/src/js/dyna-rules.js index 30e1b0224..c598d99b5 100644 --- a/src/js/dyna-rules.js +++ b/src/js/dyna-rules.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2016 Raymond Hill + Copyright (C) 2014-2018 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 @@ -19,90 +19,142 @@ Home: https://github.com/gorhill/uMatrix */ -/* global uDom, uBlockDashboard */ - -/******************************************************************************/ - -(function() { +/* global diff_match_patch, CodeMirror, uDom, uBlockDashboard */ 'use strict'; /******************************************************************************/ -var messaging = vAPI.messaging; +(function() { /******************************************************************************/ -var renderRules = function(details) { - var liTemplate = uDom('#templates > ul > li'); - var ulLeft = uDom('#diff > .left ul').empty().remove(); - var ulRight = uDom('#diff > .right ul').empty().remove(); - var liLeft, liRight; - var rules, rule, i; +var messaging = vAPI.messaging; - // Switches always displayed first -- just like in uMatrix - // Merge url rules and switches: they just look the same - rules = details.hnSwitches.split(/\n+/).sort(); - - for ( i = 0; i < rules.length; i++ ) { - rule = rules[i]; - liLeft = liTemplate.clone().text(rule); - liRight = liTemplate.clone().text(rule); - ulLeft.append(liLeft); - ulRight.append(liRight); +var mergeView = new CodeMirror.MergeView( + document.querySelector('.codeMirrorMergeContainer'), + { + allowEditingOriginals: true, + connect: 'align', // size of svg is not managed properly with `true` + inputStyle: 'contenteditable', + lineNumbers: true, + lineWrapping: false, + origLeft: '', + revertButtons: true, + value: '' } +); +mergeView.editor().setOption('styleActiveLine', true); +mergeView.editor().setOption('lineNumbers', false); +mergeView.leftOriginal().setOption('readOnly', 'nocursor'); - // Firewall rules follow - var allRules = {}; - var permanentRules = {}; - var sessionRules = {}; - var onLeft, onRight; +var cleanToken = 0; +var cleanEditText = ''; - rules = details.sessionRules.split(/\n+/); - i = rules.length; +var differ; + +/******************************************************************************/ + +// Incrementally update text in a CodeMirror editor for best user experience: +// - Scroll position preserved +// - Minimum amount of text updated + +var rulesToDoc = function(doc, rules) { + if ( doc.getValue() === '' || rules.length === 0 ) { + doc.setValue(rules.length !== 0 ? rules.join('\n') : ''); + return; + } + if ( differ === undefined ) { differ = new diff_match_patch(); } + var beforeText = doc.getValue(); + var afterText = rules.join('\n'); + var diffs = differ.diff_main(beforeText, afterText); + doc.startOperation(); + var i = diffs.length, + iedit = beforeText.length; while ( i-- ) { - rule = rules[i].trim(); - if ( rule === '' ) { + var diff = diffs[i]; + if ( diff[0] === 0 ) { + iedit -= diff[1].length; continue; } - sessionRules[rule] = allRules[rule] = true; - } - details.sessionRules = rules.sort().join('\n'); - - rules = details.permanentRules.split(/\n+/); - i = rules.length; - while ( i-- ) { - rule = rules[i].trim(); - if ( rule === '' ) { + var end = doc.posFromIndex(iedit); + if ( diff[0] === 1 ) { + doc.replaceRange(diff[1], end, end); continue; } - permanentRules[rule] = allRules[rule] = true; + /* diff[0] === -1 */ + iedit -= diff[1].length; + var beg = doc.posFromIndex(iedit); + doc.replaceRange('', beg, end); } - details.permanentRules = rules.sort().join('\n'); + doc.endOperation(); +}; - rules = Object.keys(allRules).sort(); - for ( i = 0; i < rules.length; i++ ) { - rule = rules[i]; - onLeft = permanentRules.hasOwnProperty(rule); - onRight = sessionRules.hasOwnProperty(rule); - liLeft = liTemplate.clone(); - liRight = liTemplate.clone(); - if ( onLeft && onRight ) { - liLeft.text(rule); - liRight.text(rule); - } else if ( onLeft ) { - liLeft.text(rule); - liRight.text(rule).addClass('notRight toRemove'); - } else { - liRight.text(rule).addClass('notLeft'); +/******************************************************************************/ + +var renderRules = (function() { + var firstVisit = true; + + return function(details) { + details.hnSwitches.sort(); + details.permanentRules.sort(); + details.sessionRules.sort(); + var orig = details.hnSwitches.concat(details.permanentRules), + edit = details.hnSwitches.concat(details.sessionRules); + rulesToDoc(mergeView.leftOriginal(), orig); + rulesToDoc(mergeView.editor(), edit); + cleanEditText = mergeView.editor().getValue().trim(); + if ( firstVisit ) { + mergeView.editor().clearHistory(); + firstVisit = false; + mergeView.editor().execCommand('goNextDiff'); } - ulLeft.append(liLeft); - ulRight.append(liRight); - } + cleanToken = mergeView.editor().changeGeneration(); + onChange(true); + }; +})(); - uDom('#diff > .left > .rulesContainer').append(ulLeft); - uDom('#diff > .right > .rulesContainer').append(ulRight); - uDom('#diff').toggleClass('dirty', details.sessionRules !== details.permanentRules); +/******************************************************************************/ + +var applyDiff = function(permanent, toAdd, toRemove, callback) { + messaging.send( + 'dashboard', + { + what: 'modifyRuleset', + permanent: permanent, + toAdd: toAdd, + toRemove: toRemove + }, + callback + ); +}; + +/******************************************************************************/ + +// CodeMirror quirk: sometimes fromStart.ch and/or toStart.ch is undefined. +// When this happens, use 0. + +mergeView.options.revertChunk = function( + mv, + from, fromStart, fromEnd, + to, toStart, toEnd +) { + if ( typeof fromStart.ch !== 'number' ) { fromStart.ch = 0; } + if ( fromEnd.ch !== 0 ) { fromEnd.line += 1; } + var toAdd = from.getRange( + { line: fromStart.line, ch: 0 }, + { line: fromEnd.line, ch: 0 } + ); + if ( typeof toStart.ch !== 'number' ) { toStart.ch = 0; } + if ( toEnd.ch !== 0 ) { toEnd.line += 1; } + var toRemove = to.getRange( + { line: toStart.line, ch: 0 }, + { line: toEnd.line, ch: 0 } + ); + applyDiff(from === mv.editor(), toAdd, toRemove); + to.replaceRange(toAdd, toStart, toEnd); + cleanToken = mergeView.editor().changeGeneration(); + cleanEditText = mergeView.editor().getValue().trim(); }; /******************************************************************************/ @@ -121,19 +173,11 @@ function handleImportFilePicker() { .replace(/\|/g, ' ') .replace(/\n/g, ' * noop\n'); } - var request = { - 'what': 'setSessionRules', - 'rules': rulesFromHTML('#diff .right li') + '\n' + result - }; - messaging.send('dashboard', request, renderRules); + applyDiff(false, result, '', renderRules); }; var file = this.files[0]; - if ( file === undefined || file.name === '' ) { - return; - } - if ( file.type.indexOf('text') !== 0 ) { - return; - } + if ( file === undefined || file.name === '' ) { return; } + if ( file.type.indexOf('text') !== 0 ) { return; } var fr = new FileReader(); fr.onload = fileReaderOnLoadHandler; fr.readAsText(file); @@ -157,98 +201,137 @@ function exportUserRulesToFile() { .replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString()) .replace(/ +/g, '_'); vAPI.download({ - 'url': 'data:text/plain,' + encodeURIComponent(rulesFromHTML('#diff .left li') + '\n'), - 'filename': filename, - 'saveAs': true + url: 'data:text/plain,' + encodeURIComponent( + mergeView.leftOriginal().getValue().trim() + '\n' + ), + filename: filename, + saveAs: true }); } /******************************************************************************/ -var rulesFromHTML = function(selector) { - var rules = []; - var lis = uDom(selector); - var li; - for ( var i = 0; i < lis.length; i++ ) { - li = lis.at(i); - if ( li.hasClassName('toRemove') ) { - rules.push(''); - } else { - rules.push(li.text()); +/* +var onFilter = (function() { + var timer; + + var process = function() { + timer = undefined; + }; + + return function() { + if ( timer !== undefined ) { clearTimeout(timer); } + timer = vAPI.setTimeout(process, 577); + }; +})(); +*/ + +/******************************************************************************/ + +var onChange = (function() { + var timer; + + var process = function(now) { + timer = undefined; + var isClean = mergeView.editor().isClean(cleanToken); + var diff = document.getElementById('diff'); + if ( + now && + isClean === false && + mergeView.editor().getValue().trim() === cleanEditText + ) { + cleanToken = mergeView.editor().changeGeneration(); + isClean = true; } + diff.classList.toggle('editing', isClean === false); + diff.classList.toggle('dirty', mergeView.leftChunks().length !== 0); + CodeMirror.commands.save = isClean ? undefined : editSaveHandler; + }; + + return function(now) { + if ( timer !== undefined ) { clearTimeout(timer); } + timer = now ? process(now) : vAPI.setTimeout(process, 57); + }; +})(); + +/******************************************************************************/ + +var revertAllHandler = function() { + var toAdd = [], toRemove = []; + var left = mergeView.leftOriginal(), + edit = mergeView.editor(); + for ( var chunk of mergeView.leftChunks() ) { + var addedLines = left.getRange( + { line: chunk.origFrom, ch: 0 }, + { line: chunk.origTo, ch: 0 } + ); + var removedLines = edit.getRange( + { line: chunk.editFrom, ch: 0 }, + { line: chunk.editTo, ch: 0 } + ); + toAdd.push(addedLines.trim()); + toRemove.push(removedLines.trim()); } - return rules.join('\n').trim(); + applyDiff(false, toAdd.join('\n'), toRemove.join('\n'), renderRules); }; /******************************************************************************/ -var revertHandler = function() { - var request = { - 'what': 'setSessionRules', - 'rules': rulesFromHTML('#diff .left li') - }; - messaging.send('dashboard', request, renderRules); +var commitAllHandler = function() { + var toAdd = [], toRemove = []; + var left = mergeView.leftOriginal(), + edit = mergeView.editor(); + for ( var chunk of mergeView.leftChunks() ) { + var addedLines = edit.getRange( + { line: chunk.editFrom, ch: 0 }, + { line: chunk.editTo, ch: 0 } + ); + var removedLines = left.getRange( + { line: chunk.origFrom, ch: 0 }, + { line: chunk.origTo, ch: 0 } + ); + toAdd.push(addedLines.trim()); + toRemove.push(removedLines.trim()); + } + applyDiff(true, toAdd.join('\n'), toRemove.join('\n'), renderRules); }; /******************************************************************************/ -var commitHandler = function() { - var request = { - 'what': 'setPermanentRules', - 'rules': rulesFromHTML('#diff .right li') - }; - messaging.send('dashboard', request, renderRules); -}; - -/******************************************************************************/ - -var editStartHandler = function() { - var parent = uDom(this).ancestors('#diff'); - // If we're already editing, don't reset - if ( parent.hasClassName('edit') ) { +var editSaveHandler = function() { + var editor = mergeView.editor(); + var editText = editor.getValue().trim(); + if ( editText === cleanEditText ) { + onChange(true); return; } - uDom('#diff .right textarea').val(rulesFromHTML('#diff .right li')); - parent.toggleClass('edit', true); -}; - -/******************************************************************************/ - -var editStopHandler = function() { - var parent = uDom(this).ancestors('#diff'); - parent.toggleClass('edit', false); - var request = { - 'what': 'setSessionRules', - 'rules': uDom('#diff .right textarea').val() - }; - messaging.send('dashboard', request, renderRules); -}; - -/******************************************************************************/ - -var editCancelHandler = function() { - var parent = uDom(this).ancestors('#diff'); - parent.toggleClass('edit', false); + if ( differ === undefined ) { differ = new diff_match_patch(); } + var toAdd = [], toRemove = []; + var diffs = differ.diff_main(cleanEditText, editText); + for ( var diff of diffs ) { + if ( diff[0] === 1 ) { + toAdd.push(diff[1]); + } else if ( diff[0] === -1 ) { + toRemove.push(diff[1]); + } + } + applyDiff(false, toAdd.join(''), toRemove.join(''), renderRules); }; /******************************************************************************/ var getCloudData = function() { - return rulesFromHTML('#diff .left li'); + return mergeView.leftOriginal().getValue().trim(); }; var setCloudData = function(data, append) { - if ( typeof data !== 'string' ) { - return; - } - if ( append ) { - data = rulesFromHTML('#diff .right li') + '\n' + data; - } - var request = { - 'what': 'setSessionRules', - 'rules': data - }; - messaging.send('dashboard', request, renderRules); + if ( typeof data !== 'string' ) { return; } + applyDiff( + false, + data, + append ? '' : mergeView.editor().getValue().trim(), + renderRules + ); }; self.cloud.onPush = getCloudData; @@ -256,19 +339,18 @@ self.cloud.onPull = setCloudData; /******************************************************************************/ +messaging.send('dashboard', { what: 'getRules' }, renderRules); + // Handle user interaction uDom('#importButton').on('click', startImportFilePicker); uDom('#importFilePicker').on('change', handleImportFilePicker); uDom('#exportButton').on('click', exportUserRulesToFile); +uDom('#revertButton').on('click', revertAllHandler); +uDom('#commitButton').on('click', commitAllHandler); +uDom('#editSaveButton').on('click', editSaveHandler); -uDom('#revertButton').on('click', revertHandler); -uDom('#commitButton').on('click', commitHandler); -uDom('#editEnterButton').on('click', editStartHandler); -uDom('#diff > .pane.right > .rulesContainer').on('dblclick', editStartHandler); -uDom('#editStopButton').on('click', editStopHandler); -uDom('#editCancelButton').on('click', editCancelHandler); - -messaging.send('dashboard', { what: 'getRules' }, renderRules); +// https://groups.google.com/forum/#!topic/codemirror/UQkTrt078Vs +mergeView.editor().on('updateDiff', function() { onChange(); }); /******************************************************************************/ diff --git a/src/js/dynamic-net-filtering.js b/src/js/dynamic-net-filtering.js index 34bad0df4..0e6199907 100644 --- a/src/js/dynamic-net-filtering.js +++ b/src/js/dynamic-net-filtering.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2017 Raymond Hill + Copyright (C) 2014-2018 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 @@ -30,10 +30,6 @@ /******************************************************************************/ -var magicId = 'chmdgxwtetgu'; - -/******************************************************************************/ - var Matrix = function() { this.reset(); }; @@ -77,6 +73,7 @@ var nameToActionMap = { var reHostnameVeryCoarse = /[g-z_-]/; var reIPv4VeryCoarse = /\.\d+$/; var reBadHostname = /[^0-9a-z_.\[\]:%-]/; +var reNotASCII = /[^\x20-\x7F]/; // http://tools.ietf.org/html/rfc5952 // 4.3: "MUST be represented in lowercase" @@ -115,78 +112,69 @@ Matrix.prototype.reset = function() { this.type = ''; this.y = ''; this.z = ''; - this.rules = {}; + this.rules = new Map(); + this.changed = false; }; /******************************************************************************/ Matrix.prototype.assign = function(other) { - var thisRules = this.rules; - var otherRules = other.rules; - var k; - // Remove rules not in other - for ( k in thisRules ) { - if ( thisRules.hasOwnProperty(k) === false ) { - continue; - } - if ( otherRules.hasOwnProperty(k) === false ) { - delete thisRules[k]; + for ( var k of this.rules.keys() ) { + if ( other.rules.has(k) === false ) { + this.rules.delete(k); + this.changed = true; } } - // Add/change rules in other - for ( k in otherRules ) { - if ( otherRules.hasOwnProperty(k) === false ) { - continue; + for ( var entry of other.rules ) { + if ( this.rules.get(entry[0]) !== entry[1] ) { + this.rules.set(entry[0], entry[1]); + this.changed = true; } - thisRules[k] = otherRules[k]; } }; /******************************************************************************/ Matrix.prototype.copyRules = function(other, srcHostname, desHostnames) { - var thisRules = this.rules; - var otherRules = other.rules; - var ruleKey, ruleValue; // Specific types - ruleValue = otherRules['* *'] || 0; - if ( ruleValue !== 0 ) { - thisRules['* *'] = ruleValue; + var bits = other.rules.get('* *'); + if ( bits !== undefined ) { + this.rules.set('* *', bits); } else { - delete thisRules['* *']; + this.rules.delete('* *'); } - ruleKey = srcHostname + ' *'; - ruleValue = otherRules[ruleKey] || 0; - if ( ruleValue !== 0 ) { - thisRules[ruleKey] = ruleValue; + var key = srcHostname + ' *'; + bits = other.rules.get(key); + if ( bits !== undefined ) { + this.rules.set(key, bits); } else { - delete thisRules[ruleKey]; + this.rules.delete(key); } // Specific destinations for ( var desHostname in desHostnames ) { - if ( desHostnames.hasOwnProperty(desHostname) === false ) { - continue; - } - ruleKey = '* ' + desHostname; - ruleValue = otherRules[ruleKey] || 0; - if ( ruleValue !== 0 ) { - thisRules[ruleKey] = ruleValue; + if ( desHostnames.hasOwnProperty(desHostname) === false ) { continue; } + key = '* ' + desHostname; + bits = other.rules.get(key); + if ( bits !== undefined ) { + this.rules.set(key, bits); } else { - delete thisRules[ruleKey]; + this.rules.delete(key); } - ruleKey = srcHostname + ' ' + desHostname ; - ruleValue = otherRules[ruleKey] || 0; - if ( ruleValue !== 0 ) { - thisRules[ruleKey] = ruleValue; + key = srcHostname + ' ' + desHostname ; + bits = other.rules.get(key); + if ( bits !== undefined ) { + this.rules.set(key, bits); } else { - delete thisRules[ruleKey]; + this.rules.delete(key); } } + this.changed = true; + return true; }; @@ -198,28 +186,25 @@ Matrix.prototype.copyRules = function(other, srcHostname, desHostnames) { // - from to * Matrix.prototype.hasSameRules = function(other, srcHostname, desHostnames) { - var thisRules = this.rules; - var otherRules = other.rules; - var ruleKey; // Specific types - ruleKey = '* *'; - if ( (thisRules[ruleKey] || 0) !== (otherRules[ruleKey] || 0) ) { + var key = '* *'; + if ( this.rules.get(key) !== other.rules.get(key) ) { return false; } - ruleKey = srcHostname + ' *'; - if ( (thisRules[ruleKey] || 0) !== (otherRules[ruleKey] || 0) ) { + key = srcHostname + ' *'; + if ( this.rules.get(key) !== other.rules.get(key) ) { return false; } // Specific destinations for ( var desHostname in desHostnames ) { - ruleKey = '* ' + desHostname; - if ( (thisRules[ruleKey] || 0) !== (otherRules[ruleKey] || 0) ) { + key = '* ' + desHostname; + if ( this.rules.get(key) !== other.rules.get(key) ) { return false; } - ruleKey = srcHostname + ' ' + desHostname ; - if ( (thisRules[ruleKey] || 0) !== (otherRules[ruleKey] || 0) ) { + key = srcHostname + ' ' + desHostname ; + if ( this.rules.get(key) !== other.rules.get(key) ) { return false; } } @@ -232,19 +217,17 @@ Matrix.prototype.hasSameRules = function(other, srcHostname, desHostnames) { Matrix.prototype.setCell = function(srcHostname, desHostname, type, state) { var bitOffset = typeBitOffsets[type]; var k = srcHostname + ' ' + desHostname; - var oldBitmap = this.rules[k]; - if ( oldBitmap === undefined ) { - oldBitmap = 0; - } + var oldBitmap = this.rules.get(k) || 0; var newBitmap = oldBitmap & ~(3 << bitOffset) | (state << bitOffset); if ( newBitmap === oldBitmap ) { return false; } if ( newBitmap === 0 ) { - delete this.rules[k]; + this.rules.delete(k); } else { - this.rules[k] = newBitmap; + this.rules.set(k, newBitmap); } + this.changed = true; return true; }; @@ -256,6 +239,7 @@ Matrix.prototype.unsetCell = function(srcHostname, desHostname, type) { return false; } this.setCell(srcHostname, desHostname, type, 0); + this.changed = true; return true; }; @@ -265,7 +249,7 @@ Matrix.prototype.unsetCell = function(srcHostname, desHostname, type) { Matrix.prototype.evaluateCell = function(srcHostname, desHostname, type) { var key = srcHostname + ' ' + desHostname; - var bitmap = this.rules[key]; + var bitmap = this.rules.get(key); if ( bitmap === undefined ) { return 0; } @@ -314,7 +298,7 @@ Matrix.prototype.evaluateCellZ = function(srcHostname, desHostname, type, broade var v; for (;;) { this.z = s; - v = this.rules[s + ' ' + desHostname]; + v = this.rules.get(s + ' ' + desHostname); if ( v !== undefined ) { v = v >>> bitOffset & 3; if ( v !== 0 ) { @@ -478,22 +462,15 @@ Matrix.prototype.desHostnameFromRule = function(rule) { /******************************************************************************/ -Matrix.prototype.toString = function() { +Matrix.prototype.toArray = function() { var out = [], - rule, type, val, - srcHostname, desHostname, toUnicode = punycode.toUnicode; - for ( rule in this.rules ) { - if ( this.rules.hasOwnProperty(rule) === false ) { - continue; - } - srcHostname = this.srcHostnameFromRule(rule); - desHostname = this.desHostnameFromRule(rule); - for ( type in typeBitOffsets ) { - if ( typeBitOffsets.hasOwnProperty(type) === false ) { - continue; - } - val = this.evaluateCell(srcHostname, desHostname, type); + for ( var key of this.rules.keys() ) { + var srcHostname = this.srcHostnameFromRule(key); + var desHostname = this.desHostnameFromRule(key); + for ( var type in typeBitOffsets ) { + if ( typeBitOffsets.hasOwnProperty(type) === false ) { continue; } + var val = this.evaluateCell(srcHostname, desHostname, type); if ( val === 0 ) { continue; } if ( srcHostname.indexOf('xn--') !== -1 ) { srcHostname = toUnicode(srcHostname); @@ -509,108 +486,92 @@ Matrix.prototype.toString = function() { ); } } - return out.join('\n'); + return out; +}; + +Matrix.prototype.toString = function() { + return this.toArray().join('\n'); }; /******************************************************************************/ Matrix.prototype.fromString = function(text, append) { - var lineIter = new µBlock.LineIterator(text), - line, pos, fields, - srcHostname, desHostname, type, action, - reNotASCII = /[^\x20-\x7F]/, - toASCII = punycode.toASCII; - - if ( append !== true ) { - this.reset(); - } - + var lineIter = new µBlock.LineIterator(text); + if ( append !== true ) { 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; - } - - // URL net filtering rules - if ( line.indexOf('://') !== -1 ) { - continue; - } - - // Valid rule syntax: - - // srcHostname desHostname type state - // type = a valid request type - // state = [`block`, `allow`, `noop`] - - // Lines with invalid syntax silently ignored - - fields = line.split(/\s+/); - if ( fields.length !== 4 ) { - continue; - } - - // Ignore special rules: - // hostname-based switch rules - if ( fields[0].endsWith(':') ) { - continue; - } - - // Performance: avoid punycoding if hostnames are made only of - // ASCII characters. - srcHostname = fields[0]; - if ( reNotASCII.test(srcHostname) ) { - srcHostname = toASCII(srcHostname); - } - desHostname = fields[1]; - if ( reNotASCII.test(desHostname) ) { - desHostname = toASCII(desHostname); - } - - // https://github.com/chrisaljoudi/uBlock/issues/1082 - // Discard rules with invalid hostnames - if ( (srcHostname !== '*' && reBadHostname.test(srcHostname)) || - (desHostname !== '*' && reBadHostname.test(desHostname)) - ) { - continue; - } - - type = fields[2]; - if ( typeBitOffsets.hasOwnProperty(type) === false ) { - continue; - } - - // https://github.com/chrisaljoudi/uBlock/issues/840 - // Discard invalid rules - if ( desHostname !== '*' && type !== '*' ) { - continue; - } - - action = nameToActionMap[fields[3]]; - if ( typeof action !== 'number' || action < 0 || action > 3 ) { - continue; - } - - this.setCell(srcHostname, desHostname, type, action); + this.addFromRuleParts(lineIter.next().trim().split(/\s+/)); } }; /******************************************************************************/ +Matrix.prototype.validateRuleParts = function(parts) { + if ( parts.length < 4 ) { return; } + + // Ignore hostname-based switch rules + if ( parts[0].endsWith(':') ) { return; } + + // Ignore URL-based rules + if ( parts[1].indexOf('/') !== -1 ) { return; } + + if ( typeBitOffsets.hasOwnProperty(parts[2]) === false ) { return; } + + if ( nameToActionMap.hasOwnProperty(parts[3]) === false ) { return; } + + // https://github.com/chrisaljoudi/uBlock/issues/840 + // Discard invalid rules + if ( parts[1] !== '*' && parts[2] !== '*' ) { return; } + + // Performance: avoid punycoding if hostnames are made only of ASCII chars. + if ( reNotASCII.test(parts[0]) ) { parts[0] = punycode.toASCII(parts[0]); } + if ( reNotASCII.test(parts[1]) ) { parts[1] = punycode.toASCII(parts[1]); } + + // https://github.com/chrisaljoudi/uBlock/issues/1082 + // Discard rules with invalid hostnames + if ( + (parts[0] !== '*' && reBadHostname.test(parts[0])) || + (parts[1] !== '*' && reBadHostname.test(parts[1])) + ) { + return; + } + + return parts; +}; + +/******************************************************************************/ + +Matrix.prototype.addFromRuleParts = function(parts) { + if ( this.validateRuleParts(parts) !== undefined ) { + this.setCell(parts[0], parts[1], parts[2], nameToActionMap[parts[3]]); + return true; + } + return false; +}; + +Matrix.prototype.removeFromRuleParts = function(parts) { + if ( this.validateRuleParts(parts) !== undefined ) { + this.setCell(parts[0], parts[1], parts[2], 0); + return true; + } + return false; +}; + +/******************************************************************************/ + +var magicId = 1; + Matrix.prototype.toSelfie = function() { return { magicId: magicId, - rules: this.rules + rules: Array.from(this.rules) }; }; -/******************************************************************************/ - Matrix.prototype.fromSelfie = function(selfie) { - this.rules = selfie.rules; + if ( selfie.magicId !== magicId ) { return false; } + this.rules = new Map(selfie.rules); + this.changed = true; + return true; }; /******************************************************************************/ diff --git a/src/js/hnswitches.js b/src/js/hnswitches.js index 17954c961..c47e18144 100644 --- a/src/js/hnswitches.js +++ b/src/js/hnswitches.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a Chromium browser extension to black/white list requests. - Copyright (C) 2015-2017 Raymond Hill + Copyright (C) 2015-2018 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 @@ -62,6 +62,7 @@ var nameToSwitchStateMap = { // For performance purpose, as simple tests as possible var reHostnameVeryCoarse = /[g-z_-]/; var reIPv4VeryCoarse = /\.\d+$/; +var reNotASCII = /[^\x20-\x7F]/; // http://tools.ietf.org/html/rfc5952 // 4.3: "MUST be represented in lowercase" @@ -96,10 +97,11 @@ var selectHostnameBroadener = function(hostname) { /******************************************************************************/ HnSwitches.prototype.reset = function() { - this.switches = {}; + this.switches = new Map(); this.n = ''; this.z = ''; this.r = 0; + this.changed = true; }; /******************************************************************************/ @@ -114,14 +116,15 @@ HnSwitches.prototype.toggle = function(switchName, hostname, newVal) { if ( newVal === this.evaluate(switchName, hostname) ) { return false; } - var bits = this.switches[hostname] || 0; + var bits = this.switches.get(hostname) || 0; bits &= ~(3 << bitOffset); bits |= newVal << bitOffset; if ( bits === 0 ) { - delete this.switches[hostname]; + this.switches.delete(hostname); } else { - this.switches[hostname] = bits; + this.switches.set(hostname, bits); } + this.changed = true; return true; }; @@ -139,33 +142,30 @@ HnSwitches.prototype.toggleOneZ = function(switchName, hostname, newState) { if ( newState === undefined ) { newState = !state; } - var bits = this.switches[hostname] || 0; + var bits = this.switches.get(hostname) || 0; bits &= ~(3 << bitOffset); if ( bits === 0 ) { - delete this.switches[hostname]; + this.switches.delete(hostname); } else { - this.switches[hostname] = bits; + this.switches.set(hostname, bits); } state = this.evaluateZ(switchName, hostname); - if ( state === newState ) { - return true; + if ( state !== newState ) { + this.switches.set(hostname, bits | ((newState ? 1 : 2) << bitOffset)); } - this.switches[hostname] = bits | ((newState ? 1 : 2) << bitOffset); + this.changed = true; return true; }; /******************************************************************************/ HnSwitches.prototype.toggleBranchZ = function(switchName, targetHostname, newState) { - var changed = this.toggleOneZ(switchName, targetHostname, newState); - var targetLen = targetHostname.length; + this.toggleOneZ(switchName, targetHostname, newState); // 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; - } + var targetLen = targetHostname.length; + for ( var hostname of this.switches.keys() ) { if ( hostname === targetHostname ) { continue; } @@ -178,10 +178,10 @@ HnSwitches.prototype.toggleBranchZ = function(switchName, targetHostname, newSta if ( hostname.charAt(hostname.length - targetLen - 1) !== '.' ) { continue; } - changed = this.toggle(switchName, hostname, 0) || changed; + this.toggle(switchName, hostname, 0); } - return changed; + return this.changed; }; /******************************************************************************/ @@ -200,8 +200,8 @@ HnSwitches.prototype.toggleZ = function(switchName, hostname, deep, newState) { // 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 ) { + var bits = this.switches.get(hostname); + if ( bits === undefined ) { return 0; } var bitOffset = switchBitOffsets[switchName]; @@ -224,8 +224,8 @@ HnSwitches.prototype.evaluateZ = function(switchName, hostname) { hn = hostname, broadenSource = selectHostnameBroadener(hn); for (;;) { - bits = this.switches[hn] || 0; - if ( bits !== 0 ) { + bits = this.switches.get(hn); + if ( bits !== undefined ) { bits = bits >>> bitOffset & 3; if ( bits !== 0 ) { this.z = hn; @@ -252,20 +252,15 @@ HnSwitches.prototype.toLogData = function() { /******************************************************************************/ -HnSwitches.prototype.toString = function() { +HnSwitches.prototype.toArray = 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 ) { + for ( var hostname of this.switches.keys() ) { + for ( var switchName in switchBitOffsets ) { if ( switchBitOffsets.hasOwnProperty(switchName) === false ) { continue; } - val = this.evaluate(switchName, hostname); + var val = this.evaluate(switchName, hostname); if ( val === 0 ) { continue; } if ( hostname.indexOf('xn--') !== -1 ) { hostname = toUnicode(hostname); @@ -273,63 +268,57 @@ HnSwitches.prototype.toString = function() { out.push(switchName + ': ' + hostname + ' ' + switchStateToNameMap[val]); } } - return out.join('\n'); + return out; +}; + +HnSwitches.prototype.toString = function() { + return this.toArray().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; - + var lineIter = new µBlock.LineIterator(text); 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); - 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]); + this.addFromRuleParts(lineIter.next().trim().split(/\s+/)); } }; /******************************************************************************/ +HnSwitches.prototype.validateRuleParts = function(parts) { + if ( parts.length < 3 ) { return; } + if ( parts[0].endsWith(':') === false ) { return; } + if ( nameToSwitchStateMap.hasOwnProperty(parts[2]) === false ) { return; } + // Performance: avoid punycoding if hostname is made only of ASCII chars. + if ( reNotASCII.test(parts[1]) ) { parts[1] = punycode.toASCII(parts[1]); } + return parts; +}; + +/******************************************************************************/ + +HnSwitches.prototype.addFromRuleParts = function(parts) { + if ( this.validateRuleParts(parts) !== undefined ) { + var switchName = parts[0].slice(0, -1); + if ( switchBitOffsets.hasOwnProperty(switchName) ) { + this.toggle(switchName, parts[1], nameToSwitchStateMap[parts[2]]); + return true; + } + } + return false; +}; + +HnSwitches.prototype.removeFromRuleParts = function(parts) { + if ( this.validateRuleParts(parts) !== undefined ) { + this.toggle(parts[0].slice(0, -1), parts[1], 0); + return true; + } + return false; +}; + +/******************************************************************************/ + return HnSwitches; /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index 8cefbfc40..fd1c96568 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -863,47 +863,57 @@ var getLists = function(callback) { var getRules = function() { return { - permanentRules: µb.permanentFirewall.toString() + '\n' + µb.permanentURLFiltering.toString(), - sessionRules: µb.sessionFirewall.toString() + '\n' + µb.sessionURLFiltering.toString(), - hnSwitches: µb.hnSwitches.toString() + permanentRules: µb.permanentFirewall.toArray().concat( + µb.permanentURLFiltering.toArray() + ), + sessionRules: µb.sessionFirewall.toArray().concat( + µb.sessionURLFiltering.toArray() + ), + hnSwitches: µb.hnSwitches.toArray() }; }; -// Untangle firewall rules, url rules and switches. -var untangleRules = function(s) { - var textEnd = s.length; - var lineBeg = 0, lineEnd; - var line; - var firewallRules = []; - var urlRules = []; - var switches = []; - var reIsSwitchRule = /^[a-z-]+:\s/; - - while ( lineBeg < textEnd ) { - lineEnd = s.indexOf('\n', lineBeg); - if ( lineEnd < 0 ) { - lineEnd = s.indexOf('\r', lineBeg); - if ( lineEnd < 0 ) { - lineEnd = textEnd; - } +var modifyRuleset = function(details) { + var swRuleset = µb.hnSwitches, + hnRuleset, urlRuleset; + if ( details.permanent ) { + hnRuleset = µb.permanentFirewall; + urlRuleset = µb.permanentURLFiltering; + } else { + hnRuleset = µb.sessionFirewall; + urlRuleset = µb.sessionURLFiltering; + } + var toRemove = new Set(details.toRemove.trim().split(/\s*[\n\r]+\s*/)); + var rule, parts, _; + for ( rule of toRemove ) { + if ( rule === '' ) { continue; } + parts = rule.split(/\s/); + _ = hnRuleset.removeFromRuleParts(parts) || + swRuleset.removeFromRuleParts(parts) || + urlRuleset.removeFromRuleParts(parts); + } + var toAdd = new Set(details.toAdd.trim().split(/\s*[\n\r]+\s*/)); + for ( rule of toAdd ) { + if ( rule === '' ) { continue; } + parts = rule.split(/\s/); + _ = hnRuleset.addFromRuleParts(parts) || + swRuleset.addFromRuleParts(parts) || + urlRuleset.addFromRuleParts(parts); + } + if ( details.permanent ) { + if ( hnRuleset.changed ) { + µb.savePermanentFirewallRules(); + hnRuleset.changed = false; } - line = s.slice(lineBeg, lineEnd).trim(); - lineBeg = lineEnd + 1; - - if ( reIsSwitchRule.test(line) ) { - switches.push(line); - } else if ( line.indexOf('://') !== -1 ) { - urlRules.push(line); - } else { - firewallRules.push(line); + if ( urlRuleset.changed ) { + µb.savePermanentURLFilteringRules(); + urlRuleset.changed = false; } } - - return { - firewallRules: firewallRules.join('\n'), - urlRules: urlRules.join('\n'), - switches: switches.join('\n') - }; + if ( swRuleset.changed ) { + µb.saveHostnameSwitches(); + swRuleset.changed = false; + } }; /******************************************************************************/ @@ -938,6 +948,13 @@ var onMessage = function(request, sender, callback) { response = getRules(); break; + case 'modifyRuleset': + // https://github.com/chrisaljoudi/uBlock/issues/772 + µb.cosmeticFilteringEngine.removeFromSelectorCache('*'); + modifyRuleset(request); + response = getRules(); + break; + case 'purgeAllCaches': if ( request.hard ) { µb.assets.remove(/./); @@ -968,28 +985,6 @@ var onMessage = function(request, sender, callback) { resetUserData(); break; - case 'setSessionRules': - // https://github.com/chrisaljoudi/uBlock/issues/772 - µb.cosmeticFilteringEngine.removeFromSelectorCache('*'); - response = untangleRules(request.rules); - µb.sessionFirewall.fromString(response.firewallRules); - µb.sessionURLFiltering.fromString(response.urlRules); - µb.hnSwitches.fromString(response.switches); - µb.saveHostnameSwitches(); - response = getRules(); - break; - - case 'setPermanentRules': - response = untangleRules(request.rules); - µb.permanentFirewall.fromString(response.firewallRules); - µb.savePermanentFirewallRules(); - µb.permanentURLFiltering.fromString(response.urlRules); - µb.savePermanentURLFilteringRules(); - µb.hnSwitches.fromString(response.switches); - µb.saveHostnameSwitches(); - response = getRules(); - break; - case 'validateWhitelistString': response = µb.validateWhitelistString(request.raw); break; diff --git a/src/js/url-net-filtering.js b/src/js/url-net-filtering.js index b317c9200..7ba9c56ec 100644 --- a/src/js/url-net-filtering.js +++ b/src/js/url-net-filtering.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to black/white list requests. - Copyright (C) 2015-2017 Raymond Hill + Copyright (C) 2015-2018 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 @@ -136,23 +136,23 @@ URLNetFiltering.prototype.reset = function() { this.url = ''; this.type = ''; this.r = 0; + this.changed = false; }; /******************************************************************************/ URLNetFiltering.prototype.assign = function(other) { - var thisRules = this.rules, - otherRules = other.rules; // Remove rules not in other - for ( var key of thisRules.keys() ) { - if ( otherRules.has(key) === false ) { - thisRules.delete(key); + for ( var key of this.rules.keys() ) { + if ( other.rules.has(key) === false ) { + this.rules.delete(key); } } // Add/change rules in other - for ( var entry of otherRules ) { - thisRules.set(entry[0], entry[1].slice()); + for ( var entry of other.rules ) { + this.rules.set(entry[0], entry[1].slice()); } + this.changed = true; }; /******************************************************************************/ @@ -176,6 +176,7 @@ URLNetFiltering.prototype.setRule = function(srcHostname, url, type, action) { } else { addRuleEntry(entries, url, action); } + this.changed = true; return true; }; @@ -195,6 +196,7 @@ URLNetFiltering.prototype.removeRule = function(srcHostname, url, type) { if ( entries.length === 0 ) { this.rules.delete(bucketKey); } + this.changed = true; return true; }; @@ -276,7 +278,6 @@ URLNetFiltering.prototype.intToActionMap = new Map([ /******************************************************************************/ URLNetFiltering.prototype.copyRules = function(other, context, urls, type) { - var changed = false; var url, otherOwn, thisOwn; var i = urls.length; while ( i-- ) { @@ -287,21 +288,21 @@ URLNetFiltering.prototype.copyRules = function(other, context, urls, type) { thisOwn = this.r !== 0 && this.context === context && this.url === url && this.type === type; if ( otherOwn && !thisOwn ) { this.setRule(context, url, type, other.r); - changed = true; + this.changed = true; } if ( !otherOwn && thisOwn ) { this.removeRule(context, url, type); - changed = true; + this.changed = true; } } - return changed; + return this.changed; }; /******************************************************************************/ // "url-filtering:" hostname url type action -URLNetFiltering.prototype.toString = function() { +URLNetFiltering.prototype.toArray = function() { var out = [], key, pos, hn, type, entries, i, entry; for ( var item of this.rules ) { @@ -321,40 +322,52 @@ URLNetFiltering.prototype.toString = function() { ); } } - return out.sort().join('\n'); + return out; +}; + +URLNetFiltering.prototype.toString = function() { + return this.toArray().sort().join('\n'); }; /******************************************************************************/ URLNetFiltering.prototype.fromString = function(text) { this.reset(); - - var lineIter = new µBlock.LineIterator(text), - line, fields; + var lineIter = new µBlock.LineIterator(text); while ( lineIter.eot() === false ) { - line = lineIter.next().trim(); - if ( line === '' ) { continue; } - // Coarse test - if ( line.indexOf('://') === -1 ) { - continue; - } - fields = line.split(/\s+/); - if ( fields.length !== 4 ) { - continue; - } - // Finer test - if ( fields[1].indexOf('://') === -1 ) { - continue; - } - if ( nameToActionMap.hasOwnProperty(fields[3]) === false ) { - continue; - } - this.setRule(fields[0], fields[1], fields[2], nameToActionMap[fields[3]]); + this.addFromRuleParts(lineIter.next().trim().split(/\s+/)); } }; /******************************************************************************/ +URLNetFiltering.prototype.validateRuleParts = function(parts) { + if ( parts.length !== 4 ) { return; } + if ( parts[1].indexOf('://') === -1 ) { return; } + if ( nameToActionMap.hasOwnProperty(parts[3]) === false ) { return; } + return parts; +}; + +/******************************************************************************/ + +URLNetFiltering.prototype.addFromRuleParts = function(parts) { + if ( this.validateRuleParts(parts) !== undefined ) { + this.setRule(parts[0], parts[1], parts[2], nameToActionMap[parts[3]]); + return true; + } + return false; +}; + +URLNetFiltering.prototype.removeFromRuleParts = function(parts) { + if ( this.validateRuleParts(parts) !== undefined ) { + this.removeRule(parts[0], parts[1], parts[2]); + return true; + } + return false; +}; + +/******************************************************************************/ + return URLNetFiltering; /******************************************************************************/