make use of CodeMirror in "My rules" pane

This commit is contained in:
Raymond Hill 2018-03-11 10:59:39 -04:00
parent caef7d00bb
commit b10ac0020d
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
9 changed files with 593 additions and 607 deletions

View file

@ -24,6 +24,7 @@
<li><a href="https://github.com/bestiejs/punycode.js" target="_blank">Punycode.js</a> by <a href="https://github.com/mathiasbynens">Mathias Bynens</a> <li><a href="https://github.com/bestiejs/punycode.js" target="_blank">Punycode.js</a> by <a href="https://github.com/mathiasbynens">Mathias Bynens</a>
<li><a href="https://fontawesome.com/" target="_blank">Font Awesome</a> by <a href="https://github.com/davegandy">Dave Gandy</a> <li><a href="https://fontawesome.com/" target="_blank">Font Awesome</a> by <a href="https://github.com/davegandy">Dave Gandy</a>
<li><a href="https://codemirror.net/" target="_blank">CodeMirror</a> by <a href="https://github.com/marijnh">Marijn Haverbeke</a> <li><a href="https://codemirror.net/" target="_blank">CodeMirror</a> by <a href="https://github.com/marijnh">Marijn Haverbeke</a>
<li><a href="https://github.com/Swatinem/diff" target="_blank">An implementation of Myers' diff algorithm</a> by <a href="https://github.com/Swatinem">Arpad Borsos</a>
</ul> </ul>
<script src="js/vapi.js"></script> <script src="js/vapi.js"></script>

View file

@ -12,7 +12,7 @@
} }
/* For when panels are used */ /* For when panels are used */
.codeMirrorContainer > div:not(.CodeMirror) { .codeMirrorContainer > div:not([class^="CodeMirror"]) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
@ -63,3 +63,12 @@
.cm-search-widget .cm-search-widget-button:hover { .cm-search-widget .cm-search-widget-button:hover {
color: #000; color: #000;
} }
.CodeMirror-merge-l-deleted {
background-image: none;
font-weight: bold;
}
.CodeMirror-merge-l-inserted {
background-image: none;
font-weight: bold;
}

View file

@ -1,38 +1,40 @@
div > p:first-child { body {
margin-top: 0; bottom: 0;
} display: flex;
div > p:last-child { left: 0;
margin-bottom: 0; position: absolute;
} right: 0;
code { top: 0;
background-color: #eee; flex-direction: column;
font: 11px monospace;
padding: 2px 4px;
} }
#diff { #diff {
border: 0; border: 0;
border-top: 1px solid #eee; border-top: 1px solid #eee;
flex-grow: 1;
margin: 0; margin: 0;
padding: 0.5em 0 0 0; padding: 0;
white-space: nowrap; white-space: nowrap;
} }
#diff .pane { #diff .tools > * {
margin-bottom: 0.5em;
}
#diff .tools .fa {
font-size: large;
}
#diff .ruleActions {
border: 0; border: 0;
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
font: 90%/180% "Noto Mono",monospace;
margin: 0;
padding: 0; padding: 0;
position: relative; text-align: center;
white-space: normal; vertical-align: top;
width: 50%; width: 50%;
} }
#diff .pane .rulesContainer { #diff .ruleActions h3 {
position: relative; font-weight: normal;
min-height: 150px; /* too short is confusing */ margin: 0.5em 0;
} }
#diff .ruleActions { #diff .ruleFilter {
padding: 0 0 1em 0;
text-align: center; text-align: center;
} }
body[dir="ltr"] #revertButton:after { body[dir="ltr"] #revertButton:after {
@ -73,97 +75,33 @@ body[dir="rtl"] #commitButton:before {
} }
#revertButton, #revertButton,
#commitButton, #commitButton,
#diff.edit #editEnterButton { #diff.editing #exportButton,
#diff.editing #importButton,
#editSaveButton {
opacity: 0.25; opacity: 0.25;
pointer-events: none; pointer-events: none;
} }
#editStopButton, #diff.dirty:not(.editing) #revertButton,
#editCancelButton { #diff.dirty:not(.editing) #commitButton,
display: none; #diff.editing #editSaveButton {
}
#diff.dirty:not(.edit) #revertButton,
#diff.dirty:not(.edit) #commitButton {
opacity: 1; opacity: 1;
pointer-events: auto; pointer-events: auto;
} }
#diff.edit #editStopButton,
#diff.edit #editCancelButton { .codeMirrorContainer {
display: initial; height: 60vh;
} }
#diff.edit #importButton, .CodeMirror-merge, .CodeMirror-merge-pane, .CodeMirror-merge .CodeMirror {
#diff.edit #exportButton { box-sizing: border-box;
height: 100%;
}
#diff.editing .CodeMirror-merge-copy,
#diff.editing .CodeMirror-merge-copy-reverse {
display: none; display: none;
} }
#diff ul { #diff.editing .CodeMirror-merge-left .CodeMirror {
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 {
color: #888; color: #888;
} }
#diff li { #diff.editing .CodeMirror-merge-editor .CodeMirror {
background-color: #ddd; background-color: #ffe;
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 <br>.
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;
} }

View file

