diff --git a/src/css/whitelist.css b/src/css/whitelist.css index 5a0e834e8..b017cf0cd 100644 --- a/src/css/whitelist.css +++ b/src/css/whitelist.css @@ -5,12 +5,36 @@ div > p:last-child { margin-bottom: 0; } #whitelist { - box-sizing: border-box; + border: 1px solid gray; height: 60vh; - text-align: left; + margin: 0; + padding: 1px; + position: relative; + resize: vertical; + } +#whitelist.invalid { + border-color: red; + } +#whitelist textarea { + border: none; + box-sizing: border-box; + height: 100%; + padding: 0.4em; + resize: none; + text-align: left; white-space: pre; width: 100%; } -#whitelist.bad { - background-color: #fee; - } +#whitelist textarea + div { + background-color: red; + bottom: 0; + color: white; + display: none; + padding: 2px 4px; + pointer-events: none; + position: absolute; + right: 0; +} +#whitelist.invalid textarea + div { + display: block; +} diff --git a/src/js/messaging.js b/src/js/messaging.js index c6ade1f4f..360fc7e0f 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -1011,6 +1011,10 @@ var onMessage = function(request, sender, callback) { response = getRules(); break; + case 'validateWhitelistString': + response = µb.validateWhitelistString(request.raw); + break; + case 'writeHiddenSettings': µb.hiddenSettingsFromString(request.content); break; diff --git a/src/js/ublock.js b/src/js/ublock.js index d68a12929..528edb534 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -194,9 +194,7 @@ var matchBucket = function(url, hostname, bucket, start) { µBlock.whitelistFromString = function(s) { var whitelist = Object.create(null), - reInvalidHostname = /[^a-z0-9.\-\[\]:]/, - reHostnameExtractor = /([a-z0-9\[][a-z0-9.\-]*[a-z0-9\]])(?::[\d*]+)?\/(?:[^\x00-\x20\/]|$)[^\x00-\x20]*$/, - lines = s.split(/[\n\r]+/), + lineIter = new this.LineIterator(s), line, matches, key, directive, re; // Comment bucket must always be ready to be used. @@ -205,8 +203,9 @@ var matchBucket = function(url, hostname, bucket, start) { // New set of directives, scrap cached data. directiveToRegexpMap.clear(); - for ( var i = 0; i < lines.length; i++ ) { - line = lines[i].trim(); + while ( !lineIter.eot() ) { + line = lineIter.next().trim(); + // https://github.com/gorhill/uBlock/issues/171 // Skip empty lines if ( line === '' ) { @@ -228,7 +227,7 @@ var matchBucket = function(url, hostname, bucket, start) { } } // Regex-based (ensure it is valid) - else if ( line.startsWith('/') && line.endsWith('/') ) { + else if ( line.length > 2 && line.startsWith('/') && line.endsWith('/') ) { key = '//'; directive = line; try { @@ -267,6 +266,28 @@ var matchBucket = function(url, hostname, bucket, start) { return whitelist; }; +µBlock.validateWhitelistString = function(s) { + var lineIter = new this.LineIterator(s), line; + while ( !lineIter.eot() ) { + line = lineIter.next().trim(); + if ( line === '' ) { continue; } + if ( line.startsWith('#') ) { continue; } // Comment + if ( line.indexOf('/') === -1 ) { // Plain hostname + if ( reInvalidHostname.test(line) ) { return false; } + continue; + } + if ( line.length > 2 && line.startsWith('/') && line.endsWith('/') ) { // Regex-based + try { new RegExp(line.slice(1, -1)); } catch(ex) { return false; } + continue; + } + if ( reHostnameExtractor.test(line) === false ) { return false; } // URL + } + return true; +}; + +var reInvalidHostname = /[^a-z0-9.\-\[\]:]/, + reHostnameExtractor = /([a-z0-9\[][a-z0-9.\-]*[a-z0-9\]])(?::[\d*]+)?\/(?:[^\x00-\x20\/]|$)[^\x00-\x20]*$/; + /******************************************************************************/ })(); diff --git a/src/js/whitelist.js b/src/js/whitelist.js index 866ffca0f..52a3a4d7c 100644 --- a/src/js/whitelist.js +++ b/src/js/whitelist.js @@ -29,30 +29,68 @@ /******************************************************************************/ -var messaging = vAPI.messaging; -var cachedWhitelist = ''; - -// Could make it more fancy if needed. But speed... It's a compromise. -var reUnwantedChars = /[\x00-\x09\x0b\x0c\x0e-\x1f!"'()<>{}|`~]/; +var messaging = vAPI.messaging, + cachedWhitelist = ''; /******************************************************************************/ -var whitelistChanged = function() { - var textarea = uDom.nodeFromId('whitelist'); - var s = textarea.value.trim(); - var changed = s === cachedWhitelist; - var bad = reUnwantedChars.test(s); - uDom.nodeFromId('whitelistApply').disabled = changed || bad; - uDom.nodeFromId('whitelistRevert').disabled = changed; - textarea.classList.toggle('bad', bad); +var getTextareaNode = function() { + var me = getTextareaNode, + node = me.theNode; + if ( node === undefined ) { + node = me.theNode = uDom.nodeFromSelector('#whitelist textarea'); + } + return node; }; +var setErrorNodeHorizontalOffset = function(px) { + var me = setErrorNodeHorizontalOffset, + offset = me.theOffset || 0; + if ( px === offset ) { return; } + var node = me.theNode; + if ( node === undefined ) { + node = me.theNode = uDom.nodeFromSelector('#whitelist textarea + div'); + } + node.style.right = px + 'px'; + me.theOffset = px; +}; + +/******************************************************************************/ + +var whitelistChanged = (function() { + var changedWhitelist, changed, timer; + + var updateUI = function(good) { + uDom.nodeFromId('whitelistApply').disabled = changed || !good; + uDom.nodeFromId('whitelistRevert').disabled = changed; + uDom.nodeFromId('whitelist').classList.toggle('invalid', !good); + }; + + var validate = function() { + timer = undefined; + messaging.send( + 'dashboard', + { what: 'validateWhitelistString', raw: changedWhitelist }, + updateUI + ); + }; + + return function() { + changedWhitelist = getTextareaNode().value.trim(); + changed = changedWhitelist === cachedWhitelist; + if ( timer !== undefined ) { clearTimeout(timer); } + timer = vAPI.setTimeout(validate, 251); + var textarea = getTextareaNode(); + setErrorNodeHorizontalOffset(textarea.offsetWidth - textarea.clientWidth); + }; +})(); + /******************************************************************************/ var renderWhitelist = function() { var onRead = function(whitelist) { cachedWhitelist = whitelist.trim(); - uDom.nodeFromId('whitelist').value = cachedWhitelist + '\n'; + getTextareaNode().value = cachedWhitelist + '\n'; whitelistChanged(); }; messaging.send('dashboard', { what: 'getWhitelist' }, onRead); @@ -62,8 +100,8 @@ var renderWhitelist = function() { var handleImportFilePicker = function() { var fileReaderOnLoadHandler = function() { - var textarea = uDom('#whitelist'); - textarea.val([textarea.val(), this.result].join('\n').trim()); + var textarea = getTextareaNode(); + textarea.value = [textarea.value.trim(), this.result.trim()].join('\n').trim(); whitelistChanged(); }; var file = this.files[0]; @@ -92,10 +130,8 @@ var startImportFilePicker = function() { /******************************************************************************/ var exportWhitelistToFile = function() { - var val = uDom('#whitelist').val().trim(); - if ( val === '' ) { - return; - } + var val = getTextareaNode().value.trim(); + if ( val === '' ) { return; } var filename = vAPI.i18n('whitelistExportFilename') .replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString()) .replace(/ +/g, '_'); @@ -108,7 +144,7 @@ var exportWhitelistToFile = function() { /******************************************************************************/ var applyChanges = function() { - cachedWhitelist = uDom.nodeFromId('whitelist').value.trim(); + cachedWhitelist = getTextareaNode().value.trim(); var request = { what: 'setWhitelist', whitelist: cachedWhitelist @@ -117,21 +153,21 @@ var applyChanges = function() { }; var revertChanges = function() { - uDom.nodeFromId('whitelist').value = cachedWhitelist + '\n'; + getTextareaNode().value = cachedWhitelist + '\n'; whitelistChanged(); }; /******************************************************************************/ var getCloudData = function() { - return uDom.nodeFromId('whitelist').value; + return getTextareaNode().value; }; var setCloudData = function(data, append) { if ( typeof data !== 'string' ) { return; } - var textarea = uDom.nodeFromId('whitelist'); + var textarea = getTextareaNode(); if ( append ) { data = uBlockDashboard.mergeNewLines(textarea.value.trim(), data); } @@ -147,7 +183,7 @@ self.cloud.onPull = setCloudData; uDom('#importWhitelistFromFile').on('click', startImportFilePicker); uDom('#importFilePicker').on('change', handleImportFilePicker); uDom('#exportWhitelistToFile').on('click', exportWhitelistToFile); -uDom('#whitelist').on('input', whitelistChanged); +uDom('#whitelist textarea').on('input', whitelistChanged); uDom('#whitelistApply').on('click', applyChanges); uDom('#whitelistRevert').on('click', revertChanges); diff --git a/src/whitelist.html b/src/whitelist.html index 0059a3964..67a70ab92 100644 --- a/src/whitelist.html +++ b/src/whitelist.html @@ -17,7 +17,10 @@

-

+

+ +
E
+