@ -4,52 +4,50 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>uBlock — Dynamic filtering rules</title> <title>uBlock — Dynamic filtering rules</title>
<link rel="stylesheet" type="text/css" href="lib/codemirror/lib/codemirror.css">
<link rel="stylesheet" type="text/css" href="lib/codemirror/addon/dialog/dialog.css">
<link rel="stylesheet" type="text/css" href="lib/codemirror/addon/merge/merge.css">
<link rel="stylesheet" type="text/css" href="css/common.css"> <link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css"> <link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<link rel="stylesheet" type="text/css" href="css/cloud-ui.css"> <link rel="stylesheet" type="text/css" href="css/cloud-ui.css">
<link rel="stylesheet" type="text/css" href="css/dyna-rules.css"> <link rel="stylesheet" type="text/css" href="css/dyna-rules.css">
<link rel="stylesheet" type="text/css" href="css/codemirror.css">
</head> </head>
<body> <body>
<div id="cloudWidget" class="hide" data-cloud-entry="myRulesPane"></div> <div id="cloudWidget" class="hide" data-cloud-entry="myRulesPane"></div>
<p data-i18n="rulesHint"></p> <p><span data-i18n="rulesHint"></span> <a class="fa info" href="https://github.com/gorhill/uBlock/wiki/Dynamic-filtering:-rule-syntax" target="_blank">&#xf05a;</a></p>
<p data-i18n="rulesFormatHint"></p>
<div id="diff"> <div id="diff">
<div class="pane left"> <div class="tools">
<div class="ruleActions"> <div class="ruleActions">
<h2 data-i18n="rulesPermanentHeader"></h2> <h3 data-i18n="rulesPermanentHeader"></h3>
<button type="button" class="custom" id="exportButton" data-i18n="rulesExport"></button> <button type="button" class="custom" id="exportButton" data-i18n="rulesExport"></button>
<button type="button" class="custom" id="revertButton" data-i18n="rulesRevert"></button> <button type="button" class="custom" id="revertButton" data-i18n="rulesRevert"></button>
</div> </div>
<div class="rulesContainer">
<ul></ul>
</div>
</div>
<div class="pane right">
<div class="ruleActions"> <div class="ruleActions">
<h2 data-i18n="rulesTemporaryHeader"></h2> <h3 data-i18n="rulesTemporaryHeader"></h3>
<button type="button" class="custom" id="commitButton" data-i18n="rulesCommit"></button> <button type="button" class="custom" id="commitButton" data-i18n="rulesCommit"></button>
<button type="button" class="custom" id="editEnterButton" data-i18n="rulesEdit"></button>
<button type="button" class="custom" id="editStopButton" data-i18n="rulesEditSave"></button>
<button type="button" class="custom" id="editCancelButton" data-i18n="rulesEditDiscard"></button>
<button type="button" class="custom" id="importButton" data-i18n="rulesImport"></button> <button type="button" class="custom" id="importButton" data-i18n="rulesImport"></button>
<button type="button" class="custom" id="editSaveButton" data-i18n="rulesEditSave"></button>
</div> </div>
<div class="rulesContainer"> <!-- TO BE IMPLEMENTED: <div class="ruleFilter"><span class="fa">&#xf0b0;</span>&emsp;<input type="text" size="32"></div> -->
<textarea spellcheck="false"></textarea>
<ul></ul>
</div> </div>
<div class="codeMirrorContainer codeMirrorMergeContainer"></div>
</div> </div>
<div id="templates" style="display: none;"> <div id="templates" style="display: none;">
<input class="hidden" id="importFilePicker" type="file" accept="text/plain"> <input class="hidden" id="importFilePicker" type="file" accept="text/plain">
<span class="hidden" data-i18n="rulesDefaultFileName"></span> <span class="hidden" data-i18n="rulesDefaultFileName"></span>
<ul>
<li>&nbsp;</li>
</ul>
</div> </div>
<script src="lib/diff/swatinem_diff.js"></script>
<script src="lib/codemirror/lib/codemirror.js"></script>
<script src="lib/codemirror/addon/merge/merge.js"></script>
<script src="lib/codemirror/addon/selection/active-line.js"></script>
<script src="js/vapi.js"></script> <script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>

View file

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
uBlock Origin - a browser extension to block requests. 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 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 it under the terms of the GNU General Public License as published by
@ -19,90 +19,142 @@
Home: https://github.com/gorhill/uMatrix Home: https://github.com/gorhill/uMatrix
*/ */
/* global uDom, uBlockDashboard */ /* global diff_match_patch, CodeMirror, uDom, uBlockDashboard */
/******************************************************************************/
(function() {
'use strict'; 'use strict';
/******************************************************************************/ /******************************************************************************/
var messaging = vAPI.messaging; (function() {
/******************************************************************************/ /******************************************************************************/
var renderRules = function(details) { var messaging = vAPI.messaging;
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;
// Switches always displayed first -- just like in uMatrix var mergeView = new CodeMirror.MergeView(
// Merge url rules and switches: they just look the same document.querySelector('.codeMirrorMergeContainer'),
rules = details.hnSwitches.split(/\n+/).sort(); {
allowEditingOriginals: true,
for ( i = 0; i < rules.length; i++ ) { connect: 'align', // size of svg is not managed properly with `true`
rule = rules[i]; inputStyle: 'contenteditable',
liLeft = liTemplate.clone().text(rule); lineNumbers: true,
liRight = liTemplate.clone().text(rule); lineWrapping: false,
ulLeft.append(liLeft); origLeft: '',
ulRight.append(liRight); revertButtons: true,
value: ''
} }
);
mergeView.editor().setOption('styleActiveLine', true);
mergeView.editor().setOption('lineNumbers', false);
mergeView.leftOriginal().setOption('readOnly', 'nocursor');
// Firewall rules follow var cleanToken = 0;
var allRules = {}; var cleanEditText = '';
var permanentRules = {};
var sessionRules = {};
var onLeft, onRight;
rules = details.sessionRules.split(/\n+/); var differ;
i = rules.length;
/******************************************************************************/
// 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-- ) { while ( i-- ) {
rule = rules[i].trim(); var diff = diffs[i];
if ( rule === '' ) { if ( diff[0] === 0 ) {
iedit -= diff[1].length;
continue; continue;
} }
sessionRules[rule] = allRules[rule] = true; var end = doc.posFromIndex(iedit);
} if ( diff[0] === 1 ) {
details.sessionRules = rules.sort().join('\n'); doc.replaceRange(diff[1], end, end);
rules = details.permanentRules.split(/\n+/);
i = rules.length;
while ( i-- ) {
rule = rules[i].trim();
if ( rule === '' ) {
continue; 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');
}
ulLeft.append(liLeft);
ulRight.append(liRight);
}
uDom('#diff > .left > .rulesContainer').append(ulLeft); var renderRules = (function() {
uDom('#diff > .right > .rulesContainer').append(ulRight); var firstVisit = true;
uDom('#diff').toggleClass('dirty', details.sessionRules !== details.permanentRules);
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');
}
cleanToken = mergeView.editor().changeGeneration();
onChange(true);
};
})();
/******************************************************************************/
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(/\|/g, ' ')
.replace(/\n/g, ' * noop\n'); .replace(/\n/g, ' * noop\n');
} }
var request = { applyDiff(false, result, '', renderRules);
'what': 'setSessionRules',
'rules': rulesFromHTML('#diff .right li') + '\n' + result
};
messaging.send('dashboard', request, renderRules);
}; };
var file = this.files[0]; var file = this.files[0];
if ( file === undefined || file.name === '' ) { if ( file === undefined || file.name === '' ) { return; }
return; if ( file.type.indexOf('text') !== 0 ) { return; }
}
if ( file.type.indexOf('text') !== 0 ) {
return;
}
var fr = new FileReader(); var fr = new FileReader();
fr.onload = fileReaderOnLoadHandler; fr.onload = fileReaderOnLoadHandler;
fr.readAsText(file); fr.readAsText(file);
@ -157,98 +201,137 @@ function exportUserRulesToFile() {
.replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString()) .replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString())
.replace(/ +/g, '_'); .replace(/ +/g, '_');
vAPI.download({ vAPI.download({
'url': 'data:text/plain,' + encodeURIComponent(rulesFromHTML('#diff .left li') + '\n'), url: 'data:text/plain,' + encodeURIComponent(
'filename': filename, mergeView.leftOriginal().getValue().trim() + '\n'
'saveAs': true ),
filename: filename,
saveAs: true
}); });
} }
/******************************************************************************/ /******************************************************************************/
var rulesFromHTML = function(selector) { /*
var rules = []; var onFilter = (function() {
var lis = uDom(selector); var timer;
var li;
for ( var i = 0; i < lis.length; i++ ) { var process = function() {
li = lis.at(i); timer = undefined;
if ( li.hasClassName('toRemove') ) { };
rules.push('');
} else { return function() {
rules.push(li.text()); 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 commitAllHandler = function() {
var request = { var toAdd = [], toRemove = [];
'what': 'setSessionRules', var left = mergeView.leftOriginal(),
'rules': rulesFromHTML('#diff .left li') edit = mergeView.editor();
}; for ( var chunk of mergeView.leftChunks() ) {
messaging.send('dashboard', request, renderRules); 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 editSaveHandler = function() {
var request = { var editor = mergeView.editor();
'what': 'setPermanentRules', var editText = editor.getValue().trim();
'rules': rulesFromHTML('#diff .right li') if ( editText === cleanEditText ) {
}; onChange(true);
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') ) {
return; return;
} }
uDom('#diff .right textarea').val(rulesFromHTML('#diff .right li')); if ( differ === undefined ) { differ = new diff_match_patch(); }
parent.toggleClass('edit', true); var toAdd = [], toRemove = [];
}; var diffs = differ.diff_main(cleanEditText, editText);
for ( var diff of diffs ) {
/******************************************************************************/ if ( diff[0] === 1 ) {
toAdd.push(diff[1]);
var editStopHandler = function() { } else if ( diff[0] === -1 ) {
var parent = uDom(this).ancestors('#diff'); toRemove.push(diff[1]);
parent.toggleClass('edit', false); }
var request = { }
'what': 'setSessionRules', applyDiff(false, toAdd.join(''), toRemove.join(''), renderRules);
'rules': uDom('#diff .right textarea').val()
};
messaging.send('dashboard', request, renderRules);
};
/******************************************************************************/
var editCancelHandler = function() {
var parent = uDom(this).ancestors('#diff');
parent.toggleClass('edit', false);
}; };
/******************************************************************************/ /******************************************************************************/
var getCloudData = function() { var getCloudData = function() {
return rulesFromHTML('#diff .left li'); return mergeView.leftOriginal().getValue().trim();
}; };
var setCloudData = function(data, append) { var setCloudData = function(data, append) {
if ( typeof data !== 'string' ) { if ( typeof data !== 'string' ) { return; }
return; applyDiff(
} false,
if ( append ) { data,
data = rulesFromHTML('#diff .right li') + '\n' + data; append ? '' : mergeView.editor().getValue().trim(),
} renderRules
var request = { );
'what': 'setSessionRules',
'rules': data
};
messaging.send('dashboard', request, renderRules);
}; };
self.cloud.onPush = getCloudData; self.cloud.onPush = getCloudData;
@ -256,19 +339,18 @@ self.cloud.onPull = setCloudData;
/******************************************************************************/ /******************************************************************************/
messaging.send('dashboard', { what: 'getRules' }, renderRules);
// Handle user interaction // Handle user interaction
uDom('#importButton').on('click', startImportFilePicker); uDom('#importButton').on('click', startImportFilePicker);
uDom('#importFilePicker').on('change', handleImportFilePicker); uDom('#importFilePicker').on('change', handleImportFilePicker);
uDom('#exportButton').on('click', exportUserRulesToFile); 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); // https://groups.google.com/forum/#!topic/codemirror/UQkTrt078Vs
uDom('#commitButton').on('click', commitHandler); mergeView.editor().on('updateDiff', function() { onChange(); });
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);
/******************************************************************************/ /******************************************************************************/

View file

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
uBlock Origin - a browser extension to block requests. 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 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 it under the terms of the GNU General Public License as published by
@ -30,10 +30,6 @@
/******************************************************************************/ /******************************************************************************/
var magicId = 'chmdgxwtetgu';
/******************************************************************************/
var Matrix = function() { var Matrix = function() {
this.reset(); this.reset();
}; };
@ -77,6 +73,7 @@ var nameToActionMap = {
var reHostnameVeryCoarse = /[g-z_-]/; var reHostnameVeryCoarse = /[g-z_-]/;
var reIPv4VeryCoarse = /\.\d+$/; var reIPv4VeryCoarse = /\.\d+$/;
var reBadHostname = /[^0-9a-z_.\[\]:%-]/; var reBadHostname = /[^0-9a-z_.\[\]:%-]/;
var reNotASCII = /[^\x20-\x7F]/;
// http://tools.ietf.org/html/rfc5952 // http://tools.ietf.org/html/rfc5952
// 4.3: "MUST be represented in lowercase" // 4.3: "MUST be represented in lowercase"
@ -115,78 +112,69 @@ Matrix.prototype.reset = function() {
this.type = ''; this.type = '';
this.y = ''; this.y = '';
this.z = ''; this.z = '';
this.rules = {}; this.rules = new Map();
this.changed = false;
}; };
/******************************************************************************/ /******************************************************************************/
Matrix.prototype.assign = function(other) { Matrix.prototype.assign = function(other) {
var thisRules = this.rules;
var otherRules = other.rules;
var k;
// Remove rules not in other // Remove rules not in other
for ( k in thisRules ) { for ( var k of this.rules.keys() ) {
if ( thisRules.hasOwnProperty(k) === false ) { if ( other.rules.has(k) === false ) {
continue; this.rules.delete(k);
} this.changed = true;
if ( otherRules.hasOwnProperty(k) === false ) {
delete thisRules[k];
} }
} }
// Add/change rules in other // Add/change rules in other
for ( k in otherRules ) { for ( var entry of other.rules ) {
if ( otherRules.hasOwnProperty(k) === false ) { if ( this.rules.get(entry[0]) !== entry[1] ) {
continue; this.rules.set(entry[0], entry[1]);
this.changed = true;
} }
thisRules[k] = otherRules[k];
} }
}; };
/******************************************************************************/ /******************************************************************************/
Matrix.prototype.copyRules = function(other, srcHostname, desHostnames) { Matrix.prototype.copyRules = function(other, srcHostname, desHostnames) {
var thisRules = this.rules;
var otherRules = other.rules;
var ruleKey, ruleValue;
// Specific types // Specific types
ruleValue = otherRules['* *'] || 0; var bits = other.rules.get('* *');
if ( ruleValue !== 0 ) { if ( bits !== undefined ) {
thisRules['* *'] = ruleValue; this.rules.set('* *', bits);
} else { } else {
delete thisRules['* *']; this.rules.delete('* *');
} }
ruleKey = srcHostname + ' *'; var key = srcHostname + ' *';
ruleValue = otherRules[ruleKey] || 0; bits = other.rules.get(key);
if ( ruleValue !== 0 ) { if ( bits !== undefined ) {
thisRules[ruleKey] = ruleValue; this.rules.set(key, bits);
} else { } else {
delete thisRules[ruleKey]; this.rules.delete(key);
} }
// Specific destinations // Specific destinations
for ( var desHostname in desHostnames ) { for ( var desHostname in desHostnames ) {
if ( desHostnames.hasOwnProperty(desHostname) === false ) { if ( desHostnames.hasOwnProperty(desHostname) === false ) { continue; }
continue; key = '* ' + desHostname;
} bits = other.rules.get(key);
ruleKey = '* ' + desHostname; if ( bits !== undefined ) {
ruleValue = otherRules[ruleKey] || 0; this.rules.set(key, bits);
if ( ruleValue !== 0 ) {
thisRules[ruleKey] = ruleValue;
} else { } else {
delete thisRules[ruleKey]; this.rules.delete(key);
} }
ruleKey = srcHostname + ' ' + desHostname ; key = srcHostname + ' ' + desHostname ;
ruleValue = otherRules[ruleKey] || 0; bits = other.rules.get(key);
if ( ruleValue !== 0 ) { if ( bits !== undefined ) {
thisRules[ruleKey] = ruleValue; this.rules.set(key, bits);
} else { } else {
delete thisRules[ruleKey]; this.rules.delete(key);
} }
} }
this.changed = true;
return true; return true;
}; };
@ -198,28 +186,25 @@ Matrix.prototype.copyRules = function(other, srcHostname, desHostnames) {
// - from to * // - from to *
Matrix.prototype.hasSameRules = function(other, srcHostname, desHostnames) { Matrix.prototype.hasSameRules = function(other, srcHostname, desHostnames) {
var thisRules = this.rules;
var otherRules = other.rules;
var ruleKey;
// Specific types // Specific types
ruleKey = '* *'; var key = '* *';
if ( (thisRules[ruleKey] || 0) !== (otherRules[ruleKey] || 0) ) { if ( this.rules.get(key) !== other.rules.get(key) ) {
return false; return false;
} }
ruleKey = srcHostname + ' *'; key = srcHostname + ' *';
if ( (thisRules[ruleKey] || 0) !== (otherRules[ruleKey] || 0) ) { if ( this.rules.get(key) !== other.rules.get(key) ) {
return false; return false;
} }
// Specific destinations // Specific destinations
for ( var desHostname in desHostnames ) { for ( var desHostname in desHostnames ) {
ruleKey = '* ' + desHostname; key = '* ' + desHostname;
if ( (thisRules[ruleKey] || 0) !== (otherRules[ruleKey] || 0) ) { if ( this.rules.get(key) !== other.rules.get(key) ) {
return false; return false;
} }
ruleKey = srcHostname + ' ' + desHostname ; key = srcHostname + ' ' + desHostname ;
if ( (thisRules[ruleKey] || 0) !== (otherRules[ruleKey] || 0) ) { if ( this.rules.get(key) !== other.rules.get(key) ) {
return false; return false;
} }
} }
@ -232,19 +217,17 @@ Matrix.prototype.hasSameRules = function(other, srcHostname, desHostnames) {
Matrix.prototype.setCell = function(srcHostname, desHostname, type, state) { Matrix.prototype.setCell = function(srcHostname, desHostname, type, state) {
var bitOffset = typeBitOffsets[type]; var bitOffset = typeBitOffsets[type];
var k = srcHostname + ' ' + desHostname; var k = srcHostname + ' ' + desHostname;
var oldBitmap = this.rules[k]; var oldBitmap = this.rules.get(k) || 0;
if ( oldBitmap === undefined ) {
oldBitmap = 0;
}
var newBitmap = oldBitmap & ~(3 << bitOffset) | (state << bitOffset); var newBitmap = oldBitmap & ~(3 << bitOffset) | (state << bitOffset);
if ( newBitmap === oldBitmap ) { if ( newBitmap === oldBitmap ) {
return false; return false;
} }
if ( newBitmap === 0 ) { if ( newBitmap === 0 ) {
delete this.rules[k]; this.rules.delete(k);
} else { } else {
this.rules[k] = newBitmap; this.rules.set(k, newBitmap);
} }
this.changed = true;
return true; return true;
}; };
@ -256,6 +239,7 @@ Matrix.prototype.unsetCell = function(srcHostname, desHostname, type) {
return false; return false;
} }
this.setCell(srcHostname, desHostname, type, 0); this.setCell(srcHostname, desHostname, type, 0);
this.changed = true;
return true; return true;
}; };
@ -265,7 +249,7 @@ Matrix.prototype.unsetCell = function(srcHostname, desHostname, type) {
Matrix.prototype.evaluateCell = function(srcHostname, desHostname, type) { Matrix.prototype.evaluateCell = function(srcHostname, desHostname, type) {
var key = srcHostname + ' ' + desHostname; var key = srcHostname + ' ' + desHostname;
var bitmap = this.rules[key]; var bitmap = this.rules.get(key);
if ( bitmap === undefined ) { if ( bitmap === undefined ) {
return 0; return 0;
} }
@ -314,7 +298,7 @@ Matrix.prototype.evaluateCellZ = function(srcHostname, desHostname, type, broade
var v; var v;
for (;;) { for (;;) {
this.z = s; this.z = s;
v = this.rules[s + ' ' + desHostname]; v = this.rules.get(s + ' ' + desHostname);
if ( v !== undefined ) { if ( v !== undefined ) {
v = v >>> bitOffset & 3; v = v >>> bitOffset & 3;
if ( v !== 0 ) { if ( v !== 0 ) {
@ -478,22 +462,15 @@ Matrix.prototype.desHostnameFromRule = function(rule) {
/******************************************************************************/ /******************************************************************************/
Matrix.prototype.toString = function() { Matrix.prototype.toArray = function() {
var out = [], var out = [],
rule, type, val,
srcHostname, desHostname,
toUnicode = punycode.toUnicode; toUnicode = punycode.toUnicode;
for ( rule in this.rules ) { for ( var key of this.rules.keys() ) {
if ( this.rules.hasOwnProperty(rule) === false ) { var srcHostname = this.srcHostnameFromRule(key);
continue; var desHostname = this.desHostnameFromRule(key);
} for ( var type in typeBitOffsets ) {
srcHostname = this.srcHostnameFromRule(rule); if ( typeBitOffsets.hasOwnProperty(type) === false ) { continue; }
desHostname = this.desHostnameFromRule(rule); var val = this.evaluateCell(srcHostname, desHostname, type);
for ( type in typeBitOffsets ) {
if ( typeBitOffsets.hasOwnProperty(type) === false ) {
continue;
}
val = this.evaluateCell(srcHostname, desHostname, type);
if ( val === 0 ) { continue; } if ( val === 0 ) { continue; }
if ( srcHostname.indexOf('xn--') !== -1 ) { if ( srcHostname.indexOf('xn--') !== -1 ) {
srcHostname = toUnicode(srcHostname); 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) { Matrix.prototype.fromString = function(text, append) {
var lineIter = new µBlock.LineIterator(text), var lineIter = new µBlock.LineIterator(text);
line, pos, fields, if ( append !== true ) { this.reset(); }
srcHostname, desHostname, type, action,
reNotASCII = /[^\x20-\x7F]/,
toASCII = punycode.toASCII;
if ( append !== true ) {
this.reset();
}
while ( lineIter.eot() === false ) { while ( lineIter.eot() === false ) {
line = lineIter.next().trim(); this.addFromRuleParts(lineIter.next().trim().split(/\s+/));
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);
} }
}; };
/******************************************************************************/ /******************************************************************************/
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() { Matrix.prototype.toSelfie = function() {
return { return {
magicId: magicId, magicId: magicId,
rules: this.rules rules: Array.from(this.rules)
}; };
}; };
/******************************************************************************/
Matrix.prototype.fromSelfie = function(selfie) { 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;
}; };
/******************************************************************************/ /******************************************************************************/

View file

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
uBlock Origin - a Chromium browser extension to black/white list requests. 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 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 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 // For performance purpose, as simple tests as possible
var reHostnameVeryCoarse = /[g-z_-]/; var reHostnameVeryCoarse = /[g-z_-]/;
var reIPv4VeryCoarse = /\.\d+$/; var reIPv4VeryCoarse = /\.\d+$/;
var reNotASCII = /[^\x20-\x7F]/;
// http://tools.ietf.org/html/rfc5952 // http://tools.ietf.org/html/rfc5952
// 4.3: "MUST be represented in lowercase" // 4.3: "MUST be represented in lowercase"
@ -96,10 +97,11 @@ var selectHostnameBroadener = function(hostname) {
/******************************************************************************/ /******************************************************************************/
HnSwitches.prototype.reset = function() { HnSwitches.prototype.reset = function() {
this.switches = {}; this.switches = new Map();
this.n = ''; this.n = '';
this.z = ''; this.z = '';
this.r = 0; this.r = 0;
this.changed = true;
}; };
/******************************************************************************/ /******************************************************************************/
@ -114,14 +116,15 @@ HnSwitches.prototype.toggle = function(switchName, hostname, newVal) {
if ( newVal === this.evaluate(switchName, hostname) ) { if ( newVal === this.evaluate(switchName, hostname) ) {
return false; return false;
} }
var bits = this.switches[hostname] || 0; var bits = this.switches.get(hostname) || 0;
bits &= ~(3 << bitOffset); bits &= ~(3 << bitOffset);
bits |= newVal << bitOffset; bits |= newVal << bitOffset;
if ( bits === 0 ) { if ( bits === 0 ) {
delete this.switches[hostname]; this.switches.delete(hostname);
} else { } else {
this.switches[hostname] = bits; this.switches.set(hostname, bits);
} }
this.changed = true;
return true; return true;
}; };
@ -139,33 +142,30 @@ HnSwitches.prototype.toggleOneZ = function(switchName, hostname, newState) {
if ( newState === undefined ) { if ( newState === undefined ) {
newState = !state; newState = !state;
} }
var bits = this.switches[hostname] || 0; var bits = this.switches.get(hostname) || 0;
bits &= ~(3 << bitOffset); bits &= ~(3 << bitOffset);
if ( bits === 0 ) { if ( bits === 0 ) {
delete this.switches[hostname]; this.switches.delete(hostname);
} else { } else {
this.switches[hostname] = bits; this.switches.set(hostname, bits);
} }
state = this.evaluateZ(switchName, hostname); state = this.evaluateZ(switchName, hostname);
if ( state === newState ) { if ( state !== newState ) {
return true; this.switches.set(hostname, bits | ((newState ? 1 : 2) << bitOffset));
} }
this.switches[hostname] = bits | ((newState ? 1 : 2) << bitOffset); this.changed = true;
return true; return true;
}; };
/******************************************************************************/ /******************************************************************************/
HnSwitches.prototype.toggleBranchZ = function(switchName, targetHostname, newState) { HnSwitches.prototype.toggleBranchZ = function(switchName, targetHostname, newState) {
var changed = this.toggleOneZ(switchName, targetHostname, newState); this.toggleOneZ(switchName, targetHostname, newState);
var targetLen = targetHostname.length;
// Turn off all descendant switches, they will inherit the state of the // Turn off all descendant switches, they will inherit the state of the
// branch's origin. // branch's origin.
for ( var hostname in this.switches ) { var targetLen = targetHostname.length;
if ( this.switches.hasOwnProperty(hostname) === false ) { for ( var hostname of this.switches.keys() ) {
continue;
}
if ( hostname === targetHostname ) { if ( hostname === targetHostname ) {
continue; continue;
} }
@ -178,10 +178,10 @@ HnSwitches.prototype.toggleBranchZ = function(switchName, targetHostname, newSta
if ( hostname.charAt(hostname.length - targetLen - 1) !== '.' ) { if ( hostname.charAt(hostname.length - targetLen - 1) !== '.' ) {
continue; 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) // 2 = forced default state (to override a broader non-default state)
HnSwitches.prototype.evaluate = function(switchName, hostname) { HnSwitches.prototype.evaluate = function(switchName, hostname) {
var bits = this.switches[hostname] || 0; var bits = this.switches.get(hostname);
if ( bits === 0 ) { if ( bits === undefined ) {
return 0; return 0;
} }
var bitOffset = switchBitOffsets[switchName]; var bitOffset = switchBitOffsets[switchName];
@ -224,8 +224,8 @@ HnSwitches.prototype.evaluateZ = function(switchName, hostname) {
hn = hostname, hn = hostname,
broadenSource = selectHostnameBroadener(hn); broadenSource = selectHostnameBroadener(hn);
for (;;) { for (;;) {
bits = this.switches[hn] || 0; bits = this.switches.get(hn);
if ( bits !== 0 ) { if ( bits !== undefined ) {
bits = bits >>> bitOffset & 3; bits = bits >>> bitOffset & 3;
if ( bits !== 0 ) { if ( bits !== 0 ) {
this.z = hn; this.z = hn;
@ -252,20 +252,15 @@ HnSwitches.prototype.toLogData = function() {
/******************************************************************************/ /******************************************************************************/
HnSwitches.prototype.toString = function() { HnSwitches.prototype.toArray = function() {
var out = [], var out = [],
switchName, val,
hostname,
toUnicode = punycode.toUnicode; toUnicode = punycode.toUnicode;
for ( hostname in this.switches ) { for ( var hostname of this.switches.keys() ) {
if ( this.switches.hasOwnProperty(hostname) === false ) { for ( var switchName in switchBitOffsets ) {
continue;
}
for ( switchName in switchBitOffsets ) {
if ( switchBitOffsets.hasOwnProperty(switchName) === false ) { if ( switchBitOffsets.hasOwnProperty(switchName) === false ) {
continue; continue;
} }
val = this.evaluate(switchName, hostname); var val = this.evaluate(switchName, hostname);
if ( val === 0 ) { continue; } if ( val === 0 ) { continue; }
if ( hostname.indexOf('xn--') !== -1 ) { if ( hostname.indexOf('xn--') !== -1 ) {
hostname = toUnicode(hostname); hostname = toUnicode(hostname);
@ -273,59 +268,53 @@ HnSwitches.prototype.toString = function() {
out.push(switchName + ': ' + hostname + ' ' + switchStateToNameMap[val]); 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) { HnSwitches.prototype.fromString = function(text) {
var lineIter = new µBlock.LineIterator(text), var lineIter = new µBlock.LineIterator(text);
line, pos, fields,
switchName, hostname, state,
reNotASCII = /[^\x20-\x7F]/,
toASCII = punycode.toASCII;
this.reset(); this.reset();
while ( lineIter.eot() === false ) { while ( lineIter.eot() === false ) {
line = lineIter.next().trim(); this.addFromRuleParts(lineIter.next().trim().split(/\s+/));
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]; HnSwitches.prototype.validateRuleParts = function(parts) {
pos = switchName.indexOf(':'); if ( parts.length < 3 ) { return; }
if ( pos === -1 ) { if ( parts[0].endsWith(':') === false ) { return; }
continue; if ( nameToSwitchStateMap.hasOwnProperty(parts[2]) === false ) { return; }
} // Performance: avoid punycoding if hostname is made only of ASCII chars.
switchName = switchName.slice(0, pos); if ( reNotASCII.test(parts[1]) ) { parts[1] = punycode.toASCII(parts[1]); }
if ( switchBitOffsets.hasOwnProperty(switchName) === false ) { return parts;
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]; HnSwitches.prototype.addFromRuleParts = function(parts) {
if ( nameToSwitchStateMap.hasOwnProperty(state) === false ) { if ( this.validateRuleParts(parts) !== undefined ) {
continue; var switchName = parts[0].slice(0, -1);
if ( switchBitOffsets.hasOwnProperty(switchName) ) {
this.toggle(switchName, parts[1], nameToSwitchStateMap[parts[2]]);
return true;
} }
}
return false;
};
this.toggle(switchName, hostname, nameToSwitchStateMap[state]); HnSwitches.prototype.removeFromRuleParts = function(parts) {
if ( this.validateRuleParts(parts) !== undefined ) {
this.toggle(parts[0].slice(0, -1), parts[1], 0);
return true;
} }
return false;
}; };
/******************************************************************************/ /******************************************************************************/

View file

@ -863,47 +863,57 @@ var getLists = function(callback) {
var getRules = function() { var getRules = function() {
return { return {
permanentRules: µb.permanentFirewall.toString() + '\n' + µb.permanentURLFiltering.toString(), permanentRules: µb.permanentFirewall.toArray().concat(
sessionRules: µb.sessionFirewall.toString() + '\n' + µb.sessionURLFiltering.toString(), µb.permanentURLFiltering.toArray()
hnSwitches: µb.hnSwitches.toString() ),
sessionRules: µb.sessionFirewall.toArray().concat(
µb.sessionURLFiltering.toArray()
),
hnSwitches: µb.hnSwitches.toArray()
}; };
}; };
// Untangle firewall rules, url rules and switches. var modifyRuleset = function(details) {
var untangleRules = function(s) { var swRuleset = µb.hnSwitches,
var textEnd = s.length; hnRuleset, urlRuleset;
var lineBeg = 0, lineEnd; if ( details.permanent ) {
var line; hnRuleset = µb.permanentFirewall;
var firewallRules = []; urlRuleset = µb.permanentURLFiltering;
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;
}
}
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 { } else {
firewallRules.push(line); 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;
}
if ( urlRuleset.changed ) {
µb.savePermanentURLFilteringRules();
urlRuleset.changed = false;
} }
} }
if ( swRuleset.changed ) {
return { µb.saveHostnameSwitches();
firewallRules: firewallRules.join('\n'), swRuleset.changed = false;
urlRules: urlRules.join('\n'), }
switches: switches.join('\n')
};
}; };
/******************************************************************************/ /******************************************************************************/
@ -938,6 +948,13 @@ var onMessage = function(request, sender, callback) {
response = getRules(); response = getRules();
break; break;
case 'modifyRuleset':
// https://github.com/chrisaljoudi/uBlock/issues/772
µb.cosmeticFilteringEngine.removeFromSelectorCache('*');
modifyRuleset(request);
response = getRules();
break;
case 'purgeAllCaches': case 'purgeAllCaches':
if ( request.hard ) { if ( request.hard ) {
µb.assets.remove(/./); µb.assets.remove(/./);
@ -968,28 +985,6 @@ var onMessage = function(request, sender, callback) {
resetUserData(); resetUserData();
break; 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': case 'validateWhitelistString':
response = µb.validateWhitelistString(request.raw); response = µb.validateWhitelistString(request.raw);
break; break;

View file

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
uBlock Origin - a browser extension to black/white list requests. 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 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 it under the terms of the GNU General Public License as published by
@ -136,23 +136,23 @@ URLNetFiltering.prototype.reset = function() {
this.url = ''; this.url = '';
this.type = ''; this.type = '';
this.r = 0; this.r = 0;
this.changed = false;
}; };
/******************************************************************************/ /******************************************************************************/
URLNetFiltering.prototype.assign = function(other) { URLNetFiltering.prototype.assign = function(other) {
var thisRules = this.rules,
otherRules = other.rules;
// Remove rules not in other // Remove rules not in other
for ( var key of thisRules.keys() ) { for ( var key of this.rules.keys() ) {
if ( otherRules.has(key) === false ) { if ( other.rules.has(key) === false ) {
thisRules.delete(key); this.rules.delete(key);
} }
} }
// Add/change rules in other // Add/change rules in other
for ( var entry of otherRules ) { for ( var entry of other.rules ) {
thisRules.set(entry[0], entry[1].slice()); this.rules.set(entry[0], entry[1].slice());
} }
this.changed = true;
}; };
/******************************************************************************/ /******************************************************************************/
@ -176,6 +176,7 @@ URLNetFiltering.prototype.setRule = function(srcHostname, url, type, action) {
} else { } else {
addRuleEntry(entries, url, action); addRuleEntry(entries, url, action);
} }
this.changed = true;
return true; return true;
}; };
@ -195,6 +196,7 @@ URLNetFiltering.prototype.removeRule = function(srcHostname, url, type) {
if ( entries.length === 0 ) { if ( entries.length === 0 ) {
this.rules.delete(bucketKey); this.rules.delete(bucketKey);
} }
this.changed = true;
return true; return true;
}; };
@ -276,7 +278,6 @@ URLNetFiltering.prototype.intToActionMap = new Map([
/******************************************************************************/ /******************************************************************************/
URLNetFiltering.prototype.copyRules = function(other, context, urls, type) { URLNetFiltering.prototype.copyRules = function(other, context, urls, type) {
var changed = false;
var url, otherOwn, thisOwn; var url, otherOwn, thisOwn;
var i = urls.length; var i = urls.length;
while ( i-- ) { 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; thisOwn = this.r !== 0 && this.context === context && this.url === url && this.type === type;
if ( otherOwn && !thisOwn ) { if ( otherOwn && !thisOwn ) {
this.setRule(context, url, type, other.r); this.setRule(context, url, type, other.r);
changed = true; this.changed = true;
} }
if ( !otherOwn && thisOwn ) { if ( !otherOwn && thisOwn ) {
this.removeRule(context, url, type); this.removeRule(context, url, type);
changed = true; this.changed = true;
} }
} }
return changed; return this.changed;
}; };
/******************************************************************************/ /******************************************************************************/
// "url-filtering:" hostname url type action // "url-filtering:" hostname url type action
URLNetFiltering.prototype.toString = function() { URLNetFiltering.prototype.toArray = function() {
var out = [], var out = [],
key, pos, hn, type, entries, i, entry; key, pos, hn, type, entries, i, entry;
for ( var item of this.rules ) { for ( var item of this.rules ) {
@ -321,36 +322,48 @@ 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) { URLNetFiltering.prototype.fromString = function(text) {
this.reset(); this.reset();
var lineIter = new µBlock.LineIterator(text);
var lineIter = new µBlock.LineIterator(text),
line, fields;
while ( lineIter.eot() === false ) { while ( lineIter.eot() === false ) {
line = lineIter.next().trim(); this.addFromRuleParts(lineIter.next().trim().split(/\s+/));
if ( line === '' ) { continue; }
// Coarse test
if ( line.indexOf('://') === -1 ) {
continue;
} }
fields = line.split(/\s+/); };
if ( fields.length !== 4 ) {
continue; /******************************************************************************/
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;
} }
// Finer test return false;
if ( fields[1].indexOf('://') === -1 ) { };
continue;
} URLNetFiltering.prototype.removeFromRuleParts = function(parts) {
if ( nameToActionMap.hasOwnProperty(fields[3]) === false ) { if ( this.validateRuleParts(parts) !== undefined ) {
continue; this.removeRule(parts[0], parts[1], parts[2]);
} return true;
this.setRule(fields[0], fields[1], fields[2], nameToActionMap[fields[3]]);
} }
return false;
}; };
/******************************************************************************/ /******************************************************************************/