Browse Source

Bring uMatrix up to date

Notably:
- Import logger improvements from uBO
- Import CNAME uncloaking from uBO
- Import more improvements from uBO
- Make use of modern JS features

This should un-stall further development of uMatrix.
pull/2/head
Raymond Hill 5 years ago
parent
commit
9b292304d3
No known key found for this signature in database GPG Key ID: 25E1490B761470C2
  1. 4
      .jshintrc
  2. 2
      dist/version
  3. 2
      platform/chromium/manifest.json
  4. 1841
      platform/chromium/vapi-background.js
  5. 308
      platform/chromium/vapi-client-extra.js
  6. 273
      platform/chromium/vapi-client.js
  7. 237
      platform/chromium/vapi-common.js
  8. 230
      platform/chromium/vapi-webrequest.js
  9. 86
      platform/chromium/vapi.js
  10. 176
      platform/chromium/webext.js
  11. 1
      platform/firefox/manifest.json
  12. 263
      platform/firefox/vapi-cachestorage.js
  13. 316
      platform/firefox/vapi-webrequest.js
  14. 24
      platform/firefox/webext.js
  15. 187
      src/_locales/en/messages.json
  16. 7
      src/about.html
  17. 33
      src/asset-viewer.html
  18. 23
      src/background.html
  19. 68
      src/css/codemirror.css
  20. 34
      src/css/common.css
  21. 6
      src/css/dashboard.css
  22. 778
      src/css/logger-ui.css
  23. 62
      src/css/popup.css
  24. 22
      src/css/raw-settings.css
  25. 9
      src/dashboard.html
  26. 2
      src/hosts-files.html
  27. 3
      src/img/fontawesome/fontawesome-defs.svg
  28. 124
      src/js/about.js
  29. 51
      src/js/asset-viewer.js
  30. 1113
      src/js/assets.js
  31. 49
      src/js/background.js
  32. 25
      src/js/browsercache.js
  33. 465
      src/js/cachestorage.js
  34. 104
      src/js/cloud-ui.js
  35. 37
      src/js/codemirror/mode/raw-settings.js
  36. 336
      src/js/codemirror/search.js
  37. 34
      src/js/console.js
  38. 39
      src/js/contentscript-start.js
  39. 192
      src/js/contentscript.js
  40. 293
      src/js/cookies.js
  41. 133
      src/js/dashboard-common.js
  42. 56
      src/js/dashboard.js
  43. 310
      src/js/filtering-context.js
  44. 760
      src/js/hntrie.js
  45. 200
      src/js/hosts-files.js
  46. 9
      src/js/httpsb.js
  47. 228
      src/js/i18n.js
  48. 4
      src/js/liquid-dict.js
  49. 2722
      src/js/logger-ui.js
  50. 36
      src/js/logger.js
  51. 210
      src/js/lz4.js
  52. 56
      src/js/main-blocked.js
  53. 310
      src/js/matrix.js
  54. 776
      src/js/messaging.js
  55. 133
      src/js/pagestats.js
  56. 1117
      src/js/popup.js
  57. 112
      src/js/raw-settings.js
  58. 114
      src/js/settings.js
  59. 120
      src/js/start.js
  60. 908
      src/js/storage.js
  61. 549
      src/js/tab.js
  62. 543
      src/js/traffic.js
  63. 171
      src/js/uritools.js
  64. 240
      src/js/user-rules.js
  65. 128
      src/js/utils.js
  66. 24
      src/js/wasm/README.md
  67. BIN
      src/js/wasm/hntrie.wasm
  68. 710
      src/js/wasm/hntrie.wat
  69. 127
      src/lib/codemirror/addon/display/panel.js
  70. 122
      src/lib/codemirror/addon/scroll/annotatescrollbar.js
  71. 8
      src/lib/codemirror/addon/search/matchesonscrollbar.css
  72. 97
      src/lib/codemirror/addon/search/matchesonscrollbar.js
  73. 293
      src/lib/codemirror/addon/search/searchcursor.js
  74. 18882
      src/lib/codemirror/lib/codemirror.js
  75. 52
      src/lib/lz4/README.md
  76. 151
      src/lib/lz4/lz4-block-codec-any.js
  77. 297
      src/lib/lz4/lz4-block-codec-js.js
  78. 195
      src/lib/lz4/lz4-block-codec-wasm.js
  79. BIN
      src/lib/lz4/lz4-block-codec.wasm
  80. 745
      src/lib/lz4/lz4-block-codec.wat
  81. 343
      src/lib/publicsuffixlist.js
  82. 647
      src/lib/publicsuffixlist/publicsuffixlist.js
  83. 29
      src/lib/publicsuffixlist/wasm/README.md
  84. BIN
      src/lib/publicsuffixlist/wasm/publicsuffixlist.wasm
  85. 317
      src/lib/publicsuffixlist/wasm/publicsuffixlist.wat
  86. 171
      src/logger-ui.html
  87. 1
      src/main-blocked.html
  88. 20
      src/popup.html
  89. 15
      src/raw-settings.html
  90. 1
      src/settings.html
  91. 3
      src/user-rules.html

4
.jshintrc

@ -2,11 +2,13 @@
"browser": true,
"devel": true,
"eqeqeq": true,
"esnext": true,
"esversion": 8,
"globals": {
"browser": false, // global variable in Firefox, Edge
"self": false,
"chrome": false,
"log": false,
"webext": false,
"vAPI": false,
"µMatrix": false
},

2
dist/version

@ -1 +1 @@
1.4.0
1.4.1.0

2
platform/chromium/manifest.json

@ -22,7 +22,7 @@
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["/js/vapi-client.js", "/js/contentscript-start.js"],
"js": ["/js/vapi.js", "/js/vapi-client.js", "/js/contentscript-start.js"],
"run_at": "document_start",
"all_frames": true
},

1841
platform/chromium/vapi-background.js
File diff suppressed because it is too large
View File

308
platform/chromium/vapi-client-extra.js

@ -0,0 +1,308 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
// For non-background page
'use strict';
/******************************************************************************/
// Direct messaging connection ability
(( ) => {
// >>>>>>>> start of private namespace
if (
typeof vAPI !== 'object' ||
vAPI.messaging instanceof Object === false ||
vAPI.MessagingConnection instanceof Function
) {
return;
}
const listeners = new Set();
const connections = new Map();
vAPI.MessagingConnection = class {
constructor(handler, details) {
this.messaging = vAPI.messaging;
this.handler = handler;
this.id = details.id;
this.to = details.to;
this.toToken = details.toToken;
this.from = details.from;
this.fromToken = details.fromToken;
this.checkTimer = undefined;
// On Firefox it appears ports are not automatically disconnected
// when navigating to another page.
const ctor = vAPI.MessagingConnection;
if ( ctor.pagehide !== undefined ) { return; }
ctor.pagehide = ( ) => {
for ( const connection of connections.values() ) {
connection.disconnect();
connection.handler(
connection.toDetails('connectionBroken')
);
}
};
window.addEventListener('pagehide', ctor.pagehide);
}
toDetails(what, payload) {
return {
what: what,
id: this.id,
from: this.from,
fromToken: this.fromToken,
to: this.to,
toToken: this.toToken,
payload: payload
};
}
disconnect() {
if ( this.checkTimer !== undefined ) {
clearTimeout(this.checkTimer);
this.checkTimer = undefined;
}
connections.delete(this.id);
const port = this.messaging.getPort();
if ( port === null ) { return; }
port.postMessage({
channel: 'vapi',
msg: this.toDetails('connectionBroken'),
});
}
checkAsync() {
if ( this.checkTimer !== undefined ) {
clearTimeout(this.checkTimer);
}
this.checkTimer = vAPI.setTimeout(
( ) => { this.check(); },
499
);
}
check() {
this.checkTimer = undefined;
if ( connections.has(this.id) === false ) { return; }
const port = this.messaging.getPort();
if ( port === null ) { return; }
port.postMessage({
channel: 'vapi',
msg: this.toDetails('connectionCheck'),
});
this.checkAsync();
}
receive(details) {
switch ( details.what ) {
case 'connectionAccepted':
this.toToken = details.toToken;
this.handler(details);
this.checkAsync();
break;
case 'connectionBroken':
connections.delete(this.id);
this.handler(details);
break;
case 'connectionMessage':
this.handler(details);
this.checkAsync();
break;
case 'connectionCheck':
const port = this.messaging.getPort();
if ( port === null ) { return; }
if ( connections.has(this.id) ) {
this.checkAsync();
} else {
details.what = 'connectionBroken';
port.postMessage({ channel: 'vapi', msg: details });
}
break;
case 'connectionRefused':
connections.delete(this.id);
this.handler(details);
break;
}
}
send(payload) {
const port = this.messaging.getPort();
if ( port === null ) { return; }
port.postMessage({
channel: 'vapi',
msg: this.toDetails('connectionMessage', payload),
});
}
static addListener(listener) {
listeners.add(listener);
}
static async connectTo(from, to, handler) {
const port = vAPI.messaging.getPort();
if ( port === null ) { return; }
const connection = new vAPI.MessagingConnection(handler, {
id: `${from}-${to}-${vAPI.sessionId}`,
to: to,
from: from,
fromToken: port.name
});
connections.set(connection.id, connection);
port.postMessage({
channel: 'vapi',
msg: {
what: 'connectionRequested',
id: connection.id,
from: from,
fromToken: port.name,
to: to,
}
});
return connection.id;
}
static disconnectFrom(connectionId) {
const connection = connections.get(connectionId);
if ( connection === undefined ) { return; }
connection.disconnect();
}
static sendTo(connectionId, payload) {
const connection = connections.get(connectionId);
if ( connection === undefined ) { return; }
connection.send(payload);
}
static canDestroyPort() {
return listeners.length === 0 && connections.size === 0;
}
static mustDestroyPort() {
if ( connections.size === 0 ) { return; }
for ( const connection of connections.values() ) {
connection.receive({ what: 'connectionBroken' });
}
connections.clear();
}
static canProcessMessage(details) {
if ( details.channel !== 'vapi' ) { return; }
switch ( details.msg.what ) {
case 'connectionAccepted':
case 'connectionBroken':
case 'connectionCheck':
case 'connectionMessage':
case 'connectionRefused': {
const connection = connections.get(details.msg.id);
if ( connection === undefined ) { break; }
connection.receive(details.msg);
return true;
}
case 'connectionRequested':
if ( listeners.length === 0 ) { return; }
const port = vAPI.messaging.getPort();
if ( port === null ) { break; }
let listener, result;
for ( listener of listeners ) {
result = listener(details.msg);
if ( result !== undefined ) { break; }
}
if ( result === undefined ) { break; }
if ( result === true ) {
details.msg.what = 'connectionAccepted';
details.msg.toToken = port.name;
const connection = new vAPI.MessagingConnection(
listener,
details.msg
);
connections.set(connection.id, connection);
} else {
details.msg.what = 'connectionRefused';
}
port.postMessage(details);
return true;
default:
break;
}
}
};
vAPI.messaging.extensions.push(vAPI.MessagingConnection);
// <<<<<<<< end of private namespace
})();
/******************************************************************************/
// Broadcast listening ability
(( ) => {
// >>>>>>>> start of private namespace
if (
typeof vAPI !== 'object' ||
vAPI.messaging instanceof Object === false ||
vAPI.broadcastListener instanceof Object
) {
return;
}
const listeners = new Set();
vAPI.broadcastListener = {
add: function(listener) {
listeners.add(listener);
vAPI.messaging.getPort();
},
remove: function(listener) {
listeners.delete(listener);
},
canDestroyPort() {
return listeners.size === 0;
},
mustDestroyPort() {
listeners.clear();
},
canProcessMessage(details) {
if ( details.broadcast === false ) { return; }
for ( const listener of listeners ) {
listener(details.msg);
}
},
};
vAPI.messaging.extensions.push(vAPI.broadcastListener);
// <<<<<<<< end of private namespace
})();
/******************************************************************************/
/*******************************************************************************
DO NOT:
- Remove the following code
- Add code beyond the following code
Reason:
- https://github.com/gorhill/uBlock/pull/3721
- uBO never uses the return value from injected content scripts
**/
void 0;

273
platform/chromium/vapi-client.js

@ -1,7 +1,8 @@
/*******************************************************************************
uMatrix - a browser extension to block requests.
Copyright (C) 2014-2018 The uMatrix/uBlock Origin authors
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-2015 The uBlock Origin authors
Copyright (C) 2014-present 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
@ -16,60 +17,61 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uMatrix
Home: https://github.com/gorhill/uBlock
*/
// For non background pages
// For non-background page
'use strict';
/******************************************************************************/
(function(self) {
/******************************************************************************/
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1225&desc=6#c10
if ( self.vAPI === undefined || self.vAPI.uMatrix !== true ) {
self.vAPI = { uMatrix: true };
}
var vAPI = self.vAPI;
var chrome = self.chrome;
// https://github.com/chrisaljoudi/uBlock/issues/456
// Already injected?
if ( vAPI.vapiClientInjected ) {
//console.debug('vapi-client.js already injected: skipping.');
return;
}
vAPI.vapiClientInjected = true;
// Skip if already injected.
vAPI.sessionId = String.fromCharCode(Date.now() % 26 + 97) +
Math.random().toString(36).slice(2);
// >>>>>>>> start of HUGE-IF-BLOCK
if (
typeof vAPI === 'object' &&
vAPI.randomToken instanceof Function === false
) {
/******************************************************************************/
/******************************************************************************/
vAPI.shutdown = (function() {
var jobs = [];
vAPI.randomToken = function() {
const now = Date.now();
return String.fromCharCode(now % 26 + 97) +
Math.floor((1 + Math.random()) * now).toString(36);
};
var add = function(job) {
jobs.push(job);
};
vAPI.sessionId = vAPI.randomToken();
vAPI.setTimeout = vAPI.setTimeout || self.setTimeout.bind(self);
var exec = function() {
//console.debug('Shutting down...');
var job;
while ( (job = jobs.pop()) ) {
job();
}
};
/******************************************************************************/
return {
add: add,
exec: exec
};
})();
vAPI.shutdown = {
jobs: [],
add: function(job) {
this.jobs.push(job);
},
exec: function() {
// Shutdown asynchronously, to ensure shutdown jobs are called from
// the top context.
self.requestIdleCallback(( ) => {
const jobs = this.jobs.slice();
this.jobs.length = 0;
while ( jobs.length !== 0 ) {
(jobs.pop())();
}
});
},
remove: function(job) {
let pos;
while ( (pos = this.jobs.indexOf(job)) !== -1 ) {
this.jobs.splice(pos, 1);
}
}
};
/******************************************************************************/
@ -77,9 +79,10 @@ vAPI.messaging = {
port: null,
portTimer: null,
portTimerDelay: 10000,
listeners: new Set(),
extended: undefined,
extensions: [],
msgIdGenerator: 1,
pending: new Map(),
auxProcessId: 1,
shuttingDown: false,
shutdown: function() {
@ -87,41 +90,53 @@ vAPI.messaging = {
this.destroyPort();
},
// https://github.com/uBlockOrigin/uBlock-issues/issues/403
// Spurious disconnection can happen, so do not consider such events
// as world-ending, i.e. stay around. Except for embedded frames.
disconnectListener: function() {
this.port = null;
vAPI.shutdown.exec();
if ( window !== window.top ) {
vAPI.shutdown.exec();
}
},
disconnectListenerBound: null,
messageListener: function(details) {
if ( !details ) { return; }
// Sent to all listeners
if ( details.broadcast ) {
this.sendToListeners(details.msg);
return;
}
if ( details instanceof Object === false ) { return; }
// Response to specific message previously sent
var listener;
if ( details.auxProcessId ) {
listener = this.pending.get(details.auxProcessId);
if ( listener !== undefined ) {
this.pending.delete(details.auxProcessId);
listener(details.msg);
if ( details.msgId !== undefined ) {
const resolver = this.pending.get(details.msgId);
if ( resolver !== undefined ) {
this.pending.delete(details.msgId);
resolver(details.msg);
return;
}
}
// Unhandled messages
this.extensions.every(ext => ext.canProcessMessage(details) !== true);
},
messageListenerBound: null,
canDestroyPort: function() {
return this.pending.size === 0 &&
(
this.extensions.length === 0 ||
this.extensions.every(e => e.canDestroyPort())
);
},
mustDestroyPort: function() {
if ( this.extensions.length === 0 ) { return; }
this.extensions.forEach(e => e.mustDestroyPort());
this.extensions.length = 0;
},
messageListenerCallback: null,
portPoller: function() {
this.portTimer = null;
if (
this.port !== null &&
this.listeners.size === 0 &&
this.pending.size === 0
) {
if ( this.port !== null && this.canDestroyPort() ) {
return this.destroyPort();
}
this.portTimer = vAPI.setTimeout(this.portPollerBound, this.portTimerDelay);
@ -134,48 +149,50 @@ vAPI.messaging = {
clearTimeout(this.portTimer);
this.portTimer = null;
}
var port = this.port;
const port = this.port;
if ( port !== null ) {
port.disconnect();
port.onMessage.removeListener(this.messageListenerCallback);
port.onMessage.removeListener(this.messageListenerBound);
port.onDisconnect.removeListener(this.disconnectListenerBound);
this.port = null;
}
this.listeners.clear();
this.mustDestroyPort();
// service pending callbacks
if ( this.pending.size !== 0 ) {
var pending = this.pending;
const pending = this.pending;
this.pending = new Map();
for ( var callback of pending.values() ) {
if ( typeof callback === 'function' ) {
callback(null);
}
for ( const resolver of pending.values() ) {
resolver();
}
}
},
createPort: function() {
if ( this.shuttingDown ) { return null; }
if ( this.messageListenerCallback === null ) {
this.messageListenerCallback = this.messageListener.bind(this);
if ( this.messageListenerBound === null ) {
this.messageListenerBound = this.messageListener.bind(this);
this.disconnectListenerBound = this.disconnectListener.bind(this);
this.portPollerBound = this.portPoller.bind(this);
}
try {
this.port = chrome.runtime.connect({name: vAPI.sessionId}) || null;
this.port = browser.runtime.connect({name: vAPI.sessionId}) || null;
} catch (ex) {
this.port = null;
}
if ( this.port !== null ) {
this.port.onMessage.addListener(this.messageListenerCallback);
this.port.onDisconnect.addListener(this.disconnectListenerBound);
this.portTimerDelay = 10000;
if ( this.portTimer === null ) {
this.portTimer = vAPI.setTimeout(
this.portPollerBound,
this.portTimerDelay
);
}
// Not having a valid port at this point means the main process is
// not available: no point keeping the content scripts alive.
if ( this.port === null ) {
vAPI.shutdown.exec();
return null;
}
this.port.onMessage.addListener(this.messageListenerBound);
this.port.onDisconnect.addListener(this.disconnectListenerBound);
this.portTimerDelay = 10000;
if ( this.portTimer === null ) {
this.portTimer = vAPI.setTimeout(
this.portPollerBound,
this.portTimerDelay
);
}
return this.port;
},
@ -184,70 +201,68 @@ vAPI.messaging = {
return this.port !== null ? this.port : this.createPort();
},
send: function(channelName, message, callback) {
send: function(channel, msg) {
// Too large a gap between the last request and the last response means
// the main process is no longer reachable: memory leaks and bad
// performance become a risk -- especially for long-lived, dynamic
// pages. Guard against this.
if ( this.pending.size > 25 ) {
if ( this.pending.size > 50 ) {
vAPI.shutdown.exec();
}
var port = this.getPort();
const port = this.getPort();
if ( port === null ) {
if ( typeof callback === 'function' ) { callback(); }
return;
return Promise.resolve();
}
var auxProcessId;
if ( callback ) {
auxProcessId = this.auxProcessId++;
this.pending.set(auxProcessId, callback);
}
port.postMessage({
channelName: channelName,
auxProcessId: auxProcessId,
msg: message
const msgId = this.msgIdGenerator++;
const promise = new Promise(resolve => {
this.pending.set(msgId, resolve);
});
port.postMessage({ channel, msgId, msg });
return promise;
},
addListener: function(listener) {
this.listeners.add(listener);
this.getPort();
},
removeListener: function(listener) {
this.listeners.delete(listener);
},
removeAllListeners: function() {
this.listeners.clear();
},
sendToListeners: function(msg) {
for ( var listener of this.listeners ) {
listener(msg);
// Dynamically extend capabilities.
extend: function() {
if ( this.extended === undefined ) {
this.extended = vAPI.messaging.send('vapi', {
what: 'extendClient'
}).then(( ) => {
return self.vAPI instanceof Object &&
this.extensions.length !== 0;
}).catch(( ) => {
});
}
}
return this.extended;
},
};
vAPI.shutdown.add(( ) => {
vAPI.messaging.shutdown();
window.vAPI = undefined;
});
/******************************************************************************/
/******************************************************************************/
// No need to have vAPI client linger around after shutdown if
// we are not a top window (because element picker can still
// be injected in top window).
if ( window !== window.top ) {
vAPI.shutdown.add(function() {
vAPI = null;
});
}
// <<<<<<<< end of HUGE-IF-BLOCK
/******************************************************************************/
vAPI.setTimeout = vAPI.setTimeout || function(callback, delay) {
setTimeout(function() { callback(); }, delay);
};
/******************************************************************************/
})(this); // jshint ignore: line
/******************************************************************************/
/*******************************************************************************
DO NOT:
- Remove the following code
- Add code beyond the following code
Reason:
- https://github.com/gorhill/uBlock/pull/3721
- uBO never uses the return value from injected content scripts
**/
void 0;

237
platform/chromium/vapi-common.js

@ -23,32 +23,14 @@
'use strict';
/******************************************************************************/
if ( self.browser instanceof Object ) {
self.chrome = self.browser;
} else {
self.browser = self.chrome;
}
/******************************************************************************/
/******************************************************************************/
(function(self) {
vAPI.T0 = Date.now();
/******************************************************************************/
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1225&desc=6#c10
if ( self.vAPI === undefined || self.vAPI.uMatrix !== true ) {
self.vAPI = { uMatrix: true };
}
var vAPI = self.vAPI;
var chrome = self.chrome;
/******************************************************************************/
vAPI.setTimeout = vAPI.setTimeout || window.setTimeout.bind(window);
vAPI.setTimeout = vAPI.setTimeout || self.setTimeout.bind(self);
/******************************************************************************/
@ -57,113 +39,182 @@ vAPI.webextFlavor = {
soup: new Set()
};
(function() {
var ua = navigator.userAgent,
flavor = vAPI.webextFlavor,
soup = flavor.soup;
var dispatch = function() {
(( ) => {
const ua = navigator.userAgent;
const flavor = vAPI.webextFlavor;
const soup = flavor.soup;
const dispatch = function() {
window.dispatchEvent(new CustomEvent('webextFlavor'));
};
// This is always true.
soup.add('ublock');
soup.add('ublock').add('webext');
// Whether this is a dev build.
if ( /^\d+\.\d+\.\d+\D/.test(browser.runtime.getManifest().version) ) {
soup.add('devbuild');
}
if ( /\bMobile\b/.test(ua) ) {
soup.add('mobile');
}
// Asynchronous
var async = self.browser instanceof Object &&
typeof self.browser.runtime.getBrowserInfo === 'function';
if ( async ) {
self.browser.runtime.getBrowserInfo().then(function(info) {
flavor.major = parseInt(info.version, 10) || 0;
if (
browser instanceof Object &&
typeof browser.runtime.getBrowserInfo === 'function'
) {
browser.runtime.getBrowserInfo().then(info => {
flavor.major = parseInt(info.version, 10) || 60;
soup.add(info.vendor.toLowerCase())
.add(info.name.toLowerCase());
if ( flavor.major >= 53 ) { soup.add('user_stylesheet'); }
if ( flavor.major >= 57 ) { soup.add('html_filtering'); }
if ( soup.has('firefox') && flavor.major < 57 ) {
soup.delete('html_filtering');
}
dispatch();
});
if ( browser.runtime.getURL('').startsWith('moz-extension://') ) {
soup.add('mozilla')
.add('firefox')
.add('user_stylesheet')
.add('html_filtering');
flavor.major = 60;
}
return;
}
// Synchronous
var match = /Firefox\/([\d.]+)/.exec(ua);
if ( match !== null ) {
// Synchronous -- order of tests is important
let match;
if ( (match = /\bEdge\/(\d+)/.exec(ua)) !== null ) {
flavor.major = parseInt(match[1], 10) || 0;
soup.add('mozilla')
.add('firefox');
if ( flavor.major >= 53 ) { soup.add('user_stylesheet'); }
if ( flavor.major >= 57 ) { soup.add('html_filtering'); }
} else {
match = /OPR\/([\d.]+)/.exec(ua);
if ( match !== null ) {
var reEx = /Chrom(?:e|ium)\/([\d.]+)/;
if ( reEx.test(ua) ) { match = reEx.exec(ua); }
flavor.major = parseInt(match[1], 10) || 0;
soup.add('opera').add('chromium');
} else {
match = /Chromium\/([\d.]+)/.exec(ua);
if ( match !== null ) {
flavor.major = parseInt(match[1], 10) || 0;
soup.add('chromium');
} else {
match = /Chrome\/([\d.]+)/.exec(ua);
if ( match !== null ) {
flavor.major = parseInt(match[1], 10) || 0;
soup.add('google').add('chromium');
}
}
}
// https://github.com/gorhill/uBlock/issues/3588
if ( soup.has('chromium') && flavor.major >= 67 ) {
soup.add('user_stylesheet');
}
soup.add('microsoft').add('edge');
} else if ( (match = /\bOPR\/(\d+)/.exec(ua)) !== null ) {
const reEx = /\bChrom(?:e|ium)\/([\d.]+)/;
if ( reEx.test(ua) ) { match = reEx.exec(ua); }
flavor.major = parseInt(match[1], 10) || 0;
soup.add('opera').add('chromium');
} else if ( (match = /\bChromium\/(\d+)/.exec(ua)) !== null ) {
flavor.major = parseInt(match[1], 10) || 0;
soup.add('chromium');
} else if ( (match = /\bChrome\/(\d+)/.exec(ua)) !== null ) {
flavor.major = parseInt(match[1], 10) || 0;
soup.add('google').add('chromium');
} else if ( (match = /\bSafari\/(\d+)/.exec(ua)) !== null ) {
flavor.major = parseInt(match[1], 10) || 0;
soup.add('apple').add('safari');
}
// Don't starve potential listeners
if ( !async ) {
vAPI.setTimeout(dispatch, 97);
// https://github.com/gorhill/uBlock/issues/3588
if ( soup.has('chromium') && flavor.major >= 66 ) {
soup.add('user_stylesheet');
}
// Don't starve potential listeners
vAPI.setTimeout(dispatch, 97);
})();
/******************************************************************************/
// http://www.w3.org/International/questions/qa-scripts#directions
{
const punycode = self.punycode;
const reCommonHostnameFromURL = /^https?:\/\/([0-9a-z_][0-9a-z._-]*[0-9a-z])\//;
const reAuthorityFromURI = /^(?:[^:\/?#]+:)?(\/\/[^\/?#]+)/;
const reHostFromNakedAuthority = /^[0-9a-z._-]+[0-9a-z]$/i;
const reHostFromAuthority = /^(?:[^@]*@)?([^:]+)(?::\d*)?$/;
const reIPv6FromAuthority = /^(?:[^@]*@)?(\[[0-9a-f:]+\])(?::\d*)?$/i;
const reMustNormalizeHostname = /[^0-9a-z._-]/;
vAPI.hostnameFromURI = function(uri) {
let matches = reCommonHostnameFromURL.exec(uri);
if ( matches !== null ) { return matches[1]; }
matches = reAuthorityFromURI.exec(uri);
if ( matches === null ) { return ''; }
const authority = matches[1].slice(2);
if ( reHostFromNakedAuthority.test(authority) ) {
return authority.toLowerCase();
}
matches = reHostFromAuthority.exec(authority);
if ( matches === null ) {
matches = reIPv6FromAuthority.exec(authority);
if ( matches === null ) { return ''; }
}
let hostname = matches[1];
while ( hostname.endsWith('.') ) {
hostname = hostname.slice(0, -1);
}
if ( reMustNormalizeHostname.test(hostname) ) {
hostname = punycode.toASCII(hostname.toLowerCase());
}
return hostname;
};
var setScriptDirection = function(language) {
document.body.setAttribute(
'dir',
['ar', 'he', 'fa', 'ps', 'ur'].indexOf(language) !== -1 ? 'rtl' : 'ltr'
);
};
const reHostnameFromNetworkURL =
/^(?:http|ws|ftp)s?:\/\/([0-9a-z_][0-9a-z._-]*[0-9a-z])\//;
vAPI.hostnameFromNetworkURL = function(url) {
const matches = reHostnameFromNetworkURL.exec(url);
return matches !== null ? matches[1] : '';
};
const psl = self.publicSuffixList;
const reIPAddressNaive = /^\d+\.\d+\.\d+\.\d+$|^\[[\da-zA-Z:]+\]$/;
vAPI.domainFromHostname = function(hostname) {
return reIPAddressNaive.test(hostname)
? hostname
: psl.getDomain(hostname);
};
vAPI.domainFromURI = function(uri) {
return uri !== ''
? vAPI.domainFromHostname(vAPI.hostnameFromURI(uri))
: '';
};
}
/******************************************************************************/
vAPI.download = function(details) {
if ( !details.url ) {
return;
}
var a = document.createElement('a');
if ( !details.url ) { return; }
const a = document.createElement('a');
a.href = details.url;
a.setAttribute('download', details.filename || '');
a.setAttribute('type', 'text/plain');
a.dispatchEvent(new MouseEvent('click'));
};
/******************************************************************************/
vAPI.getURL = chrome.runtime.getURL;
vAPI.getURL = browser.runtime.getURL;
/******************************************************************************/
vAPI.i18n = chrome.i18n.getMessage;
vAPI.i18n = browser.i18n.getMessage;
setScriptDirection(vAPI.i18n('@@ui_locale'));
// http://www.w3.org/International/questions/qa-scripts#directions
document.body.setAttribute(
'dir',
['ar', 'he', 'fa', 'ps', 'ur'].indexOf(vAPI.i18n('@@ui_locale')) !== -1
? 'rtl'
: 'ltr'
);
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/3057
// - webNavigation.onCreatedNavigationTarget become broken on Firefox when we
// try to make the popup panel close itself using the original
// `window.open('', '_self').close()`.
vAPI.closePopup = function() {
window.close();
if ( vAPI.webextFlavor.soup.has('firefox') ) {
window.close();
return;
}
// TODO: try to figure why this was used instead of a plain window.close().
// https://github.com/gorhill/uBlock/commit/b301ac031e0c2e9a99cb6f8953319d44e22f33d2#diff-bc664f26b9c453e0d43a9379e8135c6a
window.open('', '_self').close();
};
/******************************************************************************/
@ -207,8 +258,22 @@ vAPI.localStorage = {
}
};
/******************************************************************************/
})(this);
/******************************************************************************/
/*******************************************************************************
DO NOT:
- Remove the following code
- Add code beyond the following code
Reason:
- https://github.com/gorhill/uBlock/pull/3721
- uBO never uses the return value from injected content scripts
**/
void 0;

230
platform/chromium/vapi-webrequest.js

@ -25,40 +25,17 @@
/******************************************************************************/
(function() {
(( ) => {
// https://github.com/uBlockOrigin/uBlock-issues/issues/407
if ( vAPI.webextFlavor.soup.has('chromium') === false ) { return; }
const extToTypeMap = new Map([
['eot','font'],['otf','font'],['svg','font'],['ttf','font'],['woff','font'],['woff2','font'],
['mp3','media'],['mp4','media'],['webm','media'],
['gif','image'],['ico','image'],['jpeg','image'],['jpg','image'],['png','image'],['webp','image']
]);
// https://www.reddit.com/r/uBlockOrigin/comments/9vcrk3/bug_in_ubo_1173_betas_when_saving_files_hosted_on/
// Some types can be mapped from 'other', thus include 'other' if and
// only if the caller is interested in at least one of those types.
const denormalizeTypes = function(aa) {
if ( aa.length === 0 ) {
return Array.from(vAPI.net.validTypes);
}
const out = new Set();
let i = aa.length;
while ( i-- ) {
const type = aa[i];
if ( vAPI.net.validTypes.has(type) ) {
out.add(type);
}
}
if ( out.has('other') === false ) {
for ( const type of extToTypeMap.values() ) {
if ( out.has(type) ) {
out.add('other');
break;
}
}
}
return Array.from(out);
};
const headerValue = function(headers, name) {
const headerValue = (headers, name) => {
let i = headers.length;
while ( i-- ) {
if ( headers[i].name.toLowerCase() === name ) {
@ -70,128 +47,131 @@
const parsedURL = new URL('https://www.example.org/');
vAPI.net.normalizeDetails = function(details) {
let type = details.type;
// Extend base class to normalize as per platform.
// https://github.com/uBlockOrigin/uMatrix-issues/issues/156#issuecomment-494427094
if ( type === 'main_frame' ) {
details.documentUrl = details.url;
vAPI.Net = class extends vAPI.Net {
constructor() {
super();
this.suspendedTabIds = new Set();
}
// Chromium 63+ supports the `initiator` property, which contains
// the URL of the origin from which the network request was made.
else if (
typeof details.initiator === 'string' &&
details.initiator !== 'null'
) {
details.documentUrl = details.initiator;
}
// https://github.com/gorhill/uBlock/issues/1493
// Chromium 49+/WebExtensions support a new request type: `ping`,
// which is fired as a result of using `navigator.sendBeacon`.
if ( type === 'ping' ) {
details.type = 'beacon';
return;
}
if ( type === 'imageset' ) {
details.type = 'image';
return;
}
// The rest of the function code is to normalize type
if ( type !== 'other' ) { return; }
normalizeDetails(details) {
// Chromium 63+ supports the `initiator` property, which contains
// the URL of the origin from which the network request was made.
if (
typeof details.initiator === 'string' &&
details.initiator !== 'null'
) {
details.documentUrl = details.initiator;
}
// Try to map known "extension" part of URL to request type.
parsedURL.href = details.url;
const path = parsedURL.pathname,
pos = path.indexOf('.', path.length - 6);
if ( pos !== -1 && (type = extToTypeMap.get(path.slice(pos + 1))) ) {
details.type = type;
return;
}
let type = details.type;
// Try to extract type from response headers if present.
if ( details.responseHeaders ) {
type = headerValue(details.responseHeaders, 'content-type');
if ( type.startsWith('font/') ) {
details.type = 'font';
return;
}
if ( type.startsWith('image/') ) {
if ( type === 'imageset' ) {
details.type = 'image';
return;
}
if ( type.startsWith('audio/') || type.startsWith('video/') ) {
details.type = 'media';
// The rest of the function code is to normalize type
if ( type !== 'other' ) { return; }
// Try to map known "extension" part of URL to request type.
parsedURL.href = details.url;
const path = parsedURL.pathname,
pos = path.indexOf('.', path.length - 6);
if ( pos !== -1 && (type = extToTypeMap.get(path.slice(pos + 1))) ) {
details.type = type;
return;
}
}
};
vAPI.net.denormalizeFilters = function(filters) {
const urls = filters.urls || [ '<all_urls>' ];
let types = filters.types;
if ( Array.isArray(types) ) {
types = denormalizeTypes(types);
// Try to extract type from response headers if present.
if ( details.responseHeaders ) {
type = headerValue(details.responseHeaders, 'content-type');
if ( type.startsWith('font/') ) {
details.type = 'font';
return;
}
if ( type.startsWith('image/') ) {
details.type = 'image';
return;
}
if ( type.startsWith('audio/') || type.startsWith('video/') ) {
details.type = 'media';
return;
}
}
}
if (
(vAPI.net.validTypes.has('websocket')) &&
(types === undefined || types.indexOf('websocket') !== -1) &&
(urls.indexOf('<all_urls>') === -1)
) {
if ( urls.indexOf('ws://*/*') === -1 ) {
urls.push('ws://*/*');
// https://www.reddit.com/r/uBlockOrigin/comments/9vcrk3/
// Some types can be mapped from 'other', thus include 'other' if and
// only if the caller is interested in at least one of those types.
denormalizeTypes(types) {
if ( types.length === 0 ) {
return Array.from(this.validTypes);
}
const out = new Set();
for ( const type of types ) {
if ( this.validTypes.has(type) ) {
out.add(type);
}
}
if ( urls.indexOf('wss://*/*') === -1 ) {
urls.push('wss://*/*');
if ( out.has('other') === false ) {
for ( const type of extToTypeMap.values() ) {
if ( out.has(type) ) {
out.add('other');
break;
}
}
}
return Array.from(out);
}
suspendOneRequest(details) {
this.suspendedTabIds.add(details.tabId);
return { cancel: true };
}
unsuspendAllRequests() {
for ( const tabId of this.suspendedTabIds ) {
vAPI.tabs.reload(tabId);
}
this.suspendedTabIds.clear();
}
return { types, urls };
};
})();
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/2067
// Experimental: Block everything until uBO is fully ready.
// https://github.com/uBlockOrigin/uBlock-issues/issues/548
// Use `X-DNS-Prefetch-Control` to workaround Chromium's disregard of the
// setting "Predict network actions to improve page load performance".
vAPI.net.onBeforeReady = (function() {
let pendings;
vAPI.prefetching = (( ) => {
// https://github.com/uBlockOrigin/uBlock-issues/issues/407
if ( vAPI.webextFlavor.soup.has('chromium') === false ) { return; }
const handler = function(details) {
if ( pendings === undefined ) { return; }
if ( details.tabId < 0 ) { return; }
let listening = false;
pendings.add(details.tabId);
//console.log(`Aborting tab ${details.tabId}: ${details.type} ${details.url}`);
return { cancel: true };
const onHeadersReceived = function(details) {
details.responseHeaders.push({
name: 'X-DNS-Prefetch-Control',
value: 'off'
});
return { responseHeaders: details.responseHeaders };
};
return {
experimental: true,
start: function() {
pendings = new Set();
browser.webRequest.onBeforeRequest.addListener(
handler,
{ urls: [ 'http://*/*', 'https://*/*' ] },
[ 'blocking' ]
return state => {
const wr = chrome.webRequest;
if ( state && listening ) {
wr.onHeadersReceived.removeListener(onHeadersReceived);
listening = false;
} else if ( !state && !listening ) {
wr.onHeadersReceived.addListener(
onHeadersReceived,
{
urls: [ 'http://*/*', 'https://*/*' ],
types: [ 'main_frame', 'sub_frame' ]
},
[ 'blocking', 'responseHeaders' ]
);
},
// https://github.com/gorhill/uBlock/issues/2067
// Force-reload tabs for which network requests were blocked
// during launch. This can happen only if tabs were "suspended".
stop: function() {
if ( pendings === undefined ) { return; }
browser.webRequest.onBeforeRequest.removeListener(handler);
for ( const tabId of pendings ) {
//console.log(`Reloading tab ${tabId}`);
vAPI.tabs.reload(tabId);
}
pendings = undefined;
},
listening = true;
}
};
})();

86
platform/chromium/vapi.js

@ -0,0 +1,86 @@
/*******************************************************************************
uMatrix - a browser extension to block requests.
Copyright (C) 2017-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
/* global HTMLDocument, XMLDocument */
// For background page, auxiliary pages, and content scripts.
/******************************************************************************/
if ( self.browser instanceof Object ) {
self.chrome = self.browser;
} else {
self.browser = self.chrome;
}
/******************************************************************************/
// https://bugzilla.mozilla.org/show_bug.cgi?id=1408996#c9
var vAPI = self.vAPI; // jshint ignore:line
// https://github.com/chrisaljoudi/uBlock/issues/464
// https://github.com/chrisaljoudi/uBlock/issues/1528
// A XMLDocument can be a valid HTML document.
// https://github.com/gorhill/uBlock/issues/1124
// Looks like `contentType` is on track to be standardized:
// https://dom.spec.whatwg.org/#concept-document-content-type
// https://forums.lanik.us/viewtopic.php?f=64&t=31522
// Skip text/plain documents.
if (
(
document instanceof HTMLDocument ||
document instanceof XMLDocument &&
document.createElement('div') instanceof HTMLDivElement
) &&
(
/^image\/|^text\/plain/.test(document.contentType || '') === false
) &&
(
self.vAPI instanceof Object === false || vAPI.uMatrix !== true
)
) {
vAPI = self.vAPI = { uMatrix: true };
}
/*******************************************************************************
DO NOT:
- Remove the following code
- Add code beyond the following code
Reason:
- https://github.com/gorhill/uBlock/pull/3721
- uMatrix never uses the return value from injected content scripts
**/
void 0;

176
platform/chromium/webext.js

@ -0,0 +1,176 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
// `webext` is a promisified api of `chrome`. Entries are added as
// the promisification of uBO progress.
const webext = (( ) => { // jshint ignore:line
// >>>>> start of private scope
const noopFunc = ( ) => { };
const promisifyNoFail = function(thisArg, fnName, outFn = r => r) {
const fn = thisArg[fnName];
return function() {
return new Promise(resolve => {
fn.call(thisArg, ...arguments, function() {
if ( chrome.runtime.lastError instanceof Object ) {
void chrome.runtime.lastError.message;
}
resolve(outFn(...arguments));
});
});
};
};
const promisify = function(thisArg, fnName) {
const fn = thisArg[fnName];
return function() {
return new Promise((resolve, reject) => {
fn.call(thisArg, ...arguments, function() {
const lastError = chrome.runtime.lastError;
if ( lastError instanceof Object ) {
return reject(lastError.message);
}
resolve(...arguments);
});
});
};
};
const webext = {
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction
browserAction: {
setBadgeBackgroundColor: promisifyNoFail(chrome.browserAction, 'setBadgeBackgroundColor'),
setBadgeText: promisifyNoFail(chrome.browserAction, 'setBadgeText'),
setBadgeTextColor: noopFunc,
setIcon: promisifyNoFail(chrome.browserAction, 'setIcon'),
setTitle: promisifyNoFail(chrome.browserAction, 'setTitle'),
},
cookies: {
getAll: promisifyNoFail(chrome.cookies, 'getAll'),
remove: promisifyNoFail(chrome.cookies, 'remove'),
},
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus
/*
menus: {
create: function() {
return chrome.contextMenus.create(...arguments, ( ) => {
void chrome.runtime.lastError;
});
},
onClicked: chrome.contextMenus.onClicked,
remove: promisifyNoFail(chrome.contextMenus, 'remove'),
},
*/
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/privacy
privacy: {
},
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage
storage: {
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/local
local: {
clear: promisify(chrome.storage.local, 'clear'),
get: promisify(chrome.storage.local, 'get'),
getBytesInUse: promisify(chrome.storage.local, 'getBytesInUse'),
remove: promisify(chrome.storage.local, 'remove'),
set: promisify(chrome.storage.local, 'set'),
},
},
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs
tabs: {
get: promisifyNoFail(chrome.tabs, 'get', tab => tab instanceof Object ? tab : null),
executeScript: promisifyNoFail(chrome.tabs, 'executeScript'),
insertCSS: promisifyNoFail(chrome.tabs, 'insertCSS'),
query: promisifyNoFail(chrome.tabs, 'query', tabs => Array.isArray(tabs) ? tabs : []),
reload: promisifyNoFail(chrome.tabs, 'reload'),
remove: promisifyNoFail(chrome.tabs, 'remove'),
update: promisifyNoFail(chrome.tabs, 'update', tab => tab instanceof Object ? tab : null),
},
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webNavigation
webNavigation: {
getFrame: promisify(chrome.webNavigation, 'getFrame'),
},
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/windows
windows: {
get: promisifyNoFail(chrome.windows, 'get', win => win instanceof Object ? win : null),
create: promisifyNoFail(chrome.windows, 'create', win => win instanceof Object ? win : null),
update: promisifyNoFail(chrome.windows, 'update', win => win instanceof Object ? win : null),
},
};
// browser.privacy entries
{
const settings = [
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/privacy/network
[ 'network', 'networkPredictionEnabled' ],
[ 'network', 'webRTCIPHandlingPolicy' ],
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/privacy/websites
[ 'websites', 'hyperlinkAuditingEnabled' ],
];
for ( const [ category, setting ] of settings ) {
let categoryEntry = webext.privacy[category];
if ( categoryEntry instanceof Object === false ) {
categoryEntry = webext.privacy[category] = {};
}
const settingEntry = categoryEntry[setting] = {};
const thisArg = chrome.privacy[category][setting];
settingEntry.clear = promisifyNoFail(thisArg, 'clear');
settingEntry.get = promisifyNoFail(thisArg, 'get');
settingEntry.set = promisifyNoFail(thisArg, 'set');
}
}
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/managed
if ( chrome.storage.managed instanceof Object ) {
webext.storage.managed = {
get: promisify(chrome.storage.managed, 'get'),
};
}
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/sync
if ( chrome.storage.sync instanceof Object ) {
webext.storage.sync = {
QUOTA_BYTES: chrome.storage.sync.QUOTA_BYTES,
QUOTA_BYTES_PER_ITEM: chrome.storage.sync.QUOTA_BYTES_PER_ITEM,
MAX_ITEMS: chrome.storage.sync.MAX_ITEMS,
MAX_WRITE_OPERATIONS_PER_HOUR: chrome.storage.sync.MAX_WRITE_OPERATIONS_PER_HOUR,
MAX_WRITE_OPERATIONS_PER_MINUTE: chrome.storage.sync.MAX_WRITE_OPERATIONS_PER_MINUTE,
clear: promisify(chrome.storage.sync, 'clear'),
get: promisify(chrome.storage.sync, 'get'),
getBytesInUse: promisify(chrome.storage.sync, 'getBytesInUse'),
remove: promisify(chrome.storage.sync, 'remove'),
set: promisify(chrome.storage.sync, 'set'),
};
}
// https://bugs.chromium.org/p/chromium/issues/detail?id=608854
if ( chrome.tabs.removeCSS instanceof Function ) {
webext.tabs.removeCSS = promisifyNoFail(chrome.tabs, 'removeCSS');
}
return webext;
// <<<<< end of private scope
})();

1
platform/firefox/manifest.json

@ -46,6 +46,7 @@
"permissions": [
"browsingData",
"cookies",
"dns",
"privacy",
"storage",
"tabs",

263
platform/firefox/vapi-cachestorage.js

@ -1,263 +0,0 @@
/*******************************************************************************
uMatrix - a browser extension to block requests.
Copyright (C) 2016-2017 The uBlock Origin authors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* global indexedDB, IDBDatabase */
'use strict';
/******************************************************************************/
// The code below has been originally manually imported from:
// Commit: https://github.com/nikrolls/uBlock-Edge/commit/d1538ea9bea89d507219d3219592382eee306134
// Commit date: 29 October 2016
// Commit author: https://github.com/nikrolls
// Commit message: "Implement cacheStorage using IndexedDB"
// The original imported code has been subsequently modified as it was not
// compatible with Firefox.
// (a Promise thing, see https://github.com/dfahlander/Dexie.js/issues/317)
// Furthermore, code to migrate from browser.storage.local to vAPI.cacheStorage
// has been added, for seamless migration of cache-related entries into
// indexedDB.
// Imported from uBlock Origin project.
vAPI.cacheStorage = (function() {
const STORAGE_NAME = 'uMatrixCacheStorage';
var db;
var pending = [];
// prime the db so that it's ready asap for next access.
getDb(noopfn);
return { get, set, remove, clear, getBytesInUse };
function get(input, callback) {
if ( typeof callback !== 'function' ) { return; }
if ( input === null ) {
return getAllFromDb(callback);
}
var toRead, output = {};
if ( typeof input === 'string' ) {
toRead = [ input ];
} else if ( Array.isArray(input) ) {
toRead = input;
} else /* if ( typeof input === 'object' ) */ {
toRead = Object.keys(input);
output = input;
}
return getFromDb(toRead, output, callback);
}
function set(input, callback) {
putToDb(input, callback);
}
function remove(key, callback) {
deleteFromDb(key, callback);
}
function clear(callback) {
clearDb(callback);
}
function getBytesInUse(keys, callback) {
// TODO: implement this
callback(0);
}
function genericErrorHandler(error) {
console.error('[%s]', STORAGE_NAME, error);
}
function noopfn() {
}
function processPendings() {
var cb;
while ( (cb = pending.shift()) ) {
cb(db);
}
}
function getDb(callback) {
if ( pending === undefined ) {
return callback();
}
if ( pending.length !== 0 ) {
return pending.push(callback);
}
if ( db instanceof IDBDatabase ) {
return callback(db);
}
pending.push(callback);
if ( pending.length !== 1 ) { return; }
// https://github.com/gorhill/uBlock/issues/3156
// I have observed that no event was fired in Tor Browser 7.0.7 +
// medium security level after the request to open the database was
// created. When this occurs, I have also observed that the `error`
// property was already set, so this means uBO can detect here whether
// the database can be opened successfully. A try-catch block is
// necessary when reading the `error` property because we are not
// allowed to read this propery outside of event handlers in newer
// implementation of IDBRequest (my understanding).
var req;
try {
req = indexedDB.open(STORAGE_NAME, 1);
if ( req.error ) {
console.log(req.error);
req = undefined;
}
} catch(ex) {
}
if ( req === undefined ) {
processPendings();
pending = undefined;
return;
}
req.onupgradeneeded = function(ev) {
req = undefined;
db = ev.target.result;
db.onerror = db.onabort = genericErrorHandler;
var table = db.createObjectStore(STORAGE_NAME, { keyPath: 'key' });
table.createIndex('value', 'value', { unique: false });
};
req.onsuccess = function(ev) {
req = undefined;
db = ev.target.result;
db.onerror = db.onabort = genericErrorHandler;
processPendings();
};
req.onerror = req.onblocked = function() {
req = undefined;
console.log(this.error);
processPendings();
pending = undefined;
};
}
function getFromDb(keys, store, callback) {
if ( typeof callback !== 'function' ) { return; }
if ( keys.length === 0 ) { return callback(store); }
var gotOne = function() {
if ( typeof this.result === 'object' ) {
store[this.result.key] = this.result.value;
}
};
getDb(function(db) {
if ( !db ) { return callback(); }
var transaction = db.transaction(STORAGE_NAME);
transaction.oncomplete =
transaction.onerror =
transaction.onabort = function() {
return callback(store);
};
var table = transaction.objectStore(STORAGE_NAME);
for ( var key of keys ) {
var req = table.get(key);
req.onsuccess = gotOne;
req.onerror = noopfn;
req = undefined;
}
});
}
function getAllFromDb(callback) {
if ( typeof callback !== 'function' ) {
callback = noopfn;
}
getDb(function(db) {
if ( !db ) { return callback(); }
var output = {};
var transaction = db.transaction(STORAGE_NAME);
transaction.oncomplete =
transaction.onerror =
transaction.onabort = function() {
callback(output);
};
var table = transaction.objectStore(STORAGE_NAME),
req = table.openCursor();
req.onsuccess = function(ev) {
var cursor = ev.target.result;
if ( !cursor ) { return; }
output[cursor.key] = cursor.value;
cursor.continue();
};
});
}
function putToDb(input, callback) {
if ( typeof callback !== 'function' ) {
callback = noopfn;
}
var keys = Object.keys(input);
if ( keys.length === 0 ) { return callback(); }
getDb(function(db) {
if ( !db ) { return callback(); }
var transaction = db.transaction(STORAGE_NAME, 'readwrite');
transaction.oncomplete =
transaction.onerror =
transaction.onabort = callback;
var table = transaction.objectStore(STORAGE_NAME);
for ( var key of keys ) {
var entry = {};
entry.key = key;
entry.value = input[key];
table.put(entry);
entry = undefined;
}
});
}
function deleteFromDb(input, callback) {
if ( typeof callback !== 'function' ) {
callback = noopfn;
}
var keys = Array.isArray(input) ? input.slice() : [ input ];
if ( keys.length === 0 ) { return callback(); }
getDb(function(db) {
if ( !db ) { return callback(); }
var transaction = db.transaction(STORAGE_NAME, 'readwrite');
transaction.oncomplete =
transaction.onerror =
transaction.onabort = callback;
var table = transaction.objectStore(STORAGE_NAME);
for ( var key of keys ) {
table.delete(key);
}
});
}
function clearDb(callback) {
if ( typeof callback !== 'function' ) {
callback = noopfn;
}
getDb(function(db) {
if ( !db ) { return callback(); }
var req = db.transaction(STORAGE_NAME, 'readwrite')
.objectStore(STORAGE_NAME)
.clear();
req.onsuccess = req.onerror = callback;
});
}
}());
/******************************************************************************/

316
platform/firefox/vapi-webrequest.js

@ -25,12 +25,14 @@
/******************************************************************************/
(function() {
(( ) => {
// https://github.com/uBlockOrigin/uBlock-issues/issues/407
if ( vAPI.webextFlavor.soup.has('firefox') === false ) { return; }
// https://github.com/gorhill/uBlock/issues/2950
// Firefox 56 does not normalize URLs to ASCII, uBO must do this itself.
// https://bugzilla.mozilla.org/show_bug.cgi?id=945240
const evalMustPunycode = function() {
const evalMustPunycode = ( ) => {
return vAPI.webextFlavor.soup.has('firefox') &&
vAPI.webextFlavor.major < 57;
};
@ -43,142 +45,218 @@
mustPunycode = evalMustPunycode();
}, { once: true });
const denormalizeTypes = function(aa) {
if ( aa.length === 0 ) {
return Array.from(vAPI.net.validTypes);
}
const out = new Set();
let i = aa.length;
while ( i-- ) {
let type = aa[i];
if ( vAPI.net.validTypes.has(type) ) {
out.add(type);
}
if ( type === 'image' && vAPI.net.validTypes.has('imageset') ) {
out.add('imageset');
}
if ( type === 'sub_frame' ) {
out.add('object');
}
}
return Array.from(out);
};
const punycode = self.punycode;
const reAsciiHostname = /^https?:\/\/[0-9a-z_.:@-]+[/?#]/;
const parsedURL = new URL('about:blank');
vAPI.net.normalizeDetails = function(details) {
if ( mustPunycode && !reAsciiHostname.test(details.url) ) {
parsedURL.href = details.url;
details.url = details.url.replace(
parsedURL.hostname,
punycode.toASCII(parsedURL.hostname)
);
}
// Related issues:
// - https://github.com/gorhill/uBlock/issues/1327
// - https://github.com/uBlockOrigin/uBlock-issues/issues/128
// - https://bugzilla.mozilla.org/show_bug.cgi?id=1503721
const type = details.type;
// Extend base class to normalize as per platform.
// https://github.com/gorhill/uBlock/issues/1493
// Chromium 49+/WebExtensions support a new request type: `ping`,
// which is fired as a result of using `navigator.sendBeacon`.
if ( type === 'ping' ) {
details.type = 'beacon';
return;
vAPI.Net = class extends vAPI.Net {
constructor() {
super();
this.pendingRequests = [];
this.cnames = new Map([ [ '', '' ] ]);
this.cnameIgnoreList = null;
this.cnameIgnore1stParty = true;
this.cnameIgnoreExceptions = true;
this.cnameIgnoreRootDocument = true;
this.cnameMaxTTL = 60;
this.cnameReplayFullURL = false;
this.cnameTimer = undefined;
this.canRevealCNAME = browser.dns instanceof Object;
}
if ( type === 'imageset' ) {
details.type = 'image';
return;
setOptions(options) {
super.setOptions(options);
this.cnameIgnoreList = this.regexFromStrList(options.cnameIgnoreList);
this.cnameIgnore1stParty = options.cnameIgnore1stParty !== false;
this.cnameIgnoreExceptions = options.cnameIgnoreExceptions !== false;
this.cnameIgnoreRootDocument = options.cnameIgnoreRootDocument !== false;
this.cnameMaxTTL = options.cnameMaxTTL || 120;
this.cnameReplayFullURL = options.cnameReplayFullURL === true;
this.cnames.clear(); this.cnames.set('', '');
}
normalizeDetails(details) {
if ( mustPunycode && !reAsciiHostname.test(details.url) ) {
parsedURL.href = details.url;
details.url = details.url.replace(
parsedURL.hostname,
punycode.toASCII(parsedURL.hostname)
);
}
const type = details.type;
if ( type === 'imageset' ) {
details.type = 'image';
return;
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/345
// Re-categorize an embedded object as a `sub_frame` if its
// content type is that of a HTML document.
if ( type === 'object' && Array.isArray(details.responseHeaders) ) {
for ( const header of details.responseHeaders ) {
if ( header.name.toLowerCase() === 'content-type' ) {
if ( header.value.startsWith('text/html') ) {
details.type = 'sub_frame';
// https://github.com/uBlockOrigin/uBlock-issues/issues/345
// Re-categorize an embedded object as a `sub_frame` if its
// content type is that of a HTML document.
if ( type === 'object' && Array.isArray(details.responseHeaders) ) {
for ( const header of details.responseHeaders ) {
if ( header.name.toLowerCase() === 'content-type' ) {
if ( header.value.startsWith('text/html') ) {
details.type = 'sub_frame';
}
break;
}
break;
}
}
}
};
vAPI.net.denormalizeFilters = function(filters) {
const urls = filters.urls || [ '<all_urls>' ];
let types = filters.types;
if ( Array.isArray(types) ) {
types = denormalizeTypes(types);
denormalizeTypes(types) {
if ( types.length === 0 ) {
return Array.from(this.validTypes);
}
const out = new Set();
for ( const type of types ) {
if ( this.validTypes.has(type) ) {
out.add(type);
}
if ( type === 'image' && this.validTypes.has('imageset') ) {
out.add('imageset');
}
if ( type === 'sub_frame' ) {
out.add('object');
}
}
return Array.from(out);
}
processCanonicalName(hn, cn, details) {
const hnBeg = details.url.indexOf(hn);
if ( hnBeg === -1 ) { return; }
const oldURL = details.url;
let newURL = oldURL.slice(0, hnBeg) + cn;
const hnEnd = hnBeg + hn.length;
if ( this.cnameReplayFullURL ) {
newURL += oldURL.slice(hnEnd);
} else {
const pathBeg = oldURL.indexOf('/', hnEnd);
if ( pathBeg !== -1 ) {
newURL += oldURL.slice(hnEnd, pathBeg + 1);
}
}
details.url = newURL;
details.aliasURL = oldURL;
return super.onBeforeSuspendableRequest(details);
}
if (
(vAPI.net.validTypes.has('websocket')) &&
(types === undefined || types.indexOf('websocket') !== -1) &&
(urls.indexOf('<all_urls>') === -1)
) {
if ( urls.indexOf('ws://*/*') === -1 ) {
urls.push('ws://*/*');
recordCanonicalName(hn, record) {
let cname =
typeof record.canonicalName === 'string' &&
record.canonicalName !== hn
? record.canonicalName
: '';
if (
cname !== '' &&
this.cnameIgnore1stParty &&
vAPI.domainFromHostname(cname) === vAPI.domainFromHostname(hn)
) {
cname = '';
}
if (
cname !== '' &&
this.cnameIgnoreList !== null &&
this.cnameIgnoreList.test(cname)
) {
cname = '';
}
if ( urls.indexOf('wss://*/*') === -1 ) {
urls.push('wss://*/*');
this.cnames.set(hn, cname);
if ( this.cnameTimer === undefined ) {
this.cnameTimer = self.setTimeout(
( ) => {
this.cnameTimer = undefined;
this.cnames.clear(); this.cnames.set('', '');
},
this.cnameMaxTTL * 60000
);
}
return cname;
}
return { types, urls };
};
})();
/******************************************************************************/
// Related issues:
// - https://github.com/gorhill/uBlock/issues/1327
// - https://github.com/uBlockOrigin/uBlock-issues/issues/128
// - https://bugzilla.mozilla.org/show_bug.cgi?id=1503721
vAPI.net.onBeforeReady = (function() {
let pendings;
const handler = function(details) {
if ( pendings === undefined ) { return; }
if ( details.tabId < 0 ) { return; }
//console.log(`Deferring tab ${details.tabId}: ${details.type} ${details.url}`);
const pending = {
details: Object.assign({}, details),
resolve: undefined,
promise: undefined
};
pending.promise = new Promise(function(resolve) {
pending.resolve = resolve;
});
pendings.push(pending);
return pending.promise;
};
return {
start: function() {
pendings = [];
browser.webRequest.onBeforeRequest.addListener(
handler,
{ urls: [ 'http://*/*', 'https://*/*' ] },
[ 'blocking' ]
regexFromStrList(list) {
if (
typeof list !== 'string' ||
list.length === 0 ||
list === 'unset' ||
browser.dns instanceof Object === false
) {
return null;
}
if ( list === '*' ) {
return /^./;
}
return new RegExp(
'(?:^|\.)(?:' +
list.trim()
.split(/\s+/)
.map(a => a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
.join('|') +
')$'
);
},
stop: function(resolver) {
if ( pendings === undefined ) { return; }
for ( const pending of pendings ) {
const details = pending.details;
vAPI.net.normalizeDetails(details);
//console.log(`Processing tab ${details.tabId}: ${details.type} ${details.url}`);
pending.resolve(resolver(details));
}
onBeforeSuspendableRequest(details) {
const r = super.onBeforeSuspendableRequest(details);
if ( this.canRevealCNAME === false ) { return r; }
if ( r !== undefined ) {
if ( r.cancel === false ) { return; }
if (
r.cancel === true ||
r.redirectUrl !== undefined ||
this.cnameIgnoreExceptions
) {
return r;
}
}
pendings = undefined;
},
if (
details.type === 'main_frame' &&
this.cnameIgnoreRootDocument
) {
return;
}
const hn = vAPI.hostnameFromNetworkURL(details.url);
const cname = this.cnames.get(hn);
if ( cname === '' ) { return; }
if ( cname !== undefined ) {
return this.processCanonicalName(hn, cname, details);
}
return browser.dns.resolve(hn, [ 'canonical_name' ]).then(
rec => {
const cname = this.recordCanonicalName(hn, rec);
if ( cname === '' ) { return; }
return this.processCanonicalName(hn, cname, details);
},
( ) => {
this.cnames.set(hn, '');
}
);
}
suspendOneRequest(details) {
const pending = {
details: Object.assign({}, details),
resolve: undefined,
promise: undefined
};
pending.promise = new Promise(resolve => {
pending.resolve = resolve;
});
this.pendingRequests.push(pending);
return pending.promise;
}
unsuspendAllRequests() {
const pendingRequests = this.pendingRequests;
this.pendingRequests = [];
for ( const entry of pendingRequests ) {
entry.resolve(this.onBeforeSuspendableRequest(entry.details));
}
}
canSuspend() {
return true;
}
};
})();

24
platform/firefox/webext.js

@ -0,0 +1,24 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
const webext = browser; // jshint ignore:line

187
src/_locales/en/messages.json

@ -73,8 +73,8 @@
"message": "script",
"description": "HAS TO FIT IN MATRIX HEADER!"
},
"xhrPrettyName": {
"message": "XHR",
"fetchPrettyName": {
"message": "fetch",
"description": "HAS TO FIT IN MATRIX HEADER!"
},
"framePrettyName": {
@ -139,6 +139,10 @@
"message": "Spoof <code><noscript></code> tags",
"description": "A menu entry in the matrix popup"
},
"matrixSwitchRevealCname" : {
"message": "Reveal canonical names",
"description": "A menu entry in the matrix popup"
},
"matrixRevertAllEntry" : {
"message": "Revert all temporary changes",
"description": "A menu entry in the matrix popup"
@ -284,8 +288,154 @@
"message": "Refresh",
"description": ""
},
"logAll":{
"message":"All",
"description":"Appears in the logger's tab selector"
},
"logBehindTheScene":{
"message":"Tabless",
"description":"Pretty name for behind-the-scene network requests"
},
"loggerCurrentTab":{
"message":"Current tab",
"description":"Appears in the logger's tab selector"
},
"loggerReloadTip":{
"message":"Reload the tab content",
"description":"Tooltip for the reload button in the logger page"
},
"loggerFilterInputPlaceholder" : {
"message": "filter expression(s)",
"description": "Appears in the input filed where filter expressions are entered"
},
"loggerEntryCookieDeleted" : {
"message": "cookie deleted: {{value}}",
"description": "An entry for when a cookie is deleted"
},
"loggerEntryDeleteCookieError" : {
"message": "failed to delete cookie: {{value}}",
"description": "An entry for when the browser cache is cleared"
},
"loggerEntryBrowserCacheCleared" : {
"message": "browser cache cleared",
"description": "An entry for when a cookie can't be deleted"
},
"loggerEntryAssetUpdated" : {
"message": "asset updated: {{value}}",
"description": "An entry for when an asset was updated"
},
"loggerRowFiltererButtonTip":{
"message":"Toggle logger filtering",
"description":"Tooltip for the row filterer button in the logger page"
},
"logFilterPrompt":{
"message":"filter logger content",
"description": "Placeholder string for logger output filtering input field"
},
"loggerPopupPanelTip":{
"message":"Toggle the popup panel",
"description":"Tooltip for the popup panel button in the logger page"
},
"loggerInfoTip":{
"message":"uBlock Origin wiki: The logger",
"description":"Tooltip for the top-right info label in the logger page"
},
"loggerClearTip":{
"message":"Clear logger",
"description":"Tooltip for the eraser in the logger page; used to blank the content of the logger"
},
"loggerPauseTip":{
"message":"Pause logger (discard all incoming data)",
"description":"Tooltip for the pause button in the logger page"
},
"loggerUnpauseTip":{
"message":"Unpause logger",
"description":"Tooltip for the play button in the logger page"
},
"loggerRowFiltererBuiltinTip":{
"message":"Logger filtering options",
"description":"Tooltip for the button to bring up logger output filtering options"
},
"loggerRowFiltererBuiltinNot":{
"message":"Not",
"description":"A keyword in the built-in row filtering expression"
},
"loggerRowFiltererBuiltinBlocked":{
"message":"blocked",
"description":"A keyword in the built-in row filtering expression"
},
"loggerRowFiltererBuiltinInfo":{
"message":"info",
"description":"A keyword in the built-in row filtering expression"
},
"loggerRowFiltererBuiltin1p":{
"message":"1st-party",
"description":"A keyword in the built-in row filtering expression"
},
"loggerRowFiltererBuiltin3p":{
"message":"3rd-party",
"description":"A keyword in the built-in row filtering expression"
},
"loggerEntryDetailsHeader":{
"message":"Details",
"description":"Small header to identify the 'Details' pane for a specific logger entry"
},
"loggerEntryDetailsContext":{
"message":"Context",
"description":"Label to identify a context field (typically a hostname)"
},
"loggerEntryDetailsPartyness":{
"message":"Partyness",
"description":"Label to identify a field providing partyness information"
},
"loggerEntryDetailsType":{
"message":"Type",
"description":"Label to identify the type of an entry"
},
"loggerEntryDetailsURL":{
"message":"URL",
"description":"Label to identify the URL of an entry"
},
"loggerEntryRuleHeader":{
"message":"Rule",
"description":"Small header to identify the 'Rule' pane for a specific logger entry"
},
"loggerSettingDiscardPrompt":{
"message":"Logger entries which do not fulfill all three conditions below will be automatically discarded:",
"description":"Logger setting: A sentence to describe the purpose of the settings below"
},
"loggerSettingPerEntryMaxAge":{
"message":"Preserve entries from the last {{input}} minutes",
"description":"A logger setting"
},
"loggerSettingPerTabMaxLoads":{
"message":"Preserve at most {{input}} page loads per tab",
"description":"A logger setting"
},
"loggerSettingPerTabMaxEntries":{
"message":"Preserve at most {{input}} entries per tab",
"description":"A logger setting"
},
"loggerSettingPerEntryLineCount":{
"message":"Use {{input}} lines per entry in vertically expanded mode",
"description":"A logger setting"
},
"loggerExportFormatList":{
"message":"List",
"description":"Label for radio-button to pick export format"
},
"loggerExportFormatTable":{
"message":"Table",
"description":"Label for radio-button to pick export format"
},
"loggerExportEncodePlain":{
"message":"Plain",
"description":"Label for radio-button to pick export text format"
},
"loggerExportEncodeMarkdown":{
"message":"Markdown",
"description":"Label for radio-button to pick export text format"
},
"settingsPageTitle" : {
"message": "uMatrix &ndash; Settings",
"description": ""
@ -622,31 +772,6 @@
"description": "Message asking user to confirm reset"
},
"loggerFilterInputPlaceholder" : {
"message": "filter expression(s)",
"description": "Appears in the input filed where filter expressions are entered"
},
"loggerMaxEntriesTip" : {
"message": "Maximum number of entries",
"description": "Appears as a tooltip when hovering the input field"
},
"loggerEntryCookieDeleted" : {
"message": "cookie deleted: {{value}}",
"description": "An entry for when a cookie is deleted"
},
"loggerEntryDeleteCookieError" : {
"message": "failed to delete cookie: {{value}}",
"description": "An entry for when the browser cache is cleared"
},
"loggerEntryBrowserCacheCleared" : {
"message": "browser cache cleared",
"description": "An entry for when a cookie can't be deleted"
},
"loggerEntryAssetUpdated" : {
"message": "asset updated: {{value}}",
"description": "An entry for when an asset was updated"
},
"mainBlockedPrompt1": {
"message": "uMatrix has prevented the following page from loading:",
"description": "English: uMatrix has prevented the following page from loading:"
@ -752,5 +877,9 @@
"genericApplyChanges": {
"message": "Apply changes",
"description": "for generic 'Apply changes' buttons"
},
"genericCopyToClipboard":{
"message":"Copy to clipboard",
"description":"Label for buttons used to copy something to the clipboard"
}
}

7
src/about.html

@ -6,6 +6,10 @@
<link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<style>
div.body > ul {
padding-left: 0;
padding-right: 0;
}
ul {
list-style-type: none;
}
@ -17,6 +21,8 @@ ul {
<h3>uMatrix <span id="aboutVersion"></span></h3>
<ul>
<li>Copyright (c) Raymond Hill 2013-present<br>
<li>&nbsp;
<li><span id="aboutStorageUsed"></span><br>
<li>&nbsp;
<li><span data-i18n="aboutChangelog"></span><br>
@ -55,6 +61,7 @@ ul {
<span data-i18n="aboutResetConfirm"></span>
</div>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>

33
src/asset-viewer.html

@ -4,18 +4,45 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>uMatrix — Asset viewer</title>
<link rel="stylesheet" href="lib/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="lib/codemirror/addon/search/matchesonscrollbar.css">
<link rel="stylesheet" href="css/fa-icons.css">
<link rel="stylesheet" href="css/codemirror.css">
<style>
body {
border: 0;
margin: 0;
padding: 0;
}
#content {
font: 12px monospace;
white-space: pre;
height: 100vh;
width: 100vw;
}
/* https://github.com/uBlockOrigin/uBlock-issues/issues/292 */
.CodeMirror-wrap pre {
word-break: break-all;
}
</style>
</head>
<body>
<div id="content"></div>
<div id="content" class="codeMirrorContainer"></div>
<script src="lib/codemirror/lib/codemirror.js"></script>
<script src="lib/codemirror/addon/display/panel.js"></script>
<script src="lib/codemirror/addon/scroll/annotatescrollbar.js"></script>
<script src="lib/codemirror/addon/search/matchesonscrollbar.js"></script>
<script src="lib/codemirror/addon/search/searchcursor.js"></script>
<script src="lib/codemirror/addon/selection/active-line.js"></script>
<script src="js/codemirror/search.js"></script>
<script src="js/fa-icons.js"></script>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/asset-viewer.js"></script>
</body>
</html>

23
src/background.html

@ -5,33 +5,34 @@
<title>uMatrix</title>
</head>
<body>
<script src="js/console.js"></script>
<script src="lib/lz4/lz4-block-codec-any.js"></script>
<script src="lib/punycode.js"></script>
<script src="lib/publicsuffixlist.js"></script>
<script src="lib/publicsuffixlist/publicsuffixlist.js"></script>
<script src="js/webext.js"></script>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-background.js"></script>
<!-- Forks can pick the chromium, firefox, or their own implementation -->
<script src="js/vapi-webrequest.js"></script>
<!-- Optional -->
<script src="js/vapi-cachestorage.js"></script>
<script src="js/background.js"></script>
<script src="js/traffic.js"></script>
<script src="js/hntrie.js"></script>
<script src="js/utils.js"></script>
<script src="js/uritools.js"></script>
<script src="js/lz4.js"></script>
<script src="js/usersettings.js"></script>
<script src="js/liquid-dict.js"></script>
<script src="js/matrix.js"></script>
<script src="js/recipe-manager.js"></script>
<script src="js/utils.js"></script>
<script src="js/cachestorage.js"></script>
<script src="js/assets.js"></script>
<script src="js/filtering-context.js"></script>
<script src="js/httpsb.js"></script>
<script src="js/uritools.js"></script>
<script src="js/cookies.js"></script>
<script src="js/logger.js"></script>
<script src="js/messaging.js"></script>
<script src="js/storage.js"></script>
<script src="js/pagestats.js"></script>
<script src="js/tab.js"></script>
<script src="js/traffic.js"></script>
<script src="js/browsercache.js"></script>
<script src="js/start.js"></script>
</body>

68
src/css/codemirror.css

@ -1,6 +1,5 @@
.codeMirrorContainer {
font-size: 12px;
line-height: 1.25;
overflow: hidden;
position: relative;
}
@ -18,6 +17,73 @@
height: 100%;
}
.cm-s-default .cm-comment { color: #777; }
.cm-directive { color: #333; font-weight: bold; }
.cm-staticext { color: #008; }
.cm-staticnetBlock { color: #800; }
.cm-staticnetAllow { color: #004f00; }
.cm-staticOpt { background-color: #ddd; font-weight: bold; }
.cm-search-widget {
align-items: center;
background-color: #eee;
cursor: default;
direction: ltr;
display: flex;
flex-shrink: 0;
font-size: 110%;
justify-content: center;
padding: 4px 8px;
/* position: absolute; */
right: 2em;
top: 0;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
z-index: 1000;
}
.cm-search-widget .fa-icon {
fill: #888;
font-size: 140%;
}
.cm-search-widget .fa-icon:not(.fa-icon-ro):hover {
fill: #000;
}
.cm-search-widget-input {
border: 1px solid gray;
border-radius: 3px;
display: inline-flex;
min-width: 16em;
}
.cm-search-widget-input > input {
border: 0;
flex-grow: 1;
}
.cm-search-widget-input > .cm-search-widget-count {
align-items: center;
color: #888;
display: none;
flex-grow: 0;
font-size: 80%;
padding: 0 0.4em;
pointer-events: none;
}
.cm-search-widget[data-query] .cm-search-widget-count {
display: inline-flex;
}
.cm-search-widget .cm-search-widget-button:hover {
color: #000;
}
.cm-search-widget .sourceURL {
padding-left: 0.5em;
padding-right: 0.5em;
position: absolute;
left: 0;
}
.cm-search-widget .sourceURL[href=""] {
display: none;
}
.CodeMirror-merge-l-deleted {
background-image: none;
font-weight: bold;

34
src/css/common.css

@ -12,7 +12,7 @@
}
body {
font-size: 14px;
font: 14px sans-serif;
}
body[dir="ltr"] {
direction: ltr;
@ -111,3 +111,35 @@ button.custom[disabled] {
code {
font-size: 90%;
}
.px-icon {
align-items: center;
background-color: transparent;
border: 0;
display: inline-flex;
filter: grayscale(100%);
justify-content: center;
margin: 0;
padding: 0.1em;
position: relative;
}
.px-icon > * {
pointer-events: none;
}
.px-icon.disabled,
.disabled > .px-icon,
.px-icon[disabled],
[disabled] > .px-icon {
color: #000;
fill: #000;
opacity: 0.25;
stroke: #888;
pointer-events: none;
}
.px-icon > img {
height: 1em;
width: 1em;
}
.px-icon.active {
filter: none;
}

6
src/css/dashboard.css

@ -23,9 +23,13 @@ body {
white-space: nowrap;
background-color: white;
}
#dashboard-nav-widgets span {
#dashboard-nav-widgets > span {
padding: 0 0.5em;
font-size: larger;
vertical-align: bottom;
}
#dashboard-nav-widgets > span > img {
width: 1em;
}
.tabButton {
margin: 0;

778
src/css/logger-ui.css

@ -1,425 +1,621 @@
body {
background-color: white;
border: 0;
box-sizing: border-box;
-moz-box-sizing: border-box;
color: black;
display: flex;
flex-direction: column;
height: 100vh;
margin: 0;
overflow-x: hidden;
overflow: hidden;
padding: 0;
width: 100%;
}
.fa-icon {
cursor: pointer;
font-size: 150%;
padding: 0.4em 0.6em;
width: 100vw;
}
.fa-icon:hover {
background-color: #eee;
textarea {
box-sizing: border-box;
direction: ltr;
resize: none;
width: 100%;
}
#toolbar {
.permatoolbar {
background-color: white;
border: 0;
border-bottom: 1px solid #ccc;
box-sizing: border-box;
-moz-box-sizing: border-box;
left: 0;
display: flex;
flex-shrink: 0;
font-size: 120%;
justify-content: space-between;
margin: 0;
padding: 0.5em 1em;
position: fixed;
top: 0;
width: 100%;
z-index: 10;
padding: 0.25em;
}
#toolbar > div {
.permatoolbar > div {
display: flex;
padding: 0.5em;
white-space: nowrap;
}
.permatoolbar .button {
cursor: pointer;
font-size: 150%;
padding: 0.25em;
}
.permatoolbar .button.active {
fill: #5F9EA0;
}
.permatoolbar .button:hover {
background-color: #eee;
}
#pageSelector {
padding: 0.25em 0;
width: 28em;
margin-right: 0.5em;
padding: 0.2em 0;
}
body[dir="ltr"] #pageSelector {
margin-right: 1em;
}
body[dir="rtl"] #pageSelector {
margin-left: 1em;
}
#showpopup {
display: inline-flex;
align-items: center;
}
#info {
fill: #ccc;
}
#info:hover {
fill: #000;
}
/*
https://github.com/gorhill/uBlock/issues/3293
=> https://devhints.io/css-system-font-stack
*/
#inspectors {
flex-grow: 1;
font-family: "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}
.inspector {
border-top: 1px solid #ccc;
display: flex;
flex-direction: column;
}
.vscrollable {
direction: ltr;
flex-grow: 1;
font-size: small;
overflow-x: hidden;
overflow-y: auto;
}
.inspector:not(.vExpanded) .vCompactToggler.button {
transform: scaleY(-1)
}
.hCompact .hCompactToggler.button {
transform: scaleX(-1)
}
@keyframes popupPanelShow {
from { opacity: 0; }
to { opacity: 1; }
#inspectors.dom #netInspector {
display: none;
}
#netInspector #pause > span:last-of-type {
display: none;
}
#popupPanelContainer {
background: white;
border: 1px solid gray;
#netInspector.paused #pause > span:first-of-type {
display: none;
overflow: hidden;
position: fixed;
right: 0;
z-index: 2000;
}
#netInspector.paused #pause > span:last-of-type {
display: inline-flex;
fill: #5F9EA0;
}
#netInspector #filterExprGroup {
display: flex;
margin: 0 1em;
position: relative;
}
body.popupPanelOn #popupPanelContainer {
animation-duration: 0.25s;
animation-name: popupPanelShow;
display: block;
#netInspector #filterButton {
opacity: 0.25;
}
#popupPanelContainer.hide {
width: 6em !important;
}
#popupPanelContainer > iframe {
#netInspector.f #filterButton {
opacity: 1;
}
#netInspector #filterInput {
border: 1px solid gray;
display: inline-flex;
}
#netInspector #filterInput > input {
border: 0;
padding: 0;
margin: 0;
overflow: hidden;
width: 100%;
min-width: 16em;
}
#netInspector #filterExprButton {
transform: scaleY(-1);
}
#netInspector #filterExprButton:hover {
background-color: transparent;
}
#netInspector #filterExprButton.expanded {
transform: scaleY(1);
}
#popupPanelContainer.hide > iframe {
#netInspector #filterExprPicker {
background-color: white;
border: 1px solid gray;
display: none;
position: absolute;
flex-direction: column;
font-size: small;
top: 100%;
z-index: 100;
}
body[dir="ltr"] #netInspector #filterExprPicker {
right: 0;
}
body[dir="rtl"] #netInspector #filterExprPicker {
left: 0;
}
#popupPanelButton use {
transform: scale(1, 0.4);
#netInspector #filterExprGroup:hover #filterExprButton.expanded ~ #filterExprPicker {
display: flex;
}
body.popupPanelOn #popupPanelButton use {
transform: scale(1, 1);
#netInspector #filterExprPicker > div {
border: 1px dotted #ddd;
border-left: 0;
border-right: 0;
display: flex;
padding: 0.5em;
}
body.compactView #compactViewToggler use {
transform: scale(1, -1);
transform-origin: center;
#netInspector #filterExprPicker > div:first-of-type {
border-top: 0;
}
#filterButton {
opacity: 0.25;
#netInspector #filterExprPicker > div:last-of-type {
border-bottom: 0;
}
body.f #filterButton {
opacity: 1;
#netInspector #filterExprPicker div {
display: flex;
}
#filterInput.bad {
background-color: #fee;
#netInspector #filterExprPicker span[data-filtex] {
align-items: center;
border: 1px solid transparent;
cursor: pointer;
display: inline-flex;
margin: 0 0.5em 0 0;
padding: 0.5em;
white-space: nowrap;
}
#maxEntries {
margin: 0 2em;
#netInspector #filterExprPicker span[data-filtex]:last-of-type {
margin: 0;
}
input:focus {
background-color: #ffe;
#netInspector #filterExprPicker span[data-filtex]:hover {
background-color: aliceblue;
border: 1px solid lightblue;
}
#content {
font-family: "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-size: 13px;
width: 100%;
#netInspector #filterExprPicker span.on[data-filtex] {
background-color: lightblue;
border: 1px solid lightblue;
}
#content table {
border: 0;
border-collapse: collapse;
direction: ltr;
table-layout: fixed;
#netInspector .vscrollable {
overflow: hidden;
}
#vwRenderer {
box-sizing: border-box;
height: 100%;
overflow: hidden;
position: relative;
width: 100%;
}
#content table > colgroup > col:nth-of-type(1) {
width: 4.6em;
#vwRenderer #vwScroller {
height: 100%;
overflow-x: hidden;
overflow-y: auto;
position: absolute;
width: 100%;
}
#content table > colgroup > col:nth-of-type(2) {
width: 25%;
#vwRenderer #vwScroller #vwVirtualContent {
overflow: hidden;
}
#content table > colgroup > col:nth-of-type(3) {
width: 2.2em;
#vwRenderer #vwContent {
left: 0;
overflow: hidden;
position: absolute;
width: 100%;
}
#content table > colgroup > col:nth-of-type(4) {
width: 5.4em;
#vwRenderer .logEntry {
display: block;
left: 0;
overflow: hidden;
position: absolute;
width: 100%;
}
#content table > colgroup > col:nth-of-type(5) {
width: calc(100% - 4.6em - 25% - 2.2em - 5.4em - 1.8em);
#vwRenderer .logEntry:empty {
display: none;
}
#content table > colgroup > col:nth-of-type(6) {
width: 1.8em;
#vwRenderer .logEntry > div {
height: 100%;
white-space: nowrap;
}
#content table tr {
background-color: #fafafa;
#vwRenderer .logEntry > div[data-status="--"] {
background-color: rgba(192, 0, 0, 0.1);
}
body.f table tr.f {
display: none;
body.colorBlind #vwRenderer .logEntry > div[data-status="--"] {
background-color: rgba(0, 19, 110, 0.1);
}
#content table tr:nth-of-type(2n+1) {
background-color: #eee;
#vwRenderer .logEntry > div[data-status="3"] {
background-color: rgba(108, 108, 108, 0.1);
}
#content table tr.cat_info {
color: #00f;
body.colorBlind #vwRenderer .logEntry > div[data-status="3"] {
background-color: rgba(96, 96, 96, 0.1);
}
#vwRenderer .logEntry > div[data-status="++"] {
background-color: rgba(0, 160, 0, 0.1);
}
body.colorBlind #vwRenderer .logEntry > div[data-status="++"] {
background-color: rgba(255, 194, 57, 0.1)
}
#vwRenderer .logEntry > div[data-tabid="-1"] {
text-shadow: 0 0.2em 0.4em #aaa;
}
#content table tr.blocked {
color: #f00;
#vwRenderer .logEntry > div[data-aliasid] {
color: mediumblue;
}
#content table tr.doc {
#vwRenderer .logEntry > div[data-type="tabLoad"] {
background-color: #666;
color: white;
text-align: center;
}
#vwRenderer .logEntry > div[data-type="error"] {
color: #800;
}
#vwRenderer .logEntry > div[data-type="info"] {
color: #008;
}
#vwRenderer .logEntry > div.voided {
opacity: 0.3;
}
#vwRenderer .logEntry > div.voided:hover {
opacity: 0.7;
}
body #content td {
#vwRenderer .logEntry > div > span {
border: 1px solid #ccc;
min-width: 0.5em;
padding: 3px;
vertical-align: top;
white-space: normal;
border-top: 0;
border-right: 0;
box-sizing: border-box;
display: inline-block;
height: 100%;
overflow: hidden;
padding: 0.2em;
text-align: left;
vertical-align: middle;
white-space: nowrap;
word-break: break-all;
word-wrap: break-word;
}
#content table tr td:first-of-type {
border-left: none;
#vwRenderer .logEntry > div.canDetails:hover > span {
background-color: rgba(0,0,0,0.04);
}
#content table tr td:last-of-type {
border-right: none;
body[dir="ltr"] #vwRenderer .logEntry > div > span:first-child {
border-left: 0;
}
body.compactView #content tr:not(.vExpanded) td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
body[dir="rtl"] #vwRenderer .logEntry > div > span:first-child {
border-right: 0;
}
#content table tr td:nth-of-type(1) {
cursor: default;
text-align: right;
white-space: nowrap;
#vwRenderer .logEntry > div > span:nth-of-type(1) {
text-align: center;
}
#content table tr td:nth-of-type(2):not([colspan]) {
direction: rtl;
#vwRenderer .logEntry > div > span:nth-of-type(2) {
text-align: right;
text-overflow: ellipsis;
}
.vExpanded #vwRenderer .logEntry > div > span:nth-of-type(2) {
overflow-y: auto;
white-space: pre-line;
}
#netInspector.vExpanded #vwRenderer .logEntry > div > span:nth-of-type(2) {
text-align: left;
unicode-bidi: plaintext;
}
#content table tr[data-tabid="-1"] td:nth-of-type(2):not([colspan]) {
position: relative;
#vwRenderer .logEntry > div:not(.messageRealm) > span:nth-of-type(2) {
direction: rtl;
}
#vwRenderer .logEntry > div.messageRealm > span:nth-of-type(2) {
color: blue;
text-align: left;
}
#vwRenderer .logEntry > div.messageRealm[data-type="tabLoad"] > span:nth-of-type(2) {
color: white;
text-align: center;
}
#vwRenderer .logEntry > div.messageRealm > span:nth-of-type(2) ~ span {
display: none;
}
#content table tr td:nth-of-type(3) {
#vwRenderer .logEntry > div > span:nth-of-type(3) {
text-align: center;
}
#vwRenderer #vwContent .logEntry > div > span:nth-of-type(4) {
color: #888;
position: relative;
text-overflow: ellipsis;
}
/* visual for tabless network requests */
#content table tr[data-tabid="-1"] td:nth-of-type(3)::before {
border: 5px solid #bbb;
border-bottom: 0;
border-top: 0;
bottom: 0;
content: '\00a0';
left: 0;
#vwRenderer #vwContent .logEntry > div[data-header] > span:nth-of-type(4) {
color: black;
}
.vExpanded #vwRenderer #vwContent .logEntry > div > span:nth-of-type(4) {
overflow-y: auto;
text-overflow: clip;
white-space: pre-line;
}
#vwRenderer .logEntry > div > span:nth-of-type(4) b {
color: black;
font-weight: normal;
}
#vwRenderer .logEntry > div[data-aliasid] > span:nth-of-type(4) b {
color: mediumblue;
}
#vwRenderer .logEntry > div > span:nth-of-type(4) a {
background-color: dimgray;
color: white;
display: none;
height: 100%;
padding: 0 0.25em;
opacity: 0.4;
position: absolute;
right: 0;
text-decoration: none;
top: 0;
width: calc(100% - 10px);
}
#content table tr.tab:not(.canMtx) {
opacity: 0.3;
#netInspector.vExpanded #vwRenderer .logEntry > div > span:nth-of-type(4) a {
bottom: 0px;
height: unset;
padding: 0.25em;
top: unset;
}
#content table tr.tab:not(.canMtx):hover {
opacity: 0.7;
#vwRenderer .logEntry > div > span:nth-of-type(4) a::after {
content: '\2197';
}
#content table tr.cat_net td:nth-of-type(3) {
cursor: pointer;
#vwRenderer .logEntry > div.networkRealm > span:nth-of-type(4):hover a {
align-items: center;
display: inline-flex;
}
#vwRenderer .logEntry > div > span:nth-of-type(4) a:hover {
opacity: 1;
}
#vwRenderer .logEntry > div > span:nth-of-type(5) {
text-align: right;
}
/* visual for tabless network requests */
#vwRenderer .logEntry > div > span:nth-of-type(5) {
}
#vwRenderer .logEntry > div > span:nth-of-type(6) {
font: 12px monospace;
text-align: center;
white-space: nowrap;
}
#content table tr.cat_net td:nth-of-type(5) {
#vwRenderer .logEntry > div.canDetails:hover > span:nth-of-type(6) {
background: rgba(0, 0, 255, 0.1);
cursor: zoom-in;
}
#content table tr.cat_net td:nth-of-type(5) > span > * {
opacity: 0.6;
#vwRenderer #vwBottom {
background-color: #00F;
height: 0;
overflow: hidden;
width: 100%;
}
#content table tr.cat_net td:nth-of-type(5) > span > b:first-of-type {
opacity: 1;
#vwRenderer #vwLineSizer {
left: 0;
pointer-events: none;
position: absolute;
top: 0;
visibility: hidden;
width: 100%;
}
#popupContainer {
background: white;
border: 1px solid gray;
bottom: 0;
box-sizing: content-box;
display: none;
max-height: 75vh;
overflow: hidden;
position: fixed;
right: 0;
z-index: 200;
}
#inspectors.popupOn #popupContainer {
display: block;
}
.modalDialog {
#modalOverlay {
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
height: 100vh;
border: 0;
bottom: 0;
display: none;
justify-content: center;
left: 0;
margin: 0;
position: fixed;
right: 0;
top: 0;
width: 100vw;
z-index: 5000;
z-index: 400;
}
.modalDialog > .dialog {
background-color: white;
font: 15px httpsb,sans-serif;
min-width: fit-content;
padding: 0.5em;
width: 90%;
}
#ruleEditor section {
#modalOverlay.on {
display: flex;
}
.scopeWidget {
line-height: 2.5em;
margin-bottom: 0.5em;
#modalOverlay > div {
position: relative;
}
#specificScope, .ruleCell:nth-of-type(1) {
flex-grow: 1;
#modalOverlay > div > div:nth-of-type(1) {
background-color: white;
border: 0;
box-sizing: border-box;
padding: 1em;
max-height: 90vh;
overflow-y: auto;
width: 90vw;
}
#modalOverlay > div > div:nth-of-type(2) {
stroke: #000;
stroke-width: 3px;
position: absolute;
width: 1.6em;
height: 1.6em;
bottom: calc(100% + 2px);
background-color: white;
}
#globalScope, .ruleCell:nth-of-type(2) {
width: 4em;
body[dir="ltr"] #modalOverlay > div > div:nth-of-type(2) {
right: 0;
}
.ruleEditorToolbar {
display: flex;
flex-direction: column;
justify-content: space-around;
margin-left: 0.5em;
padding: 0.2em;
body[dir="rtl"] #modalOverlay > div > div:nth-of-type(2) {
left: 0;
}
.ruleEditorToolbar .fa-icon {
padding: 0.4em;
#modalOverlay > div > div:nth-of-type(2):hover {
background-color: #eee;
}
.fa-icon.scopeRel {
color: #24c;
fill: #24c;
#modalOverlay > div > div:nth-of-type(2) > * {
pointer-events: none;
}
body[data-scope="*"] .fa-icon.scopeRel {
color: #000;
fill: #000;
#netFilteringDialog {
font-size: 95%;
}
.ruleWidgets {
display: flex;
flex-direction: column;
flex-grow: 1;
#netFilteringDialog a {
text-decoration: none;
}
.ruleRow {
display: flex;
line-height: 2em;
margin-top: 1px;
#netFilteringDialog > .headers {
border-bottom: 1px solid #888;
line-height: 2;
position: relative;
}
.ruleCell {
#netFilteringDialog > .headers > .header {
background-color: #eee;
border: 1px dotted rgba(0,0,0,0.2);
border: 1px solid #aaa;
border-bottom: 1px solid #888;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
color: #888;
cursor: pointer;
display: inline-block;
margin-left: 1px;
padding: 1px;
padding: 0 1em;
position: relative;
}
.ruleCell:hover {
border-style: solid;
}
.ruleCell:nth-of-type(1) {
margin-left: 0;
text-align: right;
}
.ruleCell:nth-of-type(2) {
text-align: center;
top: 1px;
}
.ruleCell[data-tcolor="1"] {
border-color: #debaba;
color: black;
background-color: #f8d0d0;
}
#ruleEditor.colorblind .ruleCell[data-tcolor="1"] {
border-color: rgba(0, 19, 110, 0.3);
#netFilteringDialog[data-pane="details"] > .headers > [data-pane="details"],
#netFilteringDialog[data-pane="rule"] > .headers > [data-pane="rule"] {
background-color: white;
border-color: #888;
border-bottom: 1px solid white;
color: black;
background-color: rgba(0, 19, 110, 0.2);
}
.ruleCell[data-tcolor="2"] {
border-color: #bad6ba;
color: black;
background-color: #d0f0d0;
#netFilteringDialog > div.panes {
height: 50vh;
overflow: hidden;
overflow-y: auto;
padding-top: 1em;
}
#ruleEditor.colorblind .ruleCell[data-tcolor="2"] {
border-color: rgba(255, 194, 57, 0.3);
color: black;
background-color: rgba(255, 194, 57, 0.2);
#netFilteringDialog > div.panes > div {
display: none;
height: 100%;
}
.ruleCell[data-tcolor="129"] {
color: white;
background-color: #c00;
#netFilteringDialog[data-pane="details"] > .panes > [data-pane="details"],
#netFilteringDialog[data-pane="rule"] > .panes > [data-pane="rule"] {
display: flex;
flex-direction: column;
}
#ruleEditor.colorblind .ruleCell[data-tcolor="129"] {
color: white;
background-color: rgb(0, 19, 110);
#netFilteringDialog > .panes > [data-pane="details"] > div {
align-items: stretch;
background-color: #e6e6e6;
border: 0;
border-bottom: 1px solid white;
display: flex;
}
.ruleCell[data-tcolor="130"] {
color: white;
background-color: #080;
#netFilteringDialog > .panes > [data-pane="details"] > div > span {
padding: 0.5em;
}
#ruleEditor.colorblind .ruleCell[data-tcolor="130"] {
border-color: rgb(255, 194, 57);
color: black;
background-color: rgb(255, 194, 57);
#netFilteringDialog > .panes > [data-pane="details"] > div > span:nth-of-type(1) {
border: 0;
flex-grow: 0;
flex-shrink: 0;
text-align: right;
width: 8em;
}
.ruleCell[data-pcolor="129"] {
background-image: url('../img/permanent-black-small.png');
background-repeat: no-repeat;
background-position: -1px -1px;
body[dir="ltr"] #netFilteringDialog > .panes > [data-pane="details"] > div > span:nth-of-type(1) {
border-right: 1px solid white;
}
#ruleEditor.colorblind .ruleCell[data-pcolor="129"] {
background-image: url('../img/permanent-black-small-cb.png');
body[dir="rtl"] #netFilteringDialog > .panes > [data-pane="details"] > div > span:nth-of-type(1) {
border-left: 1px solid white;
}
.ruleCell[data-pcolor="130"] {
background-image: url('../img/permanent-white-small.png');
background-repeat: no-repeat;
background-position: -1px -1px;
#netFilteringDialog > .panes > [data-pane="details"] > div > span:nth-of-type(2) {
flex-grow: 1;
max-height: 20vh;
overflow: hidden auto;
white-space: pre-line
}
#ruleEditor.colorblind .ruleCell[data-pcolor="130"] {
background-image: url('../img/permanent-white-small-cb.png');
#netFilteringDialog > .panes > [data-pane="details"] > div > span:nth-of-type(2):not(.prose) {
word-break: break-all;
}
#ruleActionPicker {
#netFilteringDialog > .panes > [data-pane="rule"] iframe {
border: 0;
height: 100%;
left: 0;
margin: 0;
padding: 0;
position: absolute;
top: 0;
width: 100%;
z-index: 10;
}
.allowRule, .blockRule {
#loggerExportDialog {
display: flex;
flex-direction: column;
}
#loggerExportDialog .options {
display: flex;
justify-content: space-between;
margin-bottom: 1em;
}
#loggerExportDialog .options > div {
display: inline-flex;
}
#loggerExportDialog .options span[data-i18n] {
border: 1px solid lightblue;
cursor: pointer;
font-size: 90%;
margin: 0;
border: 0;
padding: 0;
position: absolute;
left: 0;
width: 100%;
height: 50%;
background: transparent;
padding: 0.5em;
white-space: nowrap;
}
.allowRule {
top: 0;
#loggerExportDialog .options span[data-i18n]:hover {
background-color: aliceblue;
}
.blockRule {
top: 50%;
#loggerExportDialog .options span.on[data-i18n],
#loggerExportDialog .options span.pushbutton:active {
background-color: lightblue;
}
.ruleCell[data-tcolor="1"] .allowRule:hover {
background-color: #080;
opacity: 0.25;
#loggerExportDialog .output {
font: smaller mono;
height: 60vh;
padding: 0.5em;
white-space: pre;
}
.ruleCell[data-tcolor="1"] .blockRule:hover {
background-color: #c00;
opacity: 0.25;
#loggerSettingsDialog {
display: flex;
flex-direction: column;
}
.ruleCell[data-tcolor="2"] .allowRule:hover {
background-color: #080;
opacity: 0.25;
#loggerSettingsDialog > div {
padding-bottom: 1em;
}
.ruleCell[data-tcolor="2"] .blockRule:hover {
background-color: #c00;
opacity: 0.25;
#loggerSettingsDialog > div:last-of-type {
padding-bottom: 0;
}
.ruleCell[data-tcolor="129"] .allowRule:hover {
background-color: transparent;
#loggerSettingsDialog ul {
padding: 0;
}
.ruleCell[data-tcolor="129"] .blockRule:hover {
background-color: transparent;
body[dir="ltr"] #loggerSettingsDialog ul {
padding-left: 2em;
}
.ruleCell[data-pcolor="130"] .allowRule:hover {
background-color: transparent;
body[dir="rtl"] #loggerSettingsDialog ul {
padding-right: 2em;
}
.ruleCell[data-pcolor="130"] .blockRule:hover {
background-color: transparent;
#loggerSettingsDialog li {
list-style-type: none;
margin: 0.5em 0 0 0;
}
#ruleEditor.colorblind .ruleCell[data-tcolor="1"] .allowRule:hover,
#ruleEditor.colorblind .ruleCell[data-tcolor="2"] .allowRule:hover {
background-color: rgb(255, 194, 57);
opacity: 0.6;
#loggerSettingsDialog input {
max-width: 6em;
}
#ruleEditor.colorblind .ruleCell[data-tcolor="1"] .blockRule:hover,
#ruleEditor.colorblind .ruleCell[data-tcolor="2"] .blockRule:hover {
background-color: rgb(0, 19, 110);
opacity: 0.4;
.hide {
display: none !important;
}

62
src/css/popup.css

@ -54,9 +54,7 @@ a {
border: 0;
color: #bbb;
cursor: pointer;
display: block;
font-size: 12px;
line-height: 12px;
font: 12px/1 sans-serif;
margin: 0;
padding: 3px 0;
position: relative;
@ -65,17 +63,11 @@ a {
.paneHead {
background-color: white;
left: 0;
padding: 0;
position: fixed;
right: 0;
position: sticky;
top: 0;
z-index: 100;
}
.paneContent {
padding-top: 5.5em;
}
.paneHead > a:first-child {
background-color: #444;
@ -152,6 +144,9 @@ body .toolbar button.disabled {
opacity: 1;
stroke: none;
}
#mtxSwitches > li > svg > * {
fill: #bbb;
}
#mtxSwitches > li.relevant > svg .dot {
fill: #aaa;
}
@ -181,6 +176,13 @@ body .toolbar button.disabled {
#mtxSwitches > li > a:hover {
opacity: 0.8;
}
#mtxSwitches > li.unsupported {
cursor: default;
}
#mtxSwitches > li.unsupported > svg .on,
#mtxSwitches > li.unsupported > svg .off {
display: none;
}
.dropdown-menu-capture {
background-color: rgba(0,0,0,0.2);
@ -336,19 +338,23 @@ body[data-scope="*"] .toolbar .scopeRel.disabled {
color: #ccc;
}
body.embedded [data-extension-url],
body.tabless .needtab {
display: none;
}
.matrix {
text-align: left;
}
.matRow {
display: flex;
white-space: nowrap;
}
.matCell {
margin: 1px 1px 0 0;
border: 1px dotted rgba(0,0,0,0.2);
padding: 6px 1px 3px 1px;
padding: 6px 2px;
display: inline-block;
box-sizing: content-box;
-moz-box-sizing: content-box;
width: 2.6em;
white-space: nowrap;
text-align: center;
@ -357,9 +363,8 @@ body[data-scope="*"] .toolbar .scopeRel.disabled {
}
#matHead {
border-top: 1px dotted #ccc;
padding-bottom: 1px;
padding-top: 1px;
margin: 1px 0 0 0;
}
.paneHead .matCell:nth-child(2) {
letter-spacing: -0.3px;
@ -383,17 +388,14 @@ body[data-scope="*"] .toolbar .scopeRel.disabled {
direction: inherit;
}
.matrix .matRow.l2 > .matCell:first-child {
margin-left: 1px;
width: calc(16em - 1px);
}
.matrix .matRow > .matCell:hover {
border-style: solid;
}
.matrix .matGroup .matSection {
margin: 2px 0 0 0;
border: 0;
padding: 0;
/* background-color: rgba(0,0,0,0.05); */
}
.matrix .matGroup .matSection:hover {
}
.matrix .matGroup.g0 .matSection:first-child {
margin-top: 0;
@ -406,7 +408,7 @@ body[data-scope="*"] .toolbar .scopeRel.disabled {
display: none;
}
.matrix .matSection.collapsible.collapsed .matRow.meta {
display: block;
display: flex;
}
.matrix .matSection.collapsible.collapsed .matRow.l1:not(.meta) {
display: none;
@ -416,6 +418,9 @@ body[data-scope="*"] .toolbar .scopeRel.disabled {
}
/* Collapsing of blacklisted */
.matrix .matGroup.g4 {
margin-bottom: 1px;
}
.matrix .g4Meta {
margin: 0;
padding: 0;
@ -443,7 +448,7 @@ body.powerOff .matrix .g4Meta.g4Collapsed ~ .matSection {
display: none;
}
.matrix .g4Meta.g4Collapsed ~ .matRow.ro {
display: block;
display: flex;
}
body.powerOff .matrix .g4Meta.g4Collapsed ~ .matRow.ro {
display: none;
@ -463,12 +468,10 @@ body.powerOff .matrix .g4Meta.g4Collapsed ~ .matRow.ro {
background-color: #080;
}
.t1 {
border-color: #debaba;
color: black;
background-color: #f8d0d0;
}
.t2 {
border-color: #bad6ba;
color: black;
background-color: #d0f0d0;
}
@ -489,17 +492,14 @@ body.colorblind .t81 {
background-color: rgb(0, 19, 110);
}
body.colorblind .t82 {
border-color: rgb(255, 194, 57);
color: black;
background-color: rgb(255, 194, 57);
}
body.colorblind .t1 {
border-color: rgba(0, 19, 110, 0.3);
color: black;
background-color: rgba(0, 19, 110, 0.2);
}
body.colorblind .t2 {
border-color: rgba(255, 194, 57, 0.3);
color: black;
background-color: rgba(255, 194, 57, 0.2);
}
@ -612,7 +612,7 @@ body.colorblind .rw .matCell.t2 #blacklist:hover {
}
.matSection.collapsible .matRow.l1 .matCell:nth-of-type(1):hover #domainOnly,
#matHead.collapsible .matRow .matCell:nth-of-type(1):hover #domainOnly {
display: inline-flex;
display: inline-block;
}
#domainOnly:hover {
opacity: 1;
@ -638,12 +638,6 @@ body.noTabFound #noTabFound {
body.hConstrained {
overflow-x: auto;
}
body.hConstrained .paneHead {
left: auto;
position: absolute;
right: auto;
width: 100%;
}
body[data-touch="true"] .matCell {
line-height: 200%;
}

22
src/css/raw-settings.css

@ -1,20 +1,16 @@
div.body {
box-sizing: border-box;
display: flex;
flex-direction: column;
html {
height: 100vh;
justify-content: space-between;
overflow: hidden;
}
p {
margin: 0.5em 0;
body {
overflow: hidden;
}
textarea {
box-sizing: border-box;
flex-grow: 1;
resize: none;
#rawSettings {
border-top: 1px solid #ddd;
height: 75vh;
text-align: left;
white-space: pre;
width: 100%;
word-wrap: normal;
}
.CodeMirror-wrap pre {
word-break: break-all;
}

9
src/dashboard.html

@ -5,15 +5,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" type="image/png" href="img/icon_16.png">
<title data-i18n="dashboardPageName"></title>
<link href='css/dashboard.css' rel='stylesheet' type='text/css'>
<link href='css/common.css' rel='stylesheet' type='text/css'>
<link href="css/dashboard.css" rel="stylesheet" type="text/css">
<link href="css/common.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="dashboard-nav">
<div id="dashboard-nav-widgets">
<span>uMatrix</span>
<a class="tabButton" id="settings" href="#settings" data-dashboard-panel-url="settings.html" data-i18n="settingsPageName"></a>
<span data-i18n-title="extName"><img src="img/icon_64.png"></span><!--
--><a class="tabButton" id="settings" href="#settings" data-dashboard-panel-url="settings.html" data-i18n="settingsPageName"></a>
<a class="tabButton" id="user-rules" href="#user-rules" data-dashboard-panel-url="user-rules.html" data-i18n="userRulesPageName"></a>
<a class="tabButton" id="hosts-files" href="#hosts-files" data-dashboard-panel-url="hosts-files.html" data-i18n="ubiquitousRulesPageName"></a>
<a class="tabButton" id="raw-settings" href="#raw-settings" data-dashboard-panel-url="raw-settings.html" data-i18n="rawSettingsPageName"></a>
@ -23,6 +23,7 @@
<iframe src=""></iframe>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>

2
src/hosts-files.html

@ -66,8 +66,10 @@
</div><!-- end of div.body -->
<script src="js/fa-icons.js"></script>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dashboard-common.js"></script>

3
src/img/fontawesome/fontawesome-defs.svg

@ -28,6 +28,7 @@ License - https://github.com/FortAwesome/Font-Awesome/tree/a8386aae19e200ddb0f68
<defs>
<symbol id="angle-up" viewBox="0 0 998 582"><path d="m 998,499 q 0,13 -10,23 l -50,50 q -10,10 -23,10 -13,0 -23,-10 L 499,179 106,572 Q 96,582 83,582 70,582 60,572 L 10,522 Q 0,512 0,499 0,486 10,476 L 476,10 q 10,-10 23,-10 13,0 23,10 l 466,466 q 10,10 10,23 z"/></symbol>
<symbol id="arrow-left" viewBox="0 0 1472 1558"><path d="m 1472,715 0,128 q 0,53 -32.5,90.5 Q 1407,971 1355,971 l -704,0 293,294 q 38,36 38,90 0,54 -38,90 l -75,76 q -37,37 -90,37 -52,0 -91,-37 L 37,869 Q 0,832 0,779 0,727 37,688 L 688,38 q 38,-38 91,-38 52,0 90,38 l 75,74 q 38,38 38,91 0,53 -38,91 l -293,293 704,0 q 52,0 84.5,37.5 32.5,37.5 32.5,90.5 z"/></symbol>
<symbol id="clipboard" viewBox="0 0 1792 1792"><path d="m 768,1664 896,0 0,-640 -416,0 q -40,0 -68,-28 -28,-28 -28,-68 l 0,-416 -384,0 0,1152 z m 256,-1440 0,-64 q 0,-13 -9.5,-22.5 Q 1005,128 992,128 l -704,0 q -13,0 -22.5,9.5 Q 256,147 256,160 l 0,64 q 0,13 9.5,22.5 9.5,9.5 22.5,9.5 l 704,0 q 13,0 22.5,-9.5 9.5,-9.5 9.5,-22.5 z m 256,672 299,0 -299,-299 0,299 z m 512,128 0,672 q 0,40 -28,68 -28,28 -68,28 l -960,0 q -40,0 -68,-28 -28,-28 -28,-68 l 0,-160 -544,0 Q 56,1536 28,1508 0,1480 0,1440 L 0,96 Q 0,56 28,28 56,0 96,0 l 1088,0 q 40,0 68,28 28,28 28,68 l 0,328 q 21,13 36,28 l 408,408 q 28,28 48,76 20,48 20,88 z"/></symbol>
<symbol id="clock" viewBox="0 0 1536 1536"><path d="m 896,416 0,448 q 0,14 -9,23 -9,9 -23,9 l -320,0 q -14,0 -23,-9 -9,-9 -9,-23 l 0,-64 q 0,-14 9,-23 9,-9 23,-9 l 224,0 0,-352 q 0,-14 9,-23 9,-9 23,-9 l 64,0 q 14,0 23,9 9,9 9,23 z m 416,352 q 0,-148 -73,-273 -73,-125 -198,-198 -125,-73 -273,-73 -148,0 -273,73 -125,73 -198,198 -73,125 -73,273 0,148 73,273 73,125 198,198 125,73 273,73 148,0 273,-73 125,-73 198,-198 73,-125 73,-273 z m 224,0 q 0,209 -103,385.5 Q 1330,1330 1153.5,1433 977,1536 768,1536 559,1536 382.5,1433 206,1330 103,1153.5 0,977 0,768 0,559 103,382.5 206,206 382.5,103 559,0 768,0 977,0 1153.5,103 1330,206 1433,382.5 1536,559 1536,768 Z"/></symbol>
<symbol id="cloud-download" viewBox="0 0 1920 1408"><path d="m 1280,800 q 0,-14 -9,-23 -9,-9 -23,-9 l -224,0 0,-352 q 0,-13 -9.5,-22.5 Q 1005,384 992,384 l -192,0 q -13,0 -22.5,9.5 Q 768,403 768,416 l 0,352 -224,0 q -13,0 -22.5,9.5 -9.5,9.5 -9.5,22.5 0,14 9,23 l 352,352 q 9,9 23,9 14,0 23,-9 l 351,-351 q 10,-12 10,-24 z m 640,224 q 0,159 -112.5,271.5 Q 1695,1408 1536,1408 l -1088,0 Q 263,1408 131.5,1276.5 0,1145 0,960 0,830 70,720 140,610 258,555 256,525 256,512 256,300 406,150 556,0 768,0 q 156,0 285.5,87 129.5,87 188.5,231 71,-62 166,-62 106,0 181,75 75,75 75,181 0,76 -41,138 130,31 213.5,135.5 Q 1920,890 1920,1024 Z"/></symbol>
<symbol id="cloud-upload" viewBox="0 0 1920 1408"><path d="m 1280,736 q 0,-14 -9,-23 L 919,361 q -9,-9 -23,-9 -14,0 -23,9 L 522,712 q -10,12 -10,24 0,14 9,23 9,9 23,9 l 224,0 0,352 q 0,13 9.5,22.5 9.5,9.5 22.5,9.5 l 192,0 q 13,0 22.5,-9.5 9.5,-9.5 9.5,-22.5 l 0,-352 224,0 q 13,0 22.5,-9.5 9.5,-9.5 9.5,-22.5 z m 640,288 q 0,159 -112.5,271.5 Q 1695,1408 1536,1408 l -1088,0 Q 263,1408 131.5,1276.5 0,1145 0,960 0,830 70,720 140,610 258,555 256,525 256,512 256,300 406,150 556,0 768,0 q 156,0 285.5,87 129.5,87 188.5,231 71,-62 166,-62 106,0 181,75 75,75 75,181 0,76 -41,138 130,31 213.5,135.5 Q 1920,890 1920,1024 Z"/></symbol>
@ -42,6 +43,8 @@ License - https://github.com/FortAwesome/Font-Awesome/tree/a8386aae19e200ddb0f68
<symbol id="info-circle" viewBox="0 0 1536 1536"><path d="m 1024,1248 0,-160 q 0,-14 -9,-23 -9,-9 -23,-9 l -96,0 0,-512 q 0,-14 -9,-23 -9,-9 -23,-9 l -320,0 q -14,0 -23,9 -9,9 -9,23 l 0,160 q 0,14 9,23 9,9 23,9 l 96,0 0,320 -96,0 q -14,0 -23,9 -9,9 -9,23 l 0,160 q 0,14 9,23 9,9 23,9 l 448,0 q 14,0 23,-9 9,-9 9,-23 z M 896,352 896,192 q 0,-14 -9,-23 -9,-9 -23,-9 l -192,0 q -14,0 -23,9 -9,9 -9,23 l 0,160 q 0,14 9,23 9,9 23,9 l 192,0 q 14,0 23,-9 9,-9 9,-23 z m 640,416 q 0,209 -103,385.5 Q 1330,1330 1153.5,1433 977,1536 768,1536 559,1536 382.5,1433 206,1330 103,1153.5 0,977 0,768 0,559 103,382.5 206,206 382.5,103 559,0 768,0 977,0 1153.5,103 1330,206 1433,382.5 1536,559 1536,768 Z"/></symbol>
<symbol id="list-alt" viewBox="0 0 1792 1408"><path d="m 384,1056 0,64 q 0,13 -9.5,22.5 -9.5,9.5 -22.5,9.5 l -64,0 q -13,0 -22.5,-9.5 Q 256,1133 256,1120 l 0,-64 q 0,-13 9.5,-22.5 9.5,-9.5 22.5,-9.5 l 64,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,-256 0,64 q 0,13 -9.5,22.5 Q 365,896 352,896 l -64,0 q -13,0 -22.5,-9.5 Q 256,877 256,864 l 0,-64 q 0,-13 9.5,-22.5 Q 275,768 288,768 l 64,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,-256 0,64 q 0,13 -9.5,22.5 Q 365,640 352,640 l -64,0 q -13,0 -22.5,-9.5 Q 256,621 256,608 l 0,-64 q 0,-13 9.5,-22.5 Q 275,512 288,512 l 64,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 1152,512 0,64 q 0,13 -9.5,22.5 -9.5,9.5 -22.5,9.5 l -960,0 q -13,0 -22.5,-9.5 Q 512,1133 512,1120 l 0,-64 q 0,-13 9.5,-22.5 9.5,-9.5 22.5,-9.5 l 960,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,-256 0,64 q 0,13 -9.5,22.5 -9.5,9.5 -22.5,9.5 l -960,0 q -13,0 -22.5,-9.5 Q 512,877 512,864 l 0,-64 q 0,-13 9.5,-22.5 Q 531,768 544,768 l 960,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,-256 0,64 q 0,13 -9.5,22.5 -9.5,9.5 -22.5,9.5 l -960,0 q -13,0 -22.5,-9.5 Q 512,621 512,608 l 0,-64 q 0,-13 9.5,-22.5 Q 531,512 544,512 l 960,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 128,704 0,-832 q 0,-13 -9.5,-22.5 Q 1645,384 1632,384 l -1472,0 q -13,0 -22.5,9.5 Q 128,403 128,416 l 0,832 q 0,13 9.5,22.5 9.5,9.5 22.5,9.5 l 1472,0 q 13,0 22.5,-9.5 9.5,-9.5 9.5,-22.5 z m 128,-1088 0,1088 q 0,66 -47,113 -47,47 -113,47 l -1472,0 Q 94,1408 47,1361 0,1314 0,1248 L 0,160 Q 0,94 47,47 94,0 160,0 l 1472,0 q 66,0 113,47 47,47 47,113 z"/></symbol>
<symbol id="lock" viewBox="0 0 1152 1408"><path d="m 320,640 512,0 0,-192 q 0,-106 -75,-181 -75,-75 -181,-75 -106,0 -181,75 -75,75 -75,181 l 0,192 z m 832,96 0,576 q 0,40 -28,68 -28,28 -68,28 l -960,0 Q 56,1408 28,1380 0,1352 0,1312 L 0,736 q 0,-40 28,-68 28,-28 68,-28 l 32,0 0,-192 Q 128,264 260,132 392,0 576,0 q 184,0 316,132 132,132 132,316 l 0,192 32,0 q 40,0 68,28 28,28 28,68 z"/></symbol>
<symbol id="pause-circle-o" viewBox="0 0 1536 1536"><path d="M 768,0 Q 977,0 1153.5,103 1330,206 1433,382.5 1536,559 1536,768 1536,977 1433,1153.5 1330,1330 1153.5,1433 977,1536 768,1536 559,1536 382.5,1433 206,1330 103,1153.5 0,977 0,768 0,559 103,382.5 206,206 382.5,103 559,0 768,0 Z m 0,1312 q 148,0 273,-73 125,-73 198,-198 73,-125 73,-273 0,-148 -73,-273 -73,-125 -198,-198 -125,-73 -273,-73 -148,0 -273,73 -125,73 -198,198 -73,125 -73,273 0,148 73,273 73,125 198,198 125,73 273,73 z m 96,-224 q -14,0 -23,-9 -9,-9 -9,-23 l 0,-576 q 0,-14 9,-23 9,-9 23,-9 l 192,0 q 14,0 23,9 9,9 9,23 l 0,576 q 0,14 -9,23 -9,9 -23,9 l -192,0 z m -384,0 q -14,0 -23,-9 -9,-9 -9,-23 l 0,-576 q 0,-14 9,-23 9,-9 23,-9 l 192,0 q 14,0 23,9 9,9 9,23 l 0,576 q 0,14 -9,23 -9,9 -23,9 l -192,0 z"/></symbol>
<symbol id="play-circle-o" viewBox="0 0 1536 1536"><path d="m 1184,768 q 0,37 -32,55 l -544,320 q -15,9 -32,9 -16,0 -32,-8 -32,-19 -32,-56 l 0,-640 q 0,-37 32,-56 33,-18 64,1 l 544,320 q 32,18 32,55 z m 128,0 q 0,-148 -73,-273 -73,-125 -198,-198 -125,-73 -273,-73 -148,0 -273,73 -125,73 -198,198 -73,125 -73,273 0,148 73,273 73,125 198,198 125,73 273,73 148,0 273,-73 125,-73 198,-198 73,-125 73,-273 z m 224,0 q 0,209 -103,385.5 Q 1330,1330 1153.5,1433 977,1536 768,1536 559,1536 382.5,1433 206,1330 103,1153.5 0,977 0,768 0,559 103,382.5 206,206 382.5,103 559,0 768,0 977,0 1153.5,103 1330,206 1433,382.5 1536,559 1536,768 Z"/></symbol>
<symbol id="plus" viewBox="0 0 1408 1408"><path d="m 1408,608 0,192 q 0,40 -28,68 -28,28 -68,28 l -416,0 0,416 q 0,40 -28,68 -28,28 -68,28 l -192,0 q -40,0 -68,-28 -28,-28 -28,-68 l 0,-416 -416,0 Q 56,896 28,868 0,840 0,800 L 0,608 q 0,-40 28,-68 28,-28 68,-28 l 416,0 0,-416 Q 512,56 540,28 568,0 608,0 l 192,0 q 40,0 68,28 28,28 28,68 l 0,416 416,0 q 40,0 68,28 28,28 28,68 z"/></symbol>
<symbol id="power-off" viewBox="0 0 1536 1664"><path d="m 1536,896 q 0,156 -61,298 -61,142 -164,245 -103,103 -245,164 -142,61 -298,61 -156,0 -298,-61 Q 328,1542 225,1439 122,1336 61,1194 0,1052 0,896 0,714 80.5,553 161,392 307,283 q 43,-32 95.5,-25 52.5,7 83.5,50 32,42 24.5,94.5 Q 503,455 461,487 363,561 309.5,668 256,775 256,896 q 0,104 40.5,198.5 40.5,94.5 109.5,163.5 69,69 163.5,109.5 94.5,40.5 198.5,40.5 104,0 198.5,-40.5 Q 1061,1327 1130,1258 1199,1189 1239.5,1094.5 1280,1000 1280,896 1280,775 1226.5,668 1173,561 1075,487 1033,455 1025.5,402.5 1018,350 1050,308 q 31,-43 84,-50 53,-7 95,25 146,109 226.5,270 80.5,161 80.5,343 z m -640,-768 0,640 q 0,52 -38,90 -38,38 -90,38 -52,0 -90,-38 -38,-38 -38,-90 l 0,-640 q 0,-52 38,-90 38,-38 90,-38 52,0 90,38 38,38 38,90 z"/></symbol>
<symbol id="puzzle-piece" viewBox="0 0 1664 1572"><path d="m 1664,1098 q 0,81 -44.5,135 -44.5,54 -123.5,54 -41,0 -77.5,-17.5 -36.5,-17.5 -59,-38 -22.5,-20.5 -56.5,-38 -34,-17.5 -71,-17.5 -110,0 -110,124 0,39 16,115 16,76 15,115 l 0,5 q -22,0 -33,1 -34,3 -97.5,11.5 -63.5,8.5 -115.5,13.5 -52,5 -98,5 -61,0 -103,-26.5 -42,-26.5 -42,-83.5 0,-37 17.5,-71 17.5,-34 38,-56.5 20.5,-22.5 38,-59 17.5,-36.5 17.5,-77.5 0,-79 -54,-123.5 -54,-44.5 -135,-44.5 -84,0 -143,45.5 -59,45.5 -59,127.5 0,43 15,83 15,40 33.5,64.5 18.5,24.5 33.5,53 15,28.5 15,50.5 0,45 -46,89 -37,35 -117,35 -95,0 -245,-24 -9,-2 -27.5,-4 -18.5,-2 -27.5,-4 l -13,-2 q -1,0 -3,-1 -2,0 -2,-1 L 0,512 q 2,1 17.5,3.5 15.5,2.5 34,5 18.5,2.5 21.5,3.5 150,24 245,24 80,0 117,-35 46,-44 46,-89 0,-22 -15,-50.5 Q 451,345 432.5,320.5 414,296 399,256 384,216 384,173 384,91 443,45.5 502,0 587,0 667,0 721,44.5 775,89 775,168 q 0,41 -17.5,77.5 -17.5,36.5 -38,59 -20.5,22.5 -38,56.5 -17.5,34 -17.5,71 0,57 42,83.5 42,26.5 103,26.5 64,0 180,-15 116,-15 163,-17 l 0,2 q -1,2 -3.5,17.5 -2.5,15.5 -5,34 -2.5,18.5 -3.5,21.5 -24,150 -24,245 0,80 35,117 44,46 89,46 22,0 50.5,-15 28.5,-15 53,-33.5 24.5,-18.5 64.5,-33.5 40,-15 83,-15 82,0 127.5,59 45.5,59 45.5,143 z"/></symbol>

124
src/js/about.js

@ -1,7 +1,7 @@
/*******************************************************************************
uMatrix - a Chromium browser extension to black/white list requests.
Copyright (C) 2014-2018 Raymond Hill
Copyright (C) 2014-present 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
@ -25,83 +25,82 @@
/******************************************************************************/
uDom.onLoad(function() {
{
// >>>>> start of local scope
/******************************************************************************/
var backupUserDataToFile = function() {
var userDataReady = function(userData) {
const backupUserDataToFile = function() {
vAPI.messaging.send('dashboard', {
what: 'getAllUserData',
}).then(userData => {
vAPI.download({
'url': 'data:text/plain,' + encodeURIComponent(JSON.stringify(userData, null, 2)),
'filename': uDom('[data-i18n="aboutBackupFilename"]').text()
url: 'data:text/plain,' + encodeURIComponent(
JSON.stringify(userData, null, 2)
),
filename:
uDom.nodeFromSelector('[data-i18n="aboutBackupFilename"]')
.textContent
});
};
vAPI.messaging.send('about.js', { what: 'getAllUserData' }, userDataReady);
});
};
/******************************************************************************/
function restoreUserDataFromFile() {
var validateBackup = function(s) {
var userData = null;
const restoreUserDataFromFile = function() {
const validateBackup = function(s) {
let userData;
try {
userData = JSON.parse(s);
}
catch (e) {
userData = null;
}
if ( userData === null ) {
return null;
catch (ex) {
}
if ( userData === undefined ) { return; }
if (
typeof userData !== 'object' ||
typeof userData.app !== 'string' ||
typeof userData.version !== 'string' ||
typeof userData.when !== 'number' ||
typeof userData.settings !== 'object' ||
(typeof userData.rules !== 'string' &&
Array.isArray(userData.rules) === false)
typeof userData.rules !== 'string' &&
Array.isArray(userData.rules) === false
) {
return null;
return;
}
return userData;
};
var fileReaderOnLoadHandler = function() {
var userData = validateBackup(this.result);
if ( !userData ) {
const fileReaderOnLoadHandler = function() {
const userData = validateBackup(this.result);
if ( userData instanceof Object === false ) {
window.alert(uDom('[data-i18n="aboutRestoreError"]').text());
return;
}
var time = new Date(userData.when);
var msg = uDom('[data-i18n="aboutRestoreConfirm"]').text()
.replace('{{time}}', time.toLocaleString());
var proceed = window.confirm(msg);
const time = new Date(userData.when);
const msg = uDom.nodeFromSelector('[data-i18n="aboutRestoreConfirm"]')
.textContent
.replace('{{time}}', time.toLocaleString());
const proceed = window.confirm(msg);
if ( proceed ) {
vAPI.messaging.send(
'about.js',
{ what: 'restoreAllUserData', userData: userData }
);
vAPI.messaging.send('dashboard', {
what: 'restoreAllUserData',
userData
});
}
};
var file = this.files[0];
if ( file === undefined || file.name === '' ) {
return;
}
if ( file.type.indexOf('text') !== 0 ) {
return;
}
var fr = new FileReader();
const file = this.files[0];
if ( file === undefined || file.name === '' ) { return; }
if ( file.type.indexOf('text') !== 0 ) { return; }
const fr = new FileReader();
fr.onload = fileReaderOnLoadHandler;
fr.readAsText(file);
}
};
/******************************************************************************/
var startRestoreFilePicker = function() {
var input = document.getElementById('restoreFilePicker');
const startRestoreFilePicker = function() {
const input = document.getElementById('restoreFilePicker');
// Reset to empty string, this will ensure an change event is properly
// triggered if the user pick a file, even if it is the same as the last
// one picked.
@ -111,28 +110,30 @@ var startRestoreFilePicker = function() {
/******************************************************************************/
var resetUserData = function() {
var proceed = window.confirm(uDom('[data-i18n="aboutResetConfirm"]').text());
if ( proceed ) {
vAPI.messaging.send('about.js', { what: 'resetAllUserData' });
}
const resetUserData = function() {
const msg = uDom.nodeFromSelector('[data-i18n="aboutResetConfirm"]')
.textContent;
const proceed = window.confirm(msg);
if ( proceed !== true ) { return; }
vAPI.messaging.send('dashboard', {
what: 'resetAllUserData',
});
};
/******************************************************************************/
(function() {
var renderStats = function(details) {
document.getElementById('aboutVersion').textContent = details.version;
var template = uDom('[data-i18n="aboutStorageUsed"]').text();
var storageUsed = '?';
if ( typeof details.storageUsed === 'number' ) {
storageUsed = details.storageUsed.toLocaleString();
}
document.getElementById('aboutStorageUsed').textContent =
template.replace('{{storageUsed}}', storageUsed);
};
vAPI.messaging.send('about.js', { what: 'getSomeStats' }, renderStats);
})();
vAPI.messaging.send('dashboard', {
what: 'getSomeStats',
}).then(details => {
document.getElementById('aboutVersion').textContent = details.version;
const template = uDom('[data-i18n="aboutStorageUsed"]').text();
let storageUsed = '?';
if ( typeof details.storageUsed === 'number' ) {
storageUsed = details.storageUsed.toLocaleString();
}
document.getElementById('aboutStorageUsed').textContent =
template.replace('{{storageUsed}}', storageUsed);
});
/******************************************************************************/
@ -143,4 +144,5 @@ uDom('#resetUserDataButton').on('click', resetUserData);
/******************************************************************************/
});
// <<<<< end of local scope
}

51
src/js/asset-viewer.js

@ -1,7 +1,7 @@
/*******************************************************************************
uMatrix - a Chromium browser extension to block requests.
Copyright (C) 2014-2017 Raymond Hill
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-present 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
@ -16,30 +16,41 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uMatrix
Home: https://github.com/gorhill/uBlock
*/
/* global CodeMirror, uBlockDashboard */
'use strict';
/******************************************************************************/
(function() {
var onAssetContentReceived = function(details) {
document.getElementById('content').textContent =
details && (details.content || '');
};
var q = window.location.search;
var matches = q.match(/^\?url=([^&]+)/);
if ( !matches || matches.length !== 2 ) {
return;
}
vAPI.messaging.send(
'asset-viewer.js',
{ what : 'getAssetContent', url: matches[1] },
onAssetContentReceived
(async ( ) => {
const params = new URL(document.location).searchParams;
const assetKey = params.get('url');
if ( assetKey === null ) { return; }
const cmEditor = new CodeMirror(
document.getElementById('content'),
{
autofocus: true,
lineNumbers: true,
lineWrapping: true,
readOnly: true,
styleActiveLine: true,
}
);
uBlockDashboard.patchCodeMirrorEditor(cmEditor);
const details = await vAPI.messaging.send('default', {
what : 'getAssetContent',
url: assetKey,
});
cmEditor.setValue(details && details.content || '');
if ( details.sourceURL ) {
const a = document.querySelector('.cm-search-widget .sourceURL');
a.setAttribute('href', details.sourceURL);
a.setAttribute('title', details.sourceURL);
}
})();

1113
src/js/assets.js
File diff suppressed because it is too large
View File

49
src/js/background.js

@ -23,7 +23,7 @@
/******************************************************************************/
const µMatrix = (function() { // jshint ignore:line
const µMatrix = (( ) => { // jshint ignore:line
/******************************************************************************/
@ -54,9 +54,22 @@ const oneDay = 24 * oneHour;
*/
const rawSettingsDefault = {
assetFetchBypassBrowserCache: false,
assetFetchTimeout: 30,
autoUpdateAssetFetchPeriod: 120,
cnameIgnoreList: 'unset',
cnameIgnore1stParty: true,
cnameIgnoreExceptions: true,
cnameIgnoreRootDocument: true,
cnameMaxTTL: 60,
cnameReplayFullURL: false,
consoleLogLevel: 'unset',
contributorMode: false,
disableCSPReportInjection: false,
disableWebAssembly: false,
enforceEscapedFragment: true,
loggerPopupType: 'popup',
manualUpdateAssetFetchPeriod: 500,
placeholderBackground:
[
'url("data:image/png;base64,',
@ -136,7 +149,6 @@ return {
externalHostsFiles: [],
externalRecipeFiles: [],
iconBadgeEnabled: true,
maxLoggedRequests: 1000,
noTooltips: false,
popupCollapseAllDomains: false,
popupCollapseBlacklistedDomains: false,
@ -155,22 +167,25 @@ return {
},
rawSettingsDefault: rawSettingsDefault,
rawSettings: (function() {
let out = Object.assign({}, rawSettingsDefault),
json = vAPI.localStorage.getItem('immediateRawSettings');
if ( typeof json === 'string' ) {
try {
let o = JSON.parse(json);
if ( o instanceof Object ) {
for ( const k in o ) {
if ( out.hasOwnProperty(k) ) {
out[k] = o[k];
}
}
rawSettings: (( ) => {
const out = Object.assign({}, rawSettingsDefault);
const json = vAPI.localStorage.getItem('immediateRawSettings');
if ( typeof json !== 'string' ) { return out; }
try {
const o = JSON.parse(json);
if ( o instanceof Object ) {
for ( const k in o ) {
if ( out.hasOwnProperty(k) ) { out[k] = o[k]; }
}
self.log.verbosity = out.consoleLogLevel;
if ( typeof out.suspendTabsUntilReady === 'boolean' ) {
out.suspendTabsUntilReady = out.suspendTabsUntilReady
? 'yes'
: 'unset';
}
}
catch(ex) {
}
}
catch(ex) {
}
return out;
})(),
@ -201,6 +216,7 @@ return {
pMatrix: null,
ubiquitousBlacklist: null,
ubiquitousBlacklistRef: null,
// various stats
cookieRemovedCounter: 0,
@ -208,7 +224,6 @@ return {
cookieHeaderFoiledCounter: 0,
hyperlinkAuditingFoiledCounter: 0,
browserCacheClearedCounter: 0,
storageUsed: 0,
// record what the browser is doing behind the scene
behindTheSceneScope: 'behind-the-scene',

25
src/js/browsercache.js

@ -23,24 +23,21 @@
/******************************************************************************/
(function() {
{
// >>>>> start of local scope
/******************************************************************************/
// Browser data jobs
var clearCache = function() {
const clearCache = function() {
vAPI.setTimeout(clearCache, 15 * 60 * 1000);
var µm = µMatrix;
if ( !µm.userSettings.clearBrowserCache ) {
return;
}
const µm = µMatrix;
if ( µm.userSettings.clearBrowserCache !== true ) { return; }
µm.clearBrowserCacheCycle -= 15;
if ( µm.clearBrowserCacheCycle > 0 ) {
return;
}
if ( µm.clearBrowserCacheCycle > 0 ) { return; }
vAPI.browserData.clearCache();
@ -48,15 +45,17 @@ var clearCache = function() {
µm.browserCacheClearedCounter++;
// TODO: i18n
µm.logger.writeOne({ info: vAPI.i18n('loggerEntryBrowserCacheCleared') });
//console.debug('clearBrowserCacheCallback()> vAPI.browserData.clearCache() called');
µm.logger.writeOne({
realm: 'message',
text: vAPI.i18n('loggerEntryBrowserCacheCleared'),
});
};
vAPI.setTimeout(clearCache, 15 * 60 * 1000);
/******************************************************************************/
})();
// <<<<< end of local scope
}
/******************************************************************************/

465
src/js/cachestorage.js

@ -0,0 +1,465 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2016-present The uBlock Origin authors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* global IDBDatabase, indexedDB */
'use strict';
/******************************************************************************/
// The code below has been originally manually imported from:
// Commit: https://github.com/nikrolls/uBlock-Edge/commit/d1538ea9bea89d507219d3219592382eee306134
// Commit date: 29 October 2016
// Commit author: https://github.com/nikrolls
// Commit message: "Implement cacheStorage using IndexedDB"
// The original imported code has been subsequently modified as it was not
// compatible with Firefox.
// (a Promise thing, see https://github.com/dfahlander/Dexie.js/issues/317)
// Furthermore, code to migrate from browser.storage.local to vAPI.storage
// has been added, for seamless migration of cache-related entries into
// indexedDB.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1371255
// Firefox-specific: we use indexedDB because browser.storage.local() has
// poor performance in Firefox.
// https://github.com/uBlockOrigin/uBlock-issues/issues/328
// Use IndexedDB for Chromium as well, to take advantage of LZ4
// compression.
// https://github.com/uBlockOrigin/uBlock-issues/issues/399
// Revert Chromium support of IndexedDB, use advanced setting to force
// IndexedDB.
// https://github.com/uBlockOrigin/uBlock-issues/issues/409
// Allow forcing the use of webext storage on Firefox.
µMatrix.cacheStorage = (function() {
const STORAGE_NAME = 'uMatrixCacheStorage';
// Default to webext storage.
const localStorage = webext.storage.local;
const api = {
name: 'browser.storage.local',
get: localStorage.get,
set: localStorage.set,
remove: localStorage.remove,
clear: localStorage.clear,
getBytesInUse: localStorage.getBytesInUse,
select: function(selectedBackend) {
let actualBackend = selectedBackend;
if ( actualBackend === undefined || actualBackend === 'unset' ) {
actualBackend = vAPI.webextFlavor.soup.has('firefox')
? 'indexedDB'
: 'browser.storage.local';
}
if ( actualBackend === 'indexedDB' ) {
return selectIDB().then(success => {
if ( success || selectedBackend === 'indexedDB' ) {
clearWebext();
return 'indexedDB';
}
clearIDB();
return 'browser.storage.local';
});
}
if ( actualBackend === 'browser.storage.local' ) {
clearIDB();
}
return Promise.resolve('browser.storage.local');
},
error: undefined
};
// Reassign API entries to that of indexedDB-based ones
const selectIDB = async function() {
let db;
let dbPromise;
let dbTimer;
const noopfn = function () {
};
const disconnect = function() {
if ( dbTimer !== undefined ) {
clearTimeout(dbTimer);
dbTimer = undefined;
}
if ( db instanceof IDBDatabase ) {
db.close();
db = undefined;
}
};
const keepAlive = function() {
if ( dbTimer !== undefined ) {
clearTimeout(dbTimer);
}
dbTimer = vAPI.setTimeout(
( ) => {
dbTimer = undefined;
disconnect();
},
Math.max(
µMatrix.hiddenSettings.autoUpdateAssetFetchPeriod * 2 * 1000,
180000
)
);
};
// https://github.com/gorhill/uBlock/issues/3156
// I have observed that no event was fired in Tor Browser 7.0.7 +
// medium security level after the request to open the database was
// created. When this occurs, I have also observed that the `error`
// property was already set, so this means uBO can detect here whether
// the database can be opened successfully. A try-catch block is
// necessary when reading the `error` property because we are not
// allowed to read this propery outside of event handlers in newer
// implementation of IDBRequest (my understanding).
const getDb = function() {
keepAlive();
if ( db !== undefined ) {
return Promise.resolve(db);
}
if ( dbPromise !== undefined ) {
return dbPromise;
}
dbPromise = new Promise(resolve => {
let req;
try {
req = indexedDB.open(STORAGE_NAME, 1);
if ( req.error ) {
console.log(req.error);
req = undefined;
}
} catch(ex) {
}
if ( req === undefined ) {
db = null;
dbPromise = undefined;
return resolve(null);
}
req.onupgradeneeded = function(ev) {
if ( ev.oldVersion === 1 ) { return; }
try {
const db = ev.target.result;
db.createObjectStore(STORAGE_NAME, { keyPath: 'key' });
} catch(ex) {
req.onerror();
}
};
req.onsuccess = function(ev) {
if ( resolve === undefined ) { return; }
req = undefined;
db = ev.target.result;
dbPromise = undefined;
resolve(db);
resolve = undefined;
};
req.onerror = req.onblocked = function() {
if ( resolve === undefined ) { return; }
req = undefined;
console.log(this.error);
db = null;
dbPromise = undefined;
resolve(null);
resolve = undefined;
};
setTimeout(( ) => {
if ( resolve === undefined ) { return; }
db = null;
dbPromise = undefined;
resolve(null);
resolve = undefined;
}, 5000);
});
return dbPromise;
};
const getFromDb = async function(keys, keyvalStore, callback) {
if ( typeof callback !== 'function' ) { return; }
if ( keys.length === 0 ) { return callback(keyvalStore); }
const promises = [];
const gotOne = function() {
if ( typeof this.result !== 'object' ) { return; }
keyvalStore[this.result.key] = this.result.value;
if ( this.result.value instanceof Blob === false ) { return; }
promises.push(
µMatrix.lz4Codec.decode(
this.result.key,
this.result.value
).then(result => {
keyvalStore[result.key] = result.data;
})
);
};
try {
const db = await getDb();
if ( !db ) { return callback(); }
const transaction = db.transaction(STORAGE_NAME, 'readonly');
transaction.oncomplete =
transaction.onerror =
transaction.onabort = ( ) => {
Promise.all(promises).then(( ) => {
callback(keyvalStore);
});
};
const table = transaction.objectStore(STORAGE_NAME);
for ( const key of keys ) {
const req = table.get(key);
req.onsuccess = gotOne;
req.onerror = noopfn;
}
}
catch(reason) {
console.info(`cacheStorage.getFromDb() failed: ${reason}`);
callback();
}
};
const visitAllFromDb = async function(visitFn) {
const db = await getDb();
if ( !db ) { return visitFn(); }
const transaction = db.transaction(STORAGE_NAME, 'readonly');
transaction.oncomplete =
transaction.onerror =
transaction.onabort = ( ) => visitFn();
const table = transaction.objectStore(STORAGE_NAME);
const req = table.openCursor();
req.onsuccess = function(ev) {
let cursor = ev.target && ev.target.result;
if ( !cursor ) { return; }
let entry = cursor.value;
visitFn(entry);
cursor.continue();
};
};
const getAllFromDb = function(callback) {
if ( typeof callback !== 'function' ) { return; }
const promises = [];
const keyvalStore = {};
visitAllFromDb(entry => {
if ( entry === undefined ) {
Promise.all(promises).then(( ) => {
callback(keyvalStore);
});
return;
}
keyvalStore[entry.key] = entry.value;
if ( entry.value instanceof Blob === false ) { return; }
promises.push(
µMatrix.lz4Codec.decode(
entry.key,
entry.value
).then(result => {
keyvalStore[result.key] = result.value;
})
);
}).catch(reason => {
console.info(`cacheStorage.getAllFromDb() failed: ${reason}`);
callback();
});
};
// https://github.com/uBlockOrigin/uBlock-issues/issues/141
// Mind that IDBDatabase.transaction() and IDBObjectStore.put()
// can throw:
// https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/transaction
// https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/put
const putToDb = async function(keyvalStore, callback) {
if ( typeof callback !== 'function' ) {
callback = noopfn;
}
const keys = Object.keys(keyvalStore);
if ( keys.length === 0 ) { return callback(); }
const promises = [ getDb() ];
const entries = [];
const dontCompress =
µMatrix.hiddenSettings.cacheStorageCompression !== true;
const handleEncodingResult = result => {
entries.push({ key: result.key, value: result.data });
};
for ( const key of keys ) {
const data = keyvalStore[key];
const isString = typeof data === 'string';
if ( isString === false || dontCompress ) {
entries.push({ key, value: data });
continue;
}
promises.push(
µMatrix.lz4Codec.encode(key, data).then(handleEncodingResult)
);
}
const finish = ( ) => {
if ( callback === undefined ) { return; }
let cb = callback;
callback = undefined;
cb();
};
try {
const results = await Promise.all(promises);
const db = results[0];
if ( !db ) { return callback(); }
const transaction = db.transaction(
STORAGE_NAME,
'readwrite'
);
transaction.oncomplete =
transaction.onerror =
transaction.onabort = finish;
const table = transaction.objectStore(STORAGE_NAME);
for ( const entry of entries ) {
table.put(entry);
}
} catch (ex) {
finish();
}
};
const deleteFromDb = async function(input, callback) {
if ( typeof callback !== 'function' ) {
callback = noopfn;
}
const keys = Array.isArray(input) ? input.slice() : [ input ];
if ( keys.length === 0 ) { return callback(); }
const finish = ( ) => {
if ( callback === undefined ) { return; }
let cb = callback;
callback = undefined;
cb();
};
try {
const db = await getDb();
if ( !db ) { return callback(); }
const transaction = db.transaction(STORAGE_NAME, 'readwrite');
transaction.oncomplete =
transaction.onerror =
transaction.onabort = finish;
const table = transaction.objectStore(STORAGE_NAME);
for ( const key of keys ) {
table.delete(key);
}
} catch (ex) {
finish();
}
};
const clearDb = async function(callback) {
if ( typeof callback !== 'function' ) {
callback = noopfn;
}
try {
const db = await getDb();
if ( !db ) { return callback(); }
const transaction = db.transaction(STORAGE_NAME, 'readwrite');
transaction.oncomplete =
transaction.onerror =
transaction.onabort = ( ) => {
callback();
};
transaction.objectStore(STORAGE_NAME).clear();
}
catch(reason) {
console.info(`cacheStorage.clearDb() failed: ${reason}`);
callback();
}
};
await getDb();
if ( !db ) { return false; }
api.name = 'indexedDB';
api.get = function get(keys) {
return new Promise(resolve => {
if ( keys === null ) {
return getAllFromDb(bin => resolve(bin));
}
let toRead, output = {};
if ( typeof keys === 'string' ) {
toRead = [ keys ];
} else if ( Array.isArray(keys) ) {
toRead = keys;
} else /* if ( typeof keys === 'object' ) */ {
toRead = Object.keys(keys);
output = keys;
}
getFromDb(toRead, output, bin => resolve(bin));
});
};
api.set = function set(keys) {
return new Promise(resolve => {
putToDb(keys, details => resolve(details));
});
};
api.remove = function remove(keys) {
return new Promise(resolve => {
deleteFromDb(keys, ( ) => resolve());
});
};
api.clear = function clear() {
return new Promise(resolve => {
clearDb(( ) => resolve());
});
};
api.getBytesInUse = function getBytesInUse() {
return Promise.resolve(0);
};
return true;
};
// https://github.com/uBlockOrigin/uBlock-issues/issues/328
// Delete cache-related entries from webext storage.
const clearWebext = async function() {
const bin = await webext.storage.local.get('assetCacheRegistry');
if (
bin instanceof Object === false ||
bin.assetCacheRegistry instanceof Object === false
) {
return;
}
const toRemove = [
'assetCacheRegistry',
'assetSourceRegistry',
'resourcesSelfie',
'selfie'
];
for ( const key in bin.assetCacheRegistry ) {
if ( bin.assetCacheRegistry.hasOwnProperty(key) ) {
toRemove.push('cache/' + key);
}
}
webext.storage.local.remove(toRemove);
};
const clearIDB = function() {
try {
indexedDB.deleteDatabase(STORAGE_NAME);
} catch(ex) {
}
};
return api;
}());
/******************************************************************************/

104
src/js/cloud-ui.js

@ -25,7 +25,7 @@
/******************************************************************************/
(function() {
(( ) => {
/******************************************************************************/
@ -39,19 +39,15 @@ self.cloud = {
/******************************************************************************/
var widget = uDom.nodeFromId('cloudWidget');
if ( widget === null ) {
return;
}
const widget = uDom.nodeFromId('cloudWidget');
if ( widget === null ) { return; }
self.cloud.datakey = widget.getAttribute('data-cloud-entry') || '';
if ( self.cloud.datakey === '' ) {
return;
}
if ( self.cloud.datakey === '' ) { return; }
/******************************************************************************/
var onCloudDataReceived = function(entry) {
const onCloudDataReceived = function(entry) {
if ( entry instanceof Object === false ) { return; }
self.cloud.data = entry.data;
@ -59,7 +55,7 @@ var onCloudDataReceived = function(entry) {
uDom.nodeFromId('cloudPull').removeAttribute('disabled');
uDom.nodeFromId('cloudPullAndMerge').removeAttribute('disabled');
let timeOptions = {
const timeOptions = {
weekday: 'short',
year: 'numeric',
month: 'short',
@ -70,7 +66,7 @@ var onCloudDataReceived = function(entry) {
timeZoneName: 'short'
};
let time = new Date(entry.tstamp);
const time = new Date(entry.tstamp);
widget.querySelector('[data-i18n="cloudNoData"]').textContent =
entry.source + '\n' +
time.toLocaleString('fullwide', timeOptions);
@ -78,37 +74,31 @@ var onCloudDataReceived = function(entry) {
/******************************************************************************/
var fetchCloudData = function() {
vAPI.messaging.send(
'cloud-ui.js',
{
what: 'cloudPull',
datakey: self.cloud.datakey
},
onCloudDataReceived
);
const fetchCloudData = function() {
vAPI.messaging.send('cloud-ui.js', {
what: 'cloudPull',
datakey: self.cloud.datakey
}).then(response => {
onCloudDataReceived(response);
});
};
/******************************************************************************/
var pushData = function() {
if ( typeof self.cloud.onPush !== 'function' ) {
return;
}
vAPI.messaging.send(
'cloud-ui.js',
{
what: 'cloudPush',
datakey: self.cloud.datakey,
data: self.cloud.onPush()
},
fetchCloudData
);
const pushData = function() {
if ( typeof self.cloud.onPush !== 'function' ) { return; }
vAPI.messaging.send('cloud-ui.js', {
what: 'cloudPush',
datakey: self.cloud.datakey,
data: self.cloud.onPush()
}).then(( ) => {
fetchCloudData();
});
};
/******************************************************************************/
var pullData = function(ev) {
const pullData = function(ev) {
if ( typeof self.cloud.onPull === 'function' ) {
self.cloud.onPull(self.cloud.data, ev.shiftKey);
}
@ -116,7 +106,7 @@ var pullData = function(ev) {
/******************************************************************************/
var pullAndMergeData = function() {
const pullAndMergeData = function() {
if ( typeof self.cloud.onPull === 'function' ) {
self.cloud.onPull(self.cloud.data, true);
}
@ -124,8 +114,8 @@ var pullAndMergeData = function() {
/******************************************************************************/
var openOptions = function() {
let input = uDom.nodeFromId('cloudDeviceName');
const openOptions = function() {
const input = uDom.nodeFromId('cloudDeviceName');
input.value = self.cloud.options.deviceName;
input.setAttribute('placeholder', self.cloud.options.defaultDeviceName);
uDom.nodeFromId('cloudOptions').classList.add('show');
@ -133,50 +123,46 @@ var openOptions = function() {
/******************************************************************************/
var closeOptions = function(ev) {
let root = uDom.nodeFromId('cloudOptions');
if ( ev.target !== root ) {
return;
}
const closeOptions = function(ev) {
const root = uDom.nodeFromId('cloudOptions');
if ( ev.target !== root ) { return; }
root.classList.remove('show');
};
/******************************************************************************/
var submitOptions = function() {
let onOptions = function(options) {
if ( typeof options !== 'object' || options === null ) {
return;
}
self.cloud.options = options;
};
const submitOptions = function() {
vAPI.messaging.send('cloud-ui.js', {
what: 'cloudSetOptions',
options: {
deviceName: uDom.nodeFromId('cloudDeviceName').value
}
}, onOptions);
}).then(options => {
if ( typeof options !== 'object' || options === null ) { return; }
self.cloud.options = options;
});
uDom.nodeFromId('cloudOptions').classList.remove('show');
};
/******************************************************************************/
var onInitialize = function(options) {
vAPI.messaging.send('cloud-ui.js', {
what: 'cloudGetOptions'
}).then(options => {
if ( typeof options !== 'object' || options === null ) { return; }
if ( !options.enabled ) { return; }
self.cloud.options = options;
let xhr = new XMLHttpRequest();
const xhr = new XMLHttpRequest();
xhr.open('GET', 'cloud-ui.html', true);
xhr.overrideMimeType('text/html;charset=utf-8');
xhr.responseType = 'text';
xhr.onload = function() {
this.onload = null;
let parser = new DOMParser(),
parsed = parser.parseFromString(this.responseText, 'text/html'),
fromParent = parsed.body;
const parser = new DOMParser();
const parsed = parser.parseFromString(this.responseText, 'text/html');
const fromParent = parsed.body;
while ( fromParent.firstElementChild !== null ) {
widget.appendChild(
document.adoptNode(fromParent.firstElementChild)
@ -198,12 +184,8 @@ var onInitialize = function(options) {
fetchCloudData();
};
xhr.send();
};
vAPI.messaging.send('cloud-ui.js', { what: 'cloudGetOptions' }, onInitialize);
});
/******************************************************************************/
// https://www.youtube.com/watch?v=aQFp67VoiDA
})();

37
src/js/codemirror/mode/raw-settings.js

@ -0,0 +1,37 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* global CodeMirror */
'use strict';
CodeMirror.defineMode("raw-settings", function() {
return {
token: function(stream) {
if ( stream.sol() ) {
stream.match(/\s*\S+/);
return 'keyword';
}
stream.skipToEnd();
return null;
}
};
});

336
src/js/codemirror/search.js

@ -0,0 +1,336 @@
// The following code is heavily based on the standard CodeMirror
// search addon found at: https://codemirror.net/addon/search/search.js
// I added/removed and modified code in order to get a closer match to a
// browser's built-in find-in-page feature which are just enough for
// uBlock Origin.
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Define search commands. Depends on dialog.js or another
// implementation of the openDialog method.
// Replace works a little oddly -- it will do the replace on the next
// Ctrl-G (or whatever is bound to findNext) press. You prevent a
// replace by making sure the match is no longer selected when hitting
// Ctrl-G.
/* globals define, require, CodeMirror */
'use strict';
(function(mod) {
if (typeof exports === "object" && typeof module === "object") // CommonJS
mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog"));
else if (typeof define === "function" && define.amd) // AMD
define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
function searchOverlay(query, caseInsensitive) {
if (typeof query === "string")
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
else if (!query.global)
query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
return {
token: function(stream) {
query.lastIndex = stream.pos;
var match = query.exec(stream.string);
if (match && match.index === stream.pos) {
stream.pos += match[0].length || 1;
return "searching";
} else if (match) {
stream.pos = match.index;
} else {
stream.skipToEnd();
}
}
};
}
function searchWidgetKeydownHandler(cm, ev) {
var keyName = CodeMirror.keyName(ev);
if ( !keyName ) { return; }
CodeMirror.lookupKey(
keyName,
cm.getOption('keyMap'),
function(command) {
if ( widgetCommandHandler(cm, command) ) {
ev.preventDefault();
ev.stopPropagation();
}
}
);
}
function searchWidgetInputHandler(cm) {
let state = getSearchState(cm);
if ( queryTextFromSearchWidget(cm) === state.queryText ) { return; }
if ( state.queryTimer !== null ) {
clearTimeout(state.queryTimer);
}
state.queryTimer = setTimeout(
() => {
state.queryTimer = null;
findCommit(cm, 0);
},
350
);
}
function searchWidgetClickHandler(cm, ev) {
var tcl = ev.target.classList;
if ( tcl.contains('cm-search-widget-up') ) {
findNext(cm, -1);
} else if ( tcl.contains('cm-search-widget-down') ) {
findNext(cm, 1);
}
if ( ev.target.localName !== 'input' ) {
ev.preventDefault();
} else {
ev.stopImmediatePropagation();
}
}
function queryTextFromSearchWidget(cm) {
return getSearchState(cm).widget.querySelector('input[type="search"]').value;
}
function queryTextToSearchWidget(cm, q) {
var input = getSearchState(cm).widget.querySelector('input[type="search"]');
if ( typeof q === 'string' && q !== input.value ) {
input.value = q;
}
input.setSelectionRange(0, input.value.length);
input.focus();
}
function SearchState(cm) {
this.query = null;
this.overlay = null;
this.panel = null;
const widgetParent =
document.querySelector('.cm-search-widget-template').cloneNode(true);
this.widget = widgetParent.children[0];
this.widget.addEventListener('keydown', searchWidgetKeydownHandler.bind(null, cm));
this.widget.addEventListener('input', searchWidgetInputHandler.bind(null, cm));
this.widget.addEventListener('mousedown', searchWidgetClickHandler.bind(null, cm));
if ( typeof cm.addPanel === 'function' ) {
this.panel = cm.addPanel(this.widget);
}
this.queryText = '';
this.queryTimer = null;
}
// We want the search widget to behave as if the focus was on the
// CodeMirror editor.
const reSearchCommands = /^(?:find|findNext|findPrev|newlineAndIndent)$/;
function widgetCommandHandler(cm, command) {
if ( reSearchCommands.test(command) === false ) { return false; }
var queryText = queryTextFromSearchWidget(cm);
if ( command === 'find' ) {
queryTextToSearchWidget(cm);
return true;
}
if ( queryText.length !== 0 ) {
findNext(cm, command === 'findPrev' ? -1 : 1);
}
return true;
}
function getSearchState(cm) {
return cm.state.search || (cm.state.search = new SearchState(cm));
}
function queryCaseInsensitive(query) {
return typeof query === "string" && query === query.toLowerCase();
}
function getSearchCursor(cm, query, pos) {
// Heuristic: if the query string is all lowercase, do a case insensitive search.
return cm.getSearchCursor(
query,
pos,
{ caseFold: queryCaseInsensitive(query), multiline: false }
);
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/658
// Modified to backslash-escape ONLY widely-used control characters.
function parseString(string) {
return string.replace(/\\[nrt\\]/g, function(match) {
if (match === "\\n") return "\n";
if (match === "\\r") return "\r";
if (match === '\\t') return '\t';
if (match === '\\\\') return '\\';
return match;
});
}
function parseQuery(query) {
var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
if (isRE) {
try { query = new RegExp(isRE[1], isRE[2].indexOf("i") === -1 ? "" : "i"); }
catch(e) {} // Not a regular expression after all, do a string search
} else {
query = parseString(query);
}
if (typeof query === "string" ? query === "" : query.test(""))
query = /x^/;
return query;
}
function startSearch(cm, state) {
state.query = parseQuery(state.queryText);
if ( state.overlay ) {
cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
}
state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
cm.addOverlay(state.overlay);
if ( cm.showMatchesOnScrollbar ) {
if ( state.annotate ) {
state.annotate.clear();
state.annotate = null;
}
state.annotate = cm.showMatchesOnScrollbar(
state.query,
queryCaseInsensitive(state.query),
{ multiline: false }
);
let count = state.annotate.matches.length;
state.widget
.querySelector('.cm-search-widget-count > span:nth-of-type(2)')
.textContent = count > 1000 ? '1000+' : count;
state.widget.setAttribute('data-query', state.queryText);
// Ensure the caret is visible
let input = state.widget.querySelector('.cm-search-widget-input > input');
input.selectionStart = input.selectionStart;
}
}
function findNext(cm, dir, callback) {
cm.operation(function() {
var state = getSearchState(cm);
if ( !state.query ) { return; }
var cursor = getSearchCursor(
cm,
state.query,
dir <= 0 ? cm.getCursor('from') : cm.getCursor('to')
);
let previous = dir < 0;
if (!cursor.find(previous)) {
cursor = getSearchCursor(
cm,
state.query,
previous ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)
);
if (!cursor.find(previous)) return;
}
cm.setSelection(cursor.from(), cursor.to());
cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);
if (callback) callback(cursor.from(), cursor.to());
});
}
function clearSearch(cm, hard) {
cm.operation(function() {
var state = getSearchState(cm);
if ( state.query ) {
state.query = state.queryText = null;
}
if ( state.overlay ) {
cm.removeOverlay(state.overlay);
state.overlay = null;
}
if ( state.annotate ) {
state.annotate.clear();
state.annotate = null;
}
state.widget.removeAttribute('data-query');
if ( hard ) {
state.panel.clear();
state.panel = null;
state.widget = null;
cm.state.search = null;
}
});
}
function findCommit(cm, dir) {
var state = getSearchState(cm);
if ( state.queryTimer !== null ) {
clearTimeout(state.queryTimer);
state.queryTimer = null;
}
var queryText = queryTextFromSearchWidget(cm);
if ( queryText === state.queryText ) { return; }
state.queryText = queryText;
if ( state.queryText === '' ) {
clearSearch(cm);
} else {
cm.operation(function() {
startSearch(cm, state);
findNext(cm, dir);
});
}
}
function findCommand(cm) {
var queryText = cm.getSelection() || undefined;
if ( !queryText ) {
var word = cm.findWordAt(cm.getCursor());
queryText = cm.getRange(word.anchor, word.head);
if ( /^\W|\W$/.test(queryText) ) {
queryText = undefined;
}
cm.setCursor(word.anchor);
}
queryTextToSearchWidget(cm, queryText);
findCommit(cm, 1);
}
function findNextCommand(cm) {
var state = getSearchState(cm);
if ( state.query ) { return findNext(cm, 1); }
}
function findPrevCommand(cm) {
var state = getSearchState(cm);
if ( state.query ) { return findNext(cm, -1); }
}
{
const searchWidgetTemplate =
'<div class="cm-search-widget-template" style="display:none;">' +
'<div class="cm-search-widget">' +
'<span class="fa-icon fa-icon-ro">search</span>&ensp;' +
'<span class="cm-search-widget-input">' +
'<input type="search">' +
'<span class="cm-search-widget-count">' +
'<span><!-- future use --></span><span>0</span>' +
'</span>' +
'</span>&ensp;' +
'<span class="cm-search-widget-up cm-search-widget-button fa-icon">angle-up</span>&ensp;' +
'<span class="cm-search-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span>&ensp;' +
'<a class="fa-icon sourceURL" href>external-link</a>' +
'</div>' +
'</div>';
const domParser = new DOMParser();
const doc = domParser.parseFromString(searchWidgetTemplate, 'text/html');
const widgetTemplate = document.adoptNode(doc.body.firstElementChild);
document.body.appendChild(widgetTemplate);
}
CodeMirror.commands.find = findCommand;
CodeMirror.commands.findNext = findNextCommand;
CodeMirror.commands.findPrev = findPrevCommand;
CodeMirror.defineInitHook(function(cm) {
getSearchState(cm);
});
});

34
src/js/console.js

@ -0,0 +1,34 @@
/*******************************************************************************
uMatrix - a browser extension to block requests.
Copyright (C) 2019-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
self.log = (function() {
const noopFunc = function() {};
const info = function(s) { console.log(`[uMatrix] ${s}`); };
return {
get verbosity( ) { return; },
set verbosity(level) {
this.info = console.info = level === 'info' ? info : noopFunc;
},
info: noopFunc,
};
})();

39
src/js/contentscript-start.js

@ -1,7 +1,7 @@
/*******************************************************************************
uMatrix - a browser extension to black/white list requests.
Copyright (C) 2017-2018 Raymond Hill
Copyright (C) 2017-present 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
@ -26,15 +26,15 @@
// Injected into content pages
(function() {
(( ) => {
if ( typeof vAPI !== 'object' ) { return; }
vAPI.selfWorkerSrcReported = vAPI.selfWorkerSrcReported || false;
var reGoodWorkerSrc = /(?:child|worker)-src[^;,]+?'none'/;
const reGoodWorkerSrc = /(?:child|worker)-src[^;,]+?'none'/;
var handler = function(ev) {
const handler = function(ev) {
if (
ev.isTrusted !== true ||
ev.originalPolicy.includes('report-uri about:blank') === false
@ -69,28 +69,21 @@
vAPI.selfWorkerSrcReported = true;
}
vAPI.messaging.send(
'contentscript.js',
{
what: 'securityPolicyViolation',
directive: 'worker-src',
blockedURI: ev.blockedURI,
documentURI: ev.documentURI,
blocked: ev.disposition === 'enforce'
}
);
vAPI.messaging.send('contentscript.js', {
what: 'securityPolicyViolation',
directive: 'worker-src',
blockedURI: ev.blockedURI,
documentURI: ev.documentURI,
blocked: ev.disposition === 'enforce',
});
return true;
};
document.addEventListener(
'securitypolicyviolation',
function(ev) {
if ( !handler(ev) ) { return; }
ev.stopPropagation();
ev.preventDefault();
},
true
);
document.addEventListener('securitypolicyviolation', ev => {
if ( !handler(ev) ) { return; }
ev.stopPropagation();
ev.preventDefault();
}, true);
})();

192
src/js/contentscript.js

@ -1,7 +1,7 @@
/*******************************************************************************
uMatrix - a browser extension to black/white list requests.
Copyright (C) 2014-2018 Raymond Hill
Copyright (C) 2014-present 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
@ -28,7 +28,7 @@
// Injected into content pages
(function() {
(( ) => {
/******************************************************************************/
@ -42,9 +42,7 @@ if (
}
// This can also happen (for example if script injected into a `data:` URI doc)
if ( !window.location ) {
return;
}
if ( !window.location ) { return; }
// This can happen
if ( typeof vAPI !== 'object' ) {
@ -65,8 +63,8 @@ vAPI.contentscriptEndInjected = true;
// Executed only once.
(function() {
var localStorageHandler = function(mustRemove) {
{
const localStorageHandler = function(mustRemove) {
if ( mustRemove ) {
window.localStorage.clear();
window.sessionStorage.clear();
@ -78,15 +76,17 @@ vAPI.contentscriptEndInjected = true;
// to site data is disabled.
// https://github.com/gorhill/httpswitchboard/issues/215
try {
var hasLocalStorage =
const hasLocalStorage =
window.localStorage && window.localStorage.length !== 0;
var hasSessionStorage =
const hasSessionStorage =
window.sessionStorage && window.sessionStorage.length !== 0;
if ( hasLocalStorage || hasSessionStorage ) {
vAPI.messaging.send('contentscript.js', {
what: 'contentScriptHasLocalStorage',
originURL: window.location.origin
}, localStorageHandler);
originURL: window.location.origin,
}).then(response => {
localStorageHandler(response);
});
}
// TODO: indexedDB
@ -105,41 +105,41 @@ vAPI.contentscriptEndInjected = true;
}
catch (e) {
}
})();
}
/******************************************************************************/
/******************************************************************************/
// https://github.com/gorhill/uMatrix/issues/45
var collapser = (function() {
var resquestIdGenerator = 1,
const collapser = (( ) => {
let resquestIdGenerator = 1,
processTimer,
toProcess = [],
toFilter = [],
toCollapse = new Map(),
cachedBlockedMap,
cachedBlockedMapHash,
cachedBlockedMapTimer,
reURLPlaceholder = /\{\{url\}\}/g;
var src1stProps = {
cachedBlockedMapTimer;
const toCollapse = new Map();
const reURLPlaceholder = /\{\{url\}\}/g;
const src1stProps = {
'embed': 'src',
'frame': 'src',
'iframe': 'src',
'img': 'src',
'object': 'data'
};
var src2ndProps = {
const src2ndProps = {
'img': 'srcset'
};
var tagToTypeMap = {
const tagToTypeMap = {
embed: 'media',
frame: 'frame',
iframe: 'frame',
img: 'image',
object: 'media'
};
var cachedBlockedSetClear = function() {
const cachedBlockedSetClear = function() {
cachedBlockedMap =
cachedBlockedMapHash =
cachedBlockedMapTimer = undefined;
@ -147,13 +147,13 @@ var collapser = (function() {
// https://github.com/chrisaljoudi/uBlock/issues/174
// Do not remove fragment from src URL
var onProcessed = function(response) {
const onProcessed = function(response) {
if ( !response ) { // This happens if uBO is disabled or restarted.
toCollapse.clear();
return;
}
var targets = toCollapse.get(response.id);
const targets = toCollapse.get(response.id);
if ( targets === undefined ) { return; }
toCollapse.delete(response.id);
if ( cachedBlockedMapHash !== response.hash ) {
@ -168,10 +168,10 @@ var collapser = (function() {
return;
}
let placeholders = response.placeholders;
const placeholders = response.placeholders;
for ( let target of targets ) {
let tag = target.localName;
for ( const target of targets ) {
const tag = target.localName;
let prop = src1stProps[tag];
if ( prop === undefined ) { continue; }
let src = target[prop];
@ -181,7 +181,7 @@ var collapser = (function() {
src = target[prop];
if ( typeof src !== 'string' || src.length === 0 ) { continue; }
}
let collapsed = cachedBlockedMap.get(tagToTypeMap[tag] + ' ' + src);
const collapsed = cachedBlockedMap.get(tagToTypeMap[tag] + ' ' + src);
if ( collapsed === undefined ) { continue; }
if ( collapsed ) {
target.style.setProperty('display', 'none', 'important');
@ -192,7 +192,7 @@ var collapser = (function() {
case 'frame':
case 'iframe':
if ( placeholders.frame !== true ) { break; }
let docurl =
const docurl =
'data:text/html,' +
encodeURIComponent(
placeholders.frameDocument.replace(
@ -244,22 +244,23 @@ var collapser = (function() {
}
};
var send = function() {
const send = function() {
processTimer = undefined;
toCollapse.set(resquestIdGenerator, toProcess);
var msg = {
vAPI.messaging.send('contentscript.js', {
what: 'lookupBlockedCollapsibles',
id: resquestIdGenerator,
toFilter: toFilter,
hash: cachedBlockedMapHash
};
vAPI.messaging.send('contentscript.js', msg, onProcessed);
hash: cachedBlockedMapHash,
}).then(response => {
onProcessed(response);
});
toProcess = [];
toFilter = [];
resquestIdGenerator += 1;
};
var process = function(delay) {
const process = function(delay) {
if ( toProcess.length === 0 ) { return; }
if ( delay === 0 ) {
if ( processTimer !== undefined ) {
@ -271,7 +272,7 @@ var collapser = (function() {
}
};
var add = function(target) {
const add = function(target) {
toProcess.push(target);
};
@ -282,20 +283,20 @@ var collapser = (function() {
}
};
var iframeSourceModified = function(mutations) {
var i = mutations.length;
const iframeSourceModified = function(mutations) {
let i = mutations.length;
while ( i-- ) {
addIFrame(mutations[i].target, true);
}
process();
};
var iframeSourceObserver;
var iframeSourceObserverOptions = {
let iframeSourceObserver;
const iframeSourceObserverOptions = {
attributes: true,
attributeFilter: [ 'src' ]
attributeFilter: [ 'src' ],
};
var addIFrame = function(iframe, dontObserve) {
const addIFrame = function(iframe, dontObserve) {
// https://github.com/gorhill/uBlock/issues/162
// Be prepared to deal with possible change of src attribute.
if ( dontObserve !== true ) {
@ -304,25 +305,24 @@ var collapser = (function() {
}
iframeSourceObserver.observe(iframe, iframeSourceObserverOptions);
}
var src = iframe.src;
const src = iframe.src;
if ( src === '' || typeof src !== 'string' ) { return; }
if ( src.startsWith('http') === false ) { return; }
toFilter.push({ type: 'frame', url: iframe.src });
add(iframe);
};
var addIFrames = function(iframes) {
var i = iframes.length;
const addIFrames = function(iframes) {
let i = iframes.length;
while ( i-- ) {
addIFrame(iframes[i]);
}
};
var addNodeList = function(nodeList) {
var node,
i = nodeList.length;
const addNodeList = function(nodeList) {
let i = nodeList.length;
while ( i-- ) {
node = nodeList[i];
const node = nodeList[i];
if ( node.nodeType !== 1 ) { continue; }
if ( node.localName === 'iframe' || node.localName === 'frame' ) {
addIFrame(node);
@ -333,7 +333,7 @@ var collapser = (function() {
}
};
var onResourceFailed = function(ev) {
const onResourceFailed = function(ev) {
if ( tagToTypeMap[ev.target.localName] !== undefined ) {
add(ev.target);
process();
@ -354,10 +354,10 @@ var collapser = (function() {
});
return {
addMany: addMany,
addIFrames: addIFrames,
addNodeList: addNodeList,
process: process
addMany,
addIFrames,
addNodeList,
process,
};
})();
@ -368,16 +368,16 @@ var collapser = (function() {
// Added node lists will be cumulated here before being processed
(function() {
(( ) => {
// This fixes http://acid3.acidtests.org/
if ( !document.body ) { return; }
var addedNodeLists = [];
var addedNodeListsTimer;
let addedNodeLists = [];
let addedNodeListsTimer;
var treeMutationObservedHandler = function() {
const treeMutationObservedHandler = function() {
addedNodeListsTimer = undefined;
var i = addedNodeLists.length;
let i = addedNodeLists.length;
while ( i-- ) {
collapser.addNodeList(addedNodeLists[i]);
}
@ -387,11 +387,10 @@ var collapser = (function() {
// https://github.com/gorhill/uBlock/issues/205
// Do not handle added node directly from within mutation observer.
var treeMutationObservedHandlerAsync = function(mutations) {
var iMutation = mutations.length,
nodeList;
const treeMutationObservedHandlerAsync = function(mutations) {
let iMutation = mutations.length;
while ( iMutation-- ) {
nodeList = mutations[iMutation].addedNodes;
const nodeList = mutations[iMutation].addedNodes;
if ( nodeList.length !== 0 ) {
addedNodeLists.push(nodeList);
}
@ -402,7 +401,7 @@ var collapser = (function() {
};
// https://github.com/gorhill/httpswitchboard/issues/176
var treeObserver = new MutationObserver(treeMutationObservedHandlerAsync);
let treeObserver = new MutationObserver(treeMutationObservedHandlerAsync);
treeObserver.observe(document.body, {
childList: true,
subtree: true
@ -437,7 +436,7 @@ var collapser = (function() {
// https://github.com/gorhill/uMatrix/issues/924
// Report inline styles.
(function() {
{
if (
document.querySelector('script:not([src])') !== null ||
document.querySelector('a[href^="javascript:"]') !== null ||
@ -446,7 +445,7 @@ var collapser = (function() {
vAPI.messaging.send('contentscript.js', {
what: 'securityPolicyViolation',
directive: 'script-src',
documentURI: window.location.href
documentURI: window.location.href,
});
}
@ -454,14 +453,14 @@ var collapser = (function() {
vAPI.messaging.send('contentscript.js', {
what: 'securityPolicyViolation',
directive: 'style-src',
documentURI: window.location.href
documentURI: window.location.href,
});
}
collapser.addMany(document.querySelectorAll('img'));
collapser.addIFrames(document.querySelectorAll('iframe, frame'));
collapser.process();
})();
}
/******************************************************************************/
/******************************************************************************/
@ -471,23 +470,23 @@ var collapser = (function() {
// https://github.com/gorhill/uMatrix/issues/232
// Force `display` property, Firefox is still affected by the issue.
(function() {
var noscripts = document.querySelectorAll('noscript');
(( ) => {
const noscripts = document.querySelectorAll('noscript');
if ( noscripts.length === 0 ) { return; }
var redirectTimer,
reMetaContent = /^\s*(\d+)\s*;\s*url=(['"]?)([^'"]+)\2/i,
reSafeURL = /^https?:\/\//;
const reMetaContent = /^\s*(\d+)\s*;\s*url=(['"]?)([^'"]+)\2/i;
const reSafeURL = /^https?:\/\//;
let redirectTimer;
var autoRefresh = function(root) {
var meta = root.querySelector('meta[http-equiv="refresh"][content]');
const autoRefresh = function(root) {
const meta = root.querySelector('meta[http-equiv="refresh"][content]');
if ( meta === null ) { return; }
var match = reMetaContent.exec(meta.getAttribute('content'));
const match = reMetaContent.exec(meta.getAttribute('content'));
if ( match === null || match[3].trim() === '' ) { return; }
var url = new URL(match[3], document.baseURI);
const url = new URL(match[3], document.baseURI);
if ( reSafeURL.test(url.href) === false ) { return; }
redirectTimer = setTimeout(
function() {
( ) => {
location.assign(url.href);
},
parseInt(match[1], 10) * 1000 + 1
@ -495,29 +494,28 @@ var collapser = (function() {
meta.parentNode.removeChild(meta);
};
var morphNoscript = function(from) {
const morphNoscript = function(from) {
if ( /^application\/(?:xhtml\+)?xml/.test(document.contentType) ) {
var to = document.createElement('span');
const to = document.createElement('span');
while ( from.firstChild !== null ) {
to.appendChild(from.firstChild);
}
return to;
}
var parser = new DOMParser();
var doc = parser.parseFromString(
const parser = new DOMParser();
const doc = parser.parseFromString(
'<span>' + from.textContent + '</span>',
'text/html'
);
return document.adoptNode(doc.querySelector('span'));
};
var renderNoscriptTags = function(response) {
const renderNoscriptTags = function(response) {
if ( response !== true ) { return; }
var parent, span;
for ( var noscript of noscripts ) {
parent = noscript.parentNode;
const parent = noscript.parentNode;
if ( parent === null ) { continue; }
span = morphNoscript(noscript);
const span = morphNoscript(noscript);
span.style.setProperty('display', 'inline', 'important');
if ( redirectTimer === undefined ) {
autoRefresh(span);
@ -526,25 +524,23 @@ var collapser = (function() {
}
};
vAPI.messaging.send(
'contentscript.js',
{ what: 'mustRenderNoscriptTags?' },
renderNoscriptTags
);
vAPI.messaging.send('contentscript.js', {
what: 'mustRenderNoscriptTags?',
}).then(response => {
renderNoscriptTags(response);
});
})();
/******************************************************************************/
/******************************************************************************/
vAPI.messaging.send(
'contentscript.js',
{ what: 'shutdown?' },
function(response) {
if ( response === true ) {
vAPI.shutdown.exec();
}
vAPI.messaging.send('contentscript.js', {
what: 'shutdown?',
}).then(response => {
if ( response === true ) {
vAPI.shutdown.exec();
}
);
});
/******************************************************************************/
/******************************************************************************/

293
src/js/cookies.js

@ -33,58 +33,59 @@
// Use cached-context approach rather than object-based approach, as details
// of the implementation do not need to be visible
µMatrix.cookieHunter = (function() {
µMatrix.cookieHunter = (( ) => {
/******************************************************************************/
var µm = µMatrix;
const µm = µMatrix;
var recordPageCookiesQueue = new Map();
var removeCookieQueue = new Set();
var cookieDict = new Map();
var cookieEntryJunkyard = [];
var processRemoveQueuePeriod = 2 * 60 * 1000;
var processCleanPeriod = 10 * 60 * 1000;
var processPageRecordQueueTimer = null;
const recordPageCookiesQueue = new Map();
const removeCookieQueue = new Set();
const cookieDict = new Map();
const cookieEntryJunkyard = [];
const processRemoveQueuePeriod = 2 * 60 * 1000;
const processCleanPeriod = 10 * 60 * 1000;
let processPageRecordQueueTimer = null;
/******************************************************************************/
var CookieEntry = function(cookie) {
this.usedOn = new Set();
this.init(cookie);
};
CookieEntry.prototype.init = function(cookie) {
this.secure = cookie.secure;
this.session = cookie.session;
this.anySubdomain = cookie.domain.charAt(0) === '.';
this.hostname = this.anySubdomain ? cookie.domain.slice(1) : cookie.domain;
this.domain = µm.URI.domainFromHostname(this.hostname) || this.hostname;
this.path = cookie.path;
this.name = cookie.name;
this.value = cookie.value;
this.tstamp = Date.now();
this.usedOn.clear();
return this;
};
const CookieEntry = class {
constructor(cookie) {
this.usedOn = new Set();
this.init(cookie);
}
// Release anything which may consume too much memory
init(cookie) {
this.secure = cookie.secure;
this.session = cookie.session;
this.anySubdomain = cookie.domain.charAt(0) === '.';
this.hostname = this.anySubdomain ? cookie.domain.slice(1) : cookie.domain;
this.domain = µm.URI.domainFromHostname(this.hostname) || this.hostname;
this.path = cookie.path;
this.name = cookie.name;
this.value = cookie.value;
this.tstamp = Date.now();
this.usedOn.clear();
return this;
}
CookieEntry.prototype.dispose = function() {
this.hostname = '';
this.domain = '';
this.path = '';
this.name = '';
this.value = '';
this.usedOn.clear();
return this;
// Reset any property which indirectly consumes memory
dispose() {
this.hostname = '';
this.domain = '';
this.path = '';
this.name = '';
this.value = '';
this.usedOn.clear();
return this;
}
};
/******************************************************************************/
var addCookieToDict = function(cookie) {
var cookieKey = cookieKeyFromCookie(cookie),
cookieEntry = cookieDict.get(cookieKey);
const addCookieToDict = function(cookie) {
const cookieKey = cookieKeyFromCookie(cookie);
let cookieEntry = cookieDict.get(cookieKey);
if ( cookieEntry === undefined ) {
cookieEntry = cookieEntryJunkyard.pop();
if ( cookieEntry ) {
@ -99,17 +100,8 @@ var addCookieToDict = function(cookie) {
/******************************************************************************/
var addCookiesToDict = function(cookies) {
var i = cookies.length;
while ( i-- ) {
addCookieToDict(cookies[i]);
}
};
/******************************************************************************/
var removeCookieFromDict = function(cookieKey) {
var cookieEntry = cookieDict.get(cookieKey);
const removeCookieFromDict = function(cookieKey) {
const cookieEntry = cookieDict.get(cookieKey);
if ( cookieEntry === undefined ) { return false; }
cookieDict.delete(cookieKey);
if ( cookieEntryJunkyard.length < 25 ) {
@ -120,7 +112,7 @@ var removeCookieFromDict = function(cookieKey) {
/******************************************************************************/
var cookieKeyBuilder = [
const cookieKeyBuilder = [
'', // 0 = scheme
'://',
'', // 2 = domain
@ -132,8 +124,8 @@ var cookieKeyBuilder = [
'}'
];
var cookieKeyFromCookie = function(cookie) {
var cb = cookieKeyBuilder;
const cookieKeyFromCookie = function(cookie) {
const cb = cookieKeyBuilder;
cb[0] = cookie.secure ? 'https' : 'http';
cb[2] = cookie.domain.charAt(0) === '.' ? cookie.domain.slice(1) : cookie.domain;
cb[3] = cookie.path;
@ -142,9 +134,9 @@ var cookieKeyFromCookie = function(cookie) {
return cb.join('');
};
var cookieKeyFromCookieURL = function(url, type, name) {
var µmuri = µm.URI.set(url);
var cb = cookieKeyBuilder;
const cookieKeyFromCookieURL = function(url, type, name) {
const µmuri = µm.URI.set(url);
const cb = cookieKeyBuilder;
cb[0] = µmuri.scheme;
cb[2] = µmuri.hostname;
cb[3] = µmuri.path;
@ -155,7 +147,7 @@ var cookieKeyFromCookieURL = function(url, type, name) {
/******************************************************************************/
var cookieURLFromCookieEntry = function(entry) {
const cookieURLFromCookieEntry = function(entry) {
if ( !entry ) {
return '';
}
@ -164,13 +156,11 @@ var cookieURLFromCookieEntry = function(entry) {
/******************************************************************************/
var cookieMatchDomains = function(cookieKey, allHostnamesString) {
var cookieEntry = cookieDict.get(cookieKey);
const cookieMatchDomains = function(cookieKey, allHostnamesString) {
const cookieEntry = cookieDict.get(cookieKey);
if ( cookieEntry === undefined ) { return false; }
if ( allHostnamesString.indexOf(' ' + cookieEntry.hostname + ' ') < 0 ) {
if ( !cookieEntry.anySubdomain ) {
return false;
}
if ( !cookieEntry.anySubdomain ) { return false; }
if ( allHostnamesString.indexOf('.' + cookieEntry.hostname + ' ') < 0 ) {
return false;
}
@ -182,7 +172,7 @@ var cookieMatchDomains = function(cookieKey, allHostnamesString) {
// Look for cookies to record for a specific web page
var recordPageCookiesAsync = function(pageStore) {
const recordPageCookiesAsync = function(pageStore) {
// Store the page stats objects so that it doesn't go away
// before we handle the job.
// rhill 2013-10-19: pageStore could be nil, for example, this can
@ -195,17 +185,17 @@ var recordPageCookiesAsync = function(pageStore) {
/******************************************************************************/
var recordPageCookie = (function() {
let queue = new Map();
const recordPageCookie = (( ) => {
const queue = new Map();
const cookieLogEntryBuilder = [ '', '{', '', '-cookie:', '', '}' ];
let queueTimer;
let cookieLogEntryBuilder = [ '', '{', '', '-cookie:', '', '}' ];
let process = function() {
const process = function() {
queueTimer = undefined;
for ( let qentry of queue ) {
let pageStore = qentry[0];
for ( const qentry of queue ) {
const pageStore = qentry[0];
if ( pageStore.tabId === '' ) { continue; }
for ( let cookieKey of qentry[1] ) {
for ( const cookieKey of qentry[1] ) {
let cookieEntry = cookieDict.get(cookieKey);
if ( cookieEntry === undefined ) { continue; }
let blocked = µm.mustBlock(
@ -221,22 +211,24 @@ var recordPageCookie = (function() {
cookieEntry.session ? 'session' : 'persistent';
cookieLogEntryBuilder[4] =
encodeURIComponent(cookieEntry.name);
let cookieURL = cookieLogEntryBuilder.join('');
const cookieURL = cookieLogEntryBuilder.join('');
pageStore.recordRequest('cookie', cookieURL, blocked);
µm.logger.writeOne({
tabId: pageStore.tabId,
srcHn: pageStore.pageHostname,
desHn: cookieEntry.hostname,
desURL: cookieURL,
type: 'cookie',
blocked
});
if ( µm.logger.enabled ) {
µm.filteringContext
.duplicate()
.fromTabId(pageStore.tabId)
.setType('cookie')
.setURL(cookieURL)
.setFilter(blocked)
.setRealm('network')
.toLogger();
}
cookieEntry.usedOn.add(pageStore.pageHostname);
if ( !blocked ) { continue; }
if ( µm.userSettings.deleteCookies ) {
removeCookieAsync(cookieKey);
}
µm.updateBadgeAsync(pageStore.tabId);
µm.updateToolbarIcon(pageStore.tabId);
}
}
queue.clear();
@ -260,27 +252,29 @@ var recordPageCookie = (function() {
// Candidate for removal
var removeCookieAsync = function(cookieKey) {
const removeCookieAsync = function(cookieKey) {
removeCookieQueue.add(cookieKey);
};
/******************************************************************************/
var chromeCookieRemove = function(cookieEntry, name) {
var url = cookieURLFromCookieEntry(cookieEntry);
const browserCookieRemove = function(cookieEntry, name) {
const url = cookieURLFromCookieEntry(cookieEntry);
if ( url === '' ) { return; }
var sessionCookieKey = cookieKeyFromCookieURL(url, 'session', name);
var persistCookieKey = cookieKeyFromCookieURL(url, 'persistent', name);
var callback = function(details) {
var success = !!details;
var template = success ? i18nCookieDeleteSuccess : i18nCookieDeleteFailure;
const sessionCookieKey = cookieKeyFromCookieURL(url, 'session', name);
const persistCookieKey = cookieKeyFromCookieURL(url, 'persistent', name);
vAPI.cookies.remove({ url, name }).then(details => {
const success = !!details;
const template = success ? i18nCookieDeleteSuccess : i18nCookieDeleteFailure;
if ( removeCookieFromDict(sessionCookieKey) ) {
if ( success ) {
µm.cookieRemovedCounter += 1;
}
µm.logger.writeOne({
info: template.replace('{{value}}', sessionCookieKey)
realm: 'message',
text: template.replace('{{value}}', sessionCookieKey)
});
}
if ( removeCookieFromDict(persistCookieKey) ) {
@ -288,23 +282,22 @@ var chromeCookieRemove = function(cookieEntry, name) {
µm.cookieRemovedCounter += 1;
}
µm.logger.writeOne({
info: template.replace('{{value}}', persistCookieKey)
realm: 'message',
text: template.replace('{{value}}', persistCookieKey)
});
}
};
vAPI.cookies.remove({ url: url, name: name }, callback);
});
};
var i18nCookieDeleteSuccess = vAPI.i18n('loggerEntryCookieDeleted');
var i18nCookieDeleteFailure = vAPI.i18n('loggerEntryDeleteCookieError');
const i18nCookieDeleteSuccess = vAPI.i18n('loggerEntryCookieDeleted');
const i18nCookieDeleteFailure = vAPI.i18n('loggerEntryDeleteCookieError');
/******************************************************************************/
var processPageRecordQueue = function() {
const processPageRecordQueue = function() {
processPageRecordQueueTimer = null;
for ( var pageStore of recordPageCookiesQueue.values() ) {
for ( const pageStore of recordPageCookiesQueue.values() ) {
findAndRecordPageCookies(pageStore);
}
recordPageCookiesQueue.clear();
@ -314,39 +307,36 @@ var processPageRecordQueue = function() {
// Effectively remove cookies.
var processRemoveQueue = function() {
var userSettings = µm.userSettings;
var deleteCookies = userSettings.deleteCookies;
const processRemoveQueue = function() {
const userSettings = µm.userSettings;
const deleteCookies = userSettings.deleteCookies;
// Session cookies which timestamp is *after* tstampObsolete will
// be left untouched
// https://github.com/gorhill/httpswitchboard/issues/257
var tstampObsolete = userSettings.deleteUnusedSessionCookies ?
const tstampObsolete = userSettings.deleteUnusedSessionCookies ?
Date.now() - userSettings.deleteUnusedSessionCookiesAfter * 60 * 1000 :
0;
var srcHostnames;
var cookieEntry;
let srcHostnames;
for ( var cookieKey of removeCookieQueue ) {
for ( const cookieKey of removeCookieQueue ) {
// rhill 2014-05-12: Apparently this can happen. I have to
// investigate how (A session cookie has same name as a
// persistent cookie?)
cookieEntry = cookieDict.get(cookieKey);
const cookieEntry = cookieDict.get(cookieKey);
if ( cookieEntry === undefined ) { continue; }
// Delete obsolete session cookies: enabled.
if ( tstampObsolete !== 0 && cookieEntry.session ) {
if ( cookieEntry.tstamp < tstampObsolete ) {
chromeCookieRemove(cookieEntry, cookieEntry.name);
browserCookieRemove(cookieEntry, cookieEntry.name);
continue;
}
}
// Delete all blocked cookies: disabled.
if ( deleteCookies === false ) {
continue;
}
if ( deleteCookies === false ) { continue; }
// Query scopes only if we are going to use them
if ( srcHostnames === undefined ) {
@ -357,7 +347,7 @@ var processRemoveQueue = function() {
// happen that a cookie is blacklisted on one web page while
// being whitelisted on another (because of per-page permissions).
if ( canRemoveCookie(cookieKey, srcHostnames) ) {
chromeCookieRemove(cookieEntry, cookieEntry.name);
browserCookieRemove(cookieEntry, cookieEntry.name);
}
}
@ -374,12 +364,12 @@ var processRemoveQueue = function() {
// Remove only some of the cookies which are candidate for removal: who knows,
// maybe a user has 1000s of cookies sitting in his browser...
var processClean = function() {
var us = µm.userSettings;
const processClean = function() {
const us = µm.userSettings;
if ( us.deleteCookies || us.deleteUnusedSessionCookies ) {
var cookieKeys = Array.from(cookieDict.keys()),
len = cookieKeys.length,
step, offset, n;
const cookieKeys = Array.from(cookieDict.keys());
const len = cookieKeys.length;
let step, offset, n;
if ( len > 25 ) {
step = len / 25;
offset = Math.floor(Math.random() * len);
@ -389,7 +379,7 @@ var processClean = function() {
offset = 0;
n = len;
}
var i = offset;
let i = offset;
while ( n-- ) {
removeCookieAsync(cookieKeys[Math.floor(i % len)]);
i += step;
@ -401,8 +391,8 @@ var processClean = function() {
/******************************************************************************/
var findAndRecordPageCookies = function(pageStore) {
for ( var cookieKey of cookieDict.keys() ) {
const findAndRecordPageCookies = function(pageStore) {
for ( const cookieKey of cookieDict.keys() ) {
if ( cookieMatchDomains(cookieKey, pageStore.allHostnamesString) ) {
recordPageCookie(pageStore, cookieKey);
}
@ -411,14 +401,13 @@ var findAndRecordPageCookies = function(pageStore) {
/******************************************************************************/
var canRemoveCookie = function(cookieKey, srcHostnames) {
var cookieEntry = cookieDict.get(cookieKey);
const canRemoveCookie = function(cookieKey, srcHostnames) {
const cookieEntry = cookieDict.get(cookieKey);
if ( cookieEntry === undefined ) { return false; }
var cookieHostname = cookieEntry.hostname;
var srcHostname;
const cookieHostname = cookieEntry.hostname;
for ( srcHostname of cookieEntry.usedOn ) {
for ( const srcHostname of cookieEntry.usedOn ) {
if ( µm.mustAllow(srcHostname, cookieHostname, 'cookie') ) {
return false;
}
@ -427,21 +416,17 @@ var canRemoveCookie = function(cookieKey, srcHostnames) {
// For example, if I am logged in into `github.com`, I do not want to be
// logged out just because I did not yet open a `github.com` page after
// re-starting the browser.
srcHostname = cookieHostname;
var pos;
let srcHostname = cookieHostname;
for (;;) {
if ( srcHostnames.has(srcHostname) ) {
if ( µm.mustAllow(srcHostname, cookieHostname, 'cookie') ) {
return false;
}
}
if ( srcHostname === cookieEntry.domain ) {
break;
}
pos = srcHostname.indexOf('.');
if ( pos === -1 ) {
break;
if (
srcHostnames.has(srcHostname) &&
µm.mustAllow(srcHostname, cookieHostname, 'cookie')
) {
return false;
}
if ( srcHostname === cookieEntry.domain ) { break; }
const pos = srcHostname.indexOf('.');
if ( pos === -1 ) { break; }
srcHostname = srcHostname.slice(pos + 1);
}
return true;
@ -454,27 +439,27 @@ var canRemoveCookie = function(cookieKey, srcHostnames) {
// https://github.com/gorhill/httpswitchboard/issues/79
// If cookie value didn't change, no need to record.
vAPI.cookies.onChanged = (function() {
let queue = new Map();
vAPI.cookies.onChanged = (( ) => {
const queue = new Map();
let queueTimer;
// Go through all pages and update if needed, as one cookie can be used
// by many web pages, so they need to be recorded for all these pages.
let process = function() {
const process = function() {
queueTimer = undefined;
let now = Date.now();
let cookieKeys = [];
for ( let qentry of queue ) {
const now = Date.now();
const cookieKeys = [];
for ( const qentry of queue ) {
if ( qentry[1] > now ) { continue; }
if ( cookieDict.has(qentry[0]) === false ) { continue; }
cookieKeys.push(qentry[0]);
queue.delete(qentry[0]);
}
if ( cookieKeys.length !== 0 ) {
for ( let pageStore of µm.pageStores.values() ) {
let allHostnamesString = pageStore.allHostnamesString;
for ( let cookieKey of cookieKeys ) {
for ( const pageStore of µm.pageStores.values() ) {
const allHostnamesString = pageStore.allHostnamesString;
for ( const cookieKey of cookieKeys ) {
if ( cookieMatchDomains(cookieKey, allHostnamesString) ) {
recordPageCookie(pageStore, cookieKey);
}
@ -487,7 +472,7 @@ vAPI.cookies.onChanged = (function() {
};
return function(cookie) {
let cookieKey = cookieKeyFromCookie(cookie);
const cookieKey = cookieKeyFromCookie(cookie);
let cookieEntry = cookieDict.get(cookieKey);
if ( cookieEntry === undefined ) {
cookieEntry = addCookieToDict(cookie);
@ -509,10 +494,11 @@ vAPI.cookies.onChanged = (function() {
// Listen to any change in cookieland, we will update page stats accordingly.
vAPI.cookies.onRemoved = function(cookie) {
var cookieKey = cookieKeyFromCookie(cookie);
const cookieKey = cookieKeyFromCookie(cookie);
if ( removeCookieFromDict(cookieKey) ) {
µm.logger.writeOne({
info: i18nCookieDeleteSuccess.replace('{{value}}', cookieKey),
realm: 'message',
text: i18nCookieDeleteSuccess.replace('{{value}}', cookieKey),
prettify: 'cookie'
});
}
@ -523,10 +509,11 @@ vAPI.cookies.onRemoved = function(cookie) {
// Listen to any change in cookieland, we will update page stats accordingly.
vAPI.cookies.onAllRemoved = function() {
for ( var cookieKey of cookieDict.keys() ) {
for ( const cookieKey of cookieDict.keys() ) {
if ( removeCookieFromDict(cookieKey) ) {
µm.logger.writeOne({
info: i18nCookieDeleteSuccess.replace('{{value}}', cookieKey),
realm: 'message',
text: i18nCookieDeleteSuccess.replace('{{value}}', cookieKey),
prettify: 'cookie'
});
}
@ -535,7 +522,11 @@ vAPI.cookies.onAllRemoved = function() {
/******************************************************************************/
vAPI.cookies.getAll(addCookiesToDict);
vAPI.cookies.getAll().then(cookies => {
for ( const cookie of cookies ) {
addCookieToDict(cookie);
}
});
vAPI.cookies.start();
vAPI.setTimeout(processRemoveQueue, processRemoveQueuePeriod);

133
src/js/dashboard-common.js

@ -1,7 +1,7 @@
/*******************************************************************************
µMatrix - a Chromium browser extension to black/white list requests.
Copyright (C) 2014 Raymond Hill
Copyright (C) 2014 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,22 +19,133 @@
Home: https://github.com/gorhill/uMatrix
*/
/******************************************************************************/
/* global CodeMirror, uDom */
uDom.onLoad(function() {
'use strict';
/******************************************************************************/
// Open links in the proper window
uDom('a').attr('target', '_blank');
uDom('a[href*="dashboard.html"]').attr('target', '_parent');
uDom('.whatisthis').on('click', function() {
uDom(this).parent()
.descendants('.whatisthis-expandable')
.toggleClass('whatisthis-expanded');
});
{
// >>>>> start of local scope
/******************************************************************************/
self.uBlockDashboard = self.uBlockDashboard || {};
/******************************************************************************/
{
let grabFocusTimer;
let grabFocusTarget;
const grabFocus = function() {
grabFocusTarget.focus();
grabFocusTimer = grabFocusTarget = undefined;
};
const grabFocusAsync = function(cm) {
grabFocusTarget = cm;
if ( grabFocusTimer === undefined ) {
grabFocusTimer = vAPI.setTimeout(grabFocus, 1);
}
};
// https://github.com/gorhill/uBlock/issues/3646
const patchSelectAll = function(cm, details) {
var vp = cm.getViewport();
if ( details.ranges.length !== 1 ) { return; }
var range = details.ranges[0],
lineFrom = range.anchor.line,
lineTo = range.head.line;
if ( lineTo === lineFrom ) { return; }
if ( range.head.ch !== 0 ) { lineTo += 1; }
if ( lineFrom !== vp.from || lineTo !== vp.to ) { return; }
details.update([
{
anchor: { line: 0, ch: 0 },
head: { line: cm.lineCount(), ch: 0 }
}
]);
grabFocusAsync(cm);
};
let lastGutterClick = 0;
let lastGutterLine = 0;
const onGutterClicked = function(cm, line) {
const delta = Date.now() - lastGutterClick;
if ( delta >= 500 || line !== lastGutterLine ) {
cm.setSelection(
{ line: line, ch: 0 },
{ line: line + 1, ch: 0 }
);
lastGutterClick = Date.now();
lastGutterLine = line;
} else {
cm.setSelection(
{ line: 0, ch: 0 },
{ line: cm.lineCount(), ch: 0 },
{ scroll: false }
);
lastGutterClick = 0;
}
grabFocusAsync(cm);
};
let resizeTimer,
resizeObserver;
const resize = function(cm) {
resizeTimer = undefined;
const child = document.querySelector('.codeMirrorFillVertical');
if ( child === null ) { return; }
const prect = document.documentElement.getBoundingClientRect();
const crect = child.getBoundingClientRect();
const cssHeight = Math.floor(Math.max(prect.bottom - crect.top, 80)) + 'px';
if ( child.style.height === cssHeight ) { return; }
child.style.height = cssHeight;
// https://github.com/gorhill/uBlock/issues/3694
// Need to call cm.refresh() when resizing occurs. However the
// cursor position may end up outside the viewport, hence we also
// call cm.scrollIntoView() to address this.
// Reference: https://codemirror.net/doc/manual.html#api_sizing
if ( cm instanceof CodeMirror ) {
cm.refresh();
cm.scrollIntoView(null);
}
};
const resizeAsync = function(cm, delay) {
if ( resizeTimer !== undefined ) { return; }
resizeTimer = vAPI.setTimeout(
resize.bind(null, cm),
typeof delay === 'number' ? delay : 66
);
};
self.uBlockDashboard.patchCodeMirrorEditor = function(cm) {
if ( document.querySelector('.codeMirrorFillVertical') !== null ) {
const boundResizeAsync = resizeAsync.bind(null, cm);
window.addEventListener('resize', boundResizeAsync);
resizeObserver = new MutationObserver(boundResizeAsync);
resizeObserver.observe(document.querySelector('.body'), {
childList: true,
subtree: true
});
resizeAsync(cm, 1);
}
if ( cm.options.inputStyle === 'contenteditable' ) {
cm.on('beforeSelectionChange', patchSelectAll);
}
cm.on('gutterClick', onGutterClicked);
};
}
uDom('a').attr('target', '_blank');
uDom('a[href*="dashboard.html"]').attr('target', '_parent');
uDom('.whatisthis').on('click', ev => {
ev.target
.parentElement
.querySelector('.whatisthis-expandable')
.classList.toggle('whatisthis-expanded');
});
// <<<<< end of local scope
}

56
src/js/dashboard.js

@ -23,33 +23,37 @@
'use strict';
{
// >>>>> start of local scope
/******************************************************************************/
(function() {
var loadDashboardPanel = function(hash) {
var button = uDom(hash);
var url = button.attr('data-dashboard-panel-url');
uDom('iframe').attr('src', url);
uDom('.tabButton').forEach(function(button){
button.toggleClass(
'selected',
button.attr('data-dashboard-panel-url') === url
);
});
};
var onTabClickHandler = function() {
loadDashboardPanel(window.location.hash);
};
uDom.onLoad(function() {
window.addEventListener('hashchange', onTabClickHandler);
var hash = window.location.hash;
if ( hash.length < 2 ) {
hash = '#settings';
}
loadDashboardPanel(hash);
const loadDashboardPanel = function(hash) {
const button = uDom(hash);
const url = button.attr('data-dashboard-panel-url');
uDom('iframe').attr('src', url);
uDom('.tabButton').forEach(function(button){
button.toggleClass(
'selected',
button.attr('data-dashboard-panel-url') === url
);
});
};
const onTabClickHandler = function() {
loadDashboardPanel(window.location.hash);
};
uDom.onLoad(function() {
window.addEventListener('hashchange', onTabClickHandler);
let hash = window.location.hash;
if ( hash.length < 2 ) {
hash = '#settings';
}
loadDashboardPanel(hash);
});
/******************************************************************************/
})();
// <<<<< end of local scope
}

310
src/js/filtering-context.js

@ -0,0 +1,310 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2018-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
/******************************************************************************/
µMatrix.FilteringContext = function(other) {
if ( other instanceof µMatrix.FilteringContext ) {
return this.fromFilteringContext(other);
}
this.tstamp = 0;
this.realm = '';
this.id = undefined;
this.type = undefined;
this.url = undefined;
this.aliasURL = undefined;
this.hostname = undefined;
this.domain = undefined;
this.docId = undefined;
this.docOrigin = undefined;
this.docHostname = undefined;
this.docDomain = undefined;
this.tabId = undefined;
this.tabOrigin = undefined;
this.tabHostname = undefined;
this.tabDomain = undefined;
this.filter = undefined;
};
µMatrix.FilteringContext.prototype = {
requestTypeNormalizer: {
font : 'css',
image : 'image',
imageset : 'image',
main_frame : 'doc',
media : 'media',
object : 'media',
other : 'other',
script : 'script',
stylesheet : 'css',
sub_frame : 'frame',
websocket : 'fetch',
xmlhttprequest: 'fetch'
},
fromTabId: function(tabId) {
const tabContext = µMatrix.tabContextManager.mustLookup(tabId);
this.tabOrigin = tabContext.origin;
this.tabHostname = tabContext.rootHostname;
this.tabDomain = tabContext.rootDomain;
this.tabId = tabContext.tabId;
return this;
},
// https://github.com/uBlockOrigin/uBlock-issues/issues/459
// In case of a request for frame and if ever no context is specified,
// assume the origin of the context is the same as the request itself.
fromWebrequestDetails: function(details) {
this.type = this.requestTypeNormalizer[details.type] || 'other';
const tabId = details.tabId;
if ( tabId > 0 && this.type === 'doc' ) {
µMatrix.tabContextManager.push(tabId, details.url);
}
this.fromTabId(tabId);
this.realm = '';
this.id = details.requestId;
this.setURL(details.url);
this.aliasURL = details.aliasURL || undefined;
this.docId = this.type !== 'frame'
? details.frameId
: details.parentFrameId;
if ( this.tabId > 0 ) {
if ( this.docId === 0 ) {
this.docOrigin = this.tabOrigin;
this.docHostname = this.tabHostname;
this.docDomain = this.tabDomain;
} else if ( details.documentUrl !== undefined ) {
this.setDocOriginFromURL(details.documentUrl);
} else {
this.setDocOrigin(this.tabOrigin);
}
} else if ( details.documentUrl !== undefined ) {
const origin = this.originFromURI(
µMatrix.normalizePageURL(0, details.documentUrl)
);
this.setDocOrigin(origin).setTabOrigin(origin);
} else if (
this.docId === -1 ||
this.type === 'doc' ||
this.type === 'frame'
) {
const origin = this.originFromURI(this.url);
this.setDocOrigin(origin).setTabOrigin(origin);
} else {
this.setDocOrigin(this.tabOrigin);
}
this.filter = undefined;
return this;
},
fromFilteringContext: function(other) {
this.realm = other.realm;
this.type = other.type;
this.url = other.url;
this.hostname = other.hostname;
this.domain = other.domain;
this.docId = other.docId;
this.docOrigin = other.docOrigin;
this.docHostname = other.docHostname;
this.docDomain = other.docDomain;
this.tabId = other.tabId;
this.tabOrigin = other.tabOrigin;
this.tabHostname = other.tabHostname;
this.tabDomain = other.tabDomain;
this.filter = undefined;
return this;
},
duplicate: function() {
return (new µMatrix.FilteringContext(this));
},
setRealm: function(a) {
this.realm = a;
return this;
},
setType: function(a) {
this.type = a;
return this;
},
setURL: function(a) {
if ( a !== this.url ) {
this.hostname = this.domain = undefined;
this.url = a;
}
return this;
},
getHostname: function() {
if ( this.hostname === undefined ) {
this.hostname = this.hostnameFromURI(this.url);
}
return this.hostname;
},
setHostname: function(a) {
if ( a !== this.hostname ) {
this.domain = undefined;
this.hostname = a;
}
return this;
},
getDomain: function() {
if ( this.domain === undefined ) {
this.domain = this.domainFromHostname(this.getHostname());
}
return this.domain;
},
setDomain: function(a) {
this.domain = a;
return this;
},
getDocOrigin: function() {
if ( this.docOrigin === undefined ) {
this.docOrigin = this.tabOrigin;
}
return this.docOrigin;
},
setDocOrigin: function(a) {
if ( a !== this.docOrigin ) {
this.docHostname = this.docDomain = undefined;
this.docOrigin = a;
}
return this;
},
setDocOriginFromURL: function(a) {
return this.setDocOrigin(this.originFromURI(a));
},
getDocHostname: function() {
if ( this.docHostname === undefined ) {
this.docHostname = this.hostnameFromURI(this.getDocOrigin());
}
return this.docHostname;
},
setDocHostname: function(a) {
if ( a !== this.docHostname ) {
this.docDomain = undefined;
this.docHostname = a;
}
return this;
},
getDocDomain: function() {
if ( this.docDomain === undefined ) {
this.docDomain = this.domainFromHostname(this.getDocHostname());
}
return this.docDomain;
},
setDocDomain: function(a) {
this.docDomain = a;
return this;
},
// The idea is to minimize the amout of work done to figure out whether
// the resource is 3rd-party to the document.
is3rdPartyToDoc: function() {
let docDomain = this.getDocDomain();
if ( docDomain === '' ) { docDomain = this.docHostname; }
if ( this.domain !== undefined && this.domain !== '' ) {
return this.domain !== docDomain;
}
const hostname = this.getHostname();
if ( hostname.endsWith(docDomain) === false ) { return true; }
const i = hostname.length - docDomain.length;
if ( i === 0 ) { return false; }
return hostname.charCodeAt(i - 1) !== 0x2E /* '.' */;
},
setTabId: function(a) {
this.tabId = a;
return this;
},
getTabOrigin: function() {
if ( this.tabOrigin === undefined ) {
const tabContext = µMatrix.tabContextManager.mustLookup(this.tabId);
this.tabOrigin = tabContext.origin;
this.tabHostname = tabContext.rootHostname;
this.tabDomain = tabContext.rootDomain;
}
return this.tabOrigin;
},
setTabOrigin: function(a) {
if ( a !== this.tabOrigin ) {
this.tabHostname = this.tabDomain = undefined;
this.tabOrigin = a;
}
return this;
},
setTabOriginFromURL: function(a) {
return this.setTabOrigin(this.originFromURI(a));
},
getTabHostname: function() {
if ( this.tabHostname === undefined ) {
this.tabHostname = this.hostnameFromURI(this.getTabOrigin());
}
return this.tabHostname;
},
setTabHostname: function(a) {
if ( a !== this.tabHostname ) {
this.tabDomain = undefined;
this.tabHostname = a;
}
return this;
},
getTabDomain: function() {
if ( this.tabDomain === undefined ) {
this.tabDomain = this.domainFromHostname(this.getTabHostname());
}
return this.tabDomain;
},
setTabDomain: function(a) {
this.docDomain = a;
return this;
},
// The idea is to minimize the amout of work done to figure out whether
// the resource is 3rd-party to the top document.
is3rdPartyToTab: function() {
let tabDomain = this.getTabDomain();
if ( tabDomain === '' ) { tabDomain = this.tabHostname; }
if ( this.domain !== undefined && this.domain !== '' ) {
return this.domain !== tabDomain;
}
const hostname = this.getHostname();
if ( hostname.endsWith(tabDomain) === false ) { return true; }
const i = hostname.length - tabDomain.length;
if ( i === 0 ) { return false; }
return hostname.charCodeAt(i - 1) !== 0x2E /* '.' */;
},
setFilter: function(a) {
this.filter = a;
return this;
},
toLogger: function() {
this.tstamp = Date.now();
if ( this.domain === undefined ) {
void this.getDomain();
}
if ( this.docDomain === undefined ) {
void this.getDocDomain();
}
if ( this.tabDomain === undefined ) {
void this.getTabDomain();
}
µMatrix.logger.writeOne(this);
},
originFromURI: µMatrix.URI.originFromURI,
hostnameFromURI: vAPI.hostnameFromURI,
domainFromHostname: vAPI.domainFromHostname,
};
µMatrix.filteringContext = new µMatrix.FilteringContext();

760
src/js/hntrie.js

@ -0,0 +1,760 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2017-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* globals WebAssembly */
'use strict';
// *****************************************************************************
// start of local namespace
{
/******************************************************************************/
// The directory from which the current script was fetched should
// also contain the related WASM file. The script is fetched from
// a trusted location, and consequently so will be the related
// WASM file.
let workingDir = '';
{
const url = new URL(document.currentScript.src);
const match = /[^\/]+$/.exec(url.pathname);
if ( match !== null ) {
url.pathname = url.pathname.slice(0, match.index);
}
workingDir = url.href;
}
/*******************************************************************************
The original prototype was to develop an idea I had about using jump indices
in a TypedArray for quickly matching hostnames (or more generally strings)[1].
Once I had a working, un-optimized prototype, I realized I had ended up
with something formally named a "trie": <https://en.wikipedia.org/wiki/Trie>,
hence the name. I have no idea whether the implementation here or one
resembling it has been done elsewhere.
"HN" in HNTrieContainer stands for "HostName", because the trie is
specialized to deal with matching hostnames -- which is a bit more
complicated than matching plain strings.
For example, `www.abc.com` is deemed matching `abc.com`, because the former
is a subdomain of the latter. The opposite is of course not true.
The resulting read-only tries created as a result of using HNTrieContainer
are simply just typed arrays filled with integers. The matching algorithm is
just a matter of reading/comparing these integers, and further using them as
indices in the array as a way to move around in the trie.
[1] To solve <https://github.com/gorhill/uBlock/issues/3193>
Since this trie is specialized for matching hostnames, the stored
strings are reversed internally, because of hostname comparison logic:
Correct matching:
index 0123456
abc.com
|
www.abc.com
index 01234567890
Incorrect matching (typically used for plain strings):
index 0123456
abc.com
|
www.abc.com
index 01234567890
------------------------------------------------------------------------------
1st iteration:
- https://github.com/gorhill/uBlock/blob/ff58107dac3a32607f8113e39ed5015584506813/src/js/hntrie.js
- Suitable for small to medium set of hostnames
- One buffer per trie
2nd iteration: goal was to make matches() method wasm-able
- https://github.com/gorhill/uBlock/blob/c3b0fd31f64bd7ffecdd282fb1208fe07aac3eb0/src/js/hntrie.js
- Suitable for small to medium set of hostnames
- Distinct tries all share same buffer:
- Reduced memory footprint
- https://stackoverflow.com/questions/45803829/memory-overhead-of-typed-arrays-vs-strings/45808835#45808835
- Reusing needle character lookups for all tries
- This significantly reduce the number of String.charCodeAt() calls
- Slightly improved creation time
This is the 3rd iteration: goal was to make add() method wasm-able and
further improve memory/CPU efficiency.
This 3rd iteration has the following new traits:
- Suitable for small to large set of hostnames
- Support multiple trie containers (instanciable)
- Designed to hold large number of hostnames
- Hostnames can be added at any time (instead of all at once)
- This means pre-sorting is no longer a requirement
- The trie is always compact
- There is no longer a need for a `vacuum` method
- This makes the add() method wasm-able
- It can return the exact hostname which caused the match
- serializable/unserializable available for fast loading
- Distinct trie reference support the iteration protocol, thus allowing
to extract all the hostnames in the trie
Its primary purpose is to replace the use of Set() as a mean to hold
large number of hostnames (ex. FilterHostnameDict in static filtering
engine).
A HNTrieContainer is mostly a large buffer in which distinct but related
tries are stored. The memory layout of the buffer is as follow:
0-254: needle being processed
255: length of needle
256-259: offset to start of trie data section (=> trie0)
260-263: offset to end of trie data section (=> trie1)
264-267: offset to start of character data section (=> char0)
268-271: offset to end of character data section (=> char1)
272: start of trie data section
*/
const PAGE_SIZE = 65536;
// i32 / i8
const TRIE0_SLOT = 256 >>> 2; // 64 / 256
const TRIE1_SLOT = TRIE0_SLOT + 1; // 65 / 260
const CHAR0_SLOT = TRIE0_SLOT + 2; // 66 / 264
const CHAR1_SLOT = TRIE0_SLOT + 3; // 67 / 268
const TRIE0_START = TRIE0_SLOT + 4 << 2; // 272
const HNTrieContainer = class {
constructor(details) {
if ( details instanceof Object === false ) { details = {}; }
let len = (details.byteLength || 0) + PAGE_SIZE-1 & ~(PAGE_SIZE-1);
this.buf = new Uint8Array(Math.max(len, 131072));
this.buf32 = new Uint32Array(this.buf.buffer);
this.needle = '';
this.buf32[TRIE0_SLOT] = TRIE0_START;
this.buf32[TRIE1_SLOT] = this.buf32[TRIE0_SLOT];
this.buf32[CHAR0_SLOT] = details.char0 || 65536;
this.buf32[CHAR1_SLOT] = this.buf32[CHAR0_SLOT];
this.wasmInstancePromise = null;
this.wasmMemory = null;
}
//--------------------------------------------------------------------------
// Public methods
//--------------------------------------------------------------------------
reset() {
this.buf32[TRIE1_SLOT] = this.buf32[TRIE0_SLOT];
this.buf32[CHAR1_SLOT] = this.buf32[CHAR0_SLOT];
}
setNeedle(needle) {
if ( needle !== this.needle ) {
const buf = this.buf;
let i = needle.length;
if ( i > 255 ) { i = 255; }
buf[255] = i;
while ( i-- ) {
buf[i] = needle.charCodeAt(i);
}
this.needle = needle;
}
return this;
}
matchesJS(iroot) {
const buf32 = this.buf32;
const buf8 = this.buf;
const char0 = buf32[CHAR0_SLOT];
let ineedle = buf8[255];
let icell = buf32[iroot+0];
if ( icell === 0 ) { return -1; }
for (;;) {
if ( ineedle === 0 ) { return -1; }
ineedle -= 1;
let c = buf8[ineedle];
let v, i0;
// find first segment with a first-character match
for (;;) {
v = buf32[icell+2];
i0 = char0 + (v & 0x00FFFFFF);
if ( buf8[i0] === c ) { break; }
icell = buf32[icell+0];
if ( icell === 0 ) { return -1; }
}
// all characters in segment must match
let n = v >>> 24;
if ( n > 1 ) {
n -= 1;
if ( n > ineedle ) { return -1; }
i0 += 1;
const i1 = i0 + n;
do {
ineedle -= 1;
if ( buf8[i0] !== buf8[ineedle] ) { return -1; }
i0 += 1;
} while ( i0 < i1 );
}
// next segment
icell = buf32[icell+1];
if ( icell === 0 ) { break; }
if ( buf32[icell+2] === 0 ) {
if ( ineedle === 0 || buf8[ineedle-1] === 0x2E ) {
return ineedle;
}
icell = buf32[icell+1];
}
}
return ineedle === 0 || buf8[ineedle-1] === 0x2E ? ineedle : -1;
}
createOne(args) {
if ( Array.isArray(args) ) {
return new this.HNTrieRef(this, ...args);
}
// grow buffer if needed
if ( (this.buf32[CHAR0_SLOT] - this.buf32[TRIE1_SLOT]) < 12 ) {
this.growBuf(12, 0);
}
const iroot = this.buf32[TRIE1_SLOT] >>> 2;
this.buf32[TRIE1_SLOT] += 12;
this.buf32[iroot+0] = 0;
this.buf32[iroot+1] = 0;
this.buf32[iroot+2] = 0;
return new this.HNTrieRef(this, iroot, 0, 0);
}
compileOne(trieRef) {
return [
trieRef.iroot,
trieRef.addCount,
trieRef.addedCount,
];
}
addJS(iroot) {
let lhnchar = this.buf[255];
if ( lhnchar === 0 ) { return 0; }
// grow buffer if needed
if (
(this.buf32[CHAR0_SLOT] - this.buf32[TRIE1_SLOT]) < 24 ||
(this.buf.length - this.buf32[CHAR1_SLOT]) < 256
) {
this.growBuf(24, 256);
}
let icell = this.buf32[iroot+0];
// special case: first node in trie
if ( icell === 0 ) {
this.buf32[iroot+0] = this.addCell(0, 0, this.addSegment(lhnchar));
return 1;
}
//
const char0 = this.buf32[CHAR0_SLOT];
let inext;
// find a matching cell: move down
for (;;) {
const vseg = this.buf32[icell+2];
// skip boundary cells
if ( vseg === 0 ) {
// remainder is at label boundary? if yes, no need to add
// the rest since the shortest match is always reported
if ( this.buf[lhnchar-1] === 0x2E /* '.' */ ) { return -1; }
icell = this.buf32[icell+1];
continue;
}
let isegchar0 = char0 + (vseg & 0x00FFFFFF);
// if first character is no match, move to next descendant
if ( this.buf[isegchar0] !== this.buf[lhnchar-1] ) {
inext = this.buf32[icell+0];
if ( inext === 0 ) {
this.buf32[icell+0] = this.addCell(0, 0, this.addSegment(lhnchar));
return 1;
}
icell = inext;
continue;
}
// 1st character was tested
let isegchar = 1;
lhnchar -= 1;
// find 1st mismatch in rest of segment
const lsegchar = vseg >>> 24;
if ( lsegchar !== 1 ) {
for (;;) {
if ( isegchar === lsegchar ) { break; }
if ( lhnchar === 0 ) { break; }
if ( this.buf[isegchar0+isegchar] !== this.buf[lhnchar-1] ) { break; }
isegchar += 1;
lhnchar -= 1;
}
}
// all segment characters matched
if ( isegchar === lsegchar ) {
inext = this.buf32[icell+1];
// needle remainder: no
if ( lhnchar === 0 ) {
// boundary cell already present
if ( inext === 0 || this.buf32[inext+2] === 0 ) { return 0; }
// need boundary cell
this.buf32[icell+1] = this.addCell(0, inext, 0);
}
// needle remainder: yes
else {
if ( inext !== 0 ) {
icell = inext;
continue;
}
// remainder is at label boundary? if yes, no need to add
// the rest since the shortest match is always reported
if ( this.buf[lhnchar-1] === 0x2E /* '.' */ ) { return -1; }
// boundary cell + needle remainder
inext = this.addCell(0, 0, 0);
this.buf32[icell+1] = inext;
this.buf32[inext+1] = this.addCell(0, 0, this.addSegment(lhnchar));
}
}
// some segment characters matched
else {
// split current cell
isegchar0 -= char0;
this.buf32[icell+2] = isegchar << 24 | isegchar0;
inext = this.addCell(
0,
this.buf32[icell+1],
lsegchar - isegchar << 24 | isegchar0 + isegchar
);
this.buf32[icell+1] = inext;
// needle remainder: no = need boundary cell
if ( lhnchar === 0 ) {
this.buf32[icell+1] = this.addCell(0, inext, 0);
}
// needle remainder: yes = need new cell for remaining characters
else {
this.buf32[inext+0] = this.addCell(0, 0, this.addSegment(lhnchar));
}
}
return 1;
}
}
optimize() {
this.shrinkBuf();
return {
byteLength: this.buf.byteLength,
char0: this.buf32[CHAR0_SLOT],
};
}
fromIterable(hostnames, add) {
if ( add === undefined ) { add = 'add'; }
const trieRef = this.createOne();
for ( const hn of hostnames ) {
trieRef[add](hn);
}
return trieRef;
}
serialize(encoder) {
if ( encoder instanceof Object ) {
return encoder.encode(
this.buf32.buffer,
this.buf32[CHAR1_SLOT]
);
}
return Array.from(
new Uint32Array(
this.buf32.buffer,
0,
this.buf32[CHAR1_SLOT] + 3 >>> 2
)
);
}
unserialize(selfie, decoder) {
this.needle = '';
const shouldDecode = typeof selfie === 'string';
let byteLength = shouldDecode
? decoder.decodeSize(selfie)
: selfie.length << 2;
if ( byteLength === 0 ) { return false; }
byteLength = byteLength + PAGE_SIZE-1 & ~(PAGE_SIZE-1);
if ( this.wasmMemory !== null ) {
const pageCountBefore = this.buf.length >>> 16;
const pageCountAfter = byteLength >>> 16;
if ( pageCountAfter > pageCountBefore ) {
this.wasmMemory.grow(pageCountAfter - pageCountBefore);
this.buf = new Uint8Array(this.wasmMemory.buffer);
this.buf32 = new Uint32Array(this.buf.buffer);
}
} else if ( byteLength > this.buf.length ) {
this.buf = new Uint8Array(byteLength);
this.buf32 = new Uint32Array(this.buf.buffer);
}
if ( shouldDecode ) {
decoder.decode(selfie, this.buf.buffer);
} else {
this.buf32.set(selfie);
}
return true;
}
//--------------------------------------------------------------------------
// Private methods
//--------------------------------------------------------------------------
addCell(idown, iright, v) {
let icell = this.buf32[TRIE1_SLOT];
this.buf32[TRIE1_SLOT] = icell + 12;
icell >>>= 2;
this.buf32[icell+0] = idown;
this.buf32[icell+1] = iright;
this.buf32[icell+2] = v;
return icell;
}
addSegment(lsegchar) {
if ( lsegchar === 0 ) { return 0; }
let char1 = this.buf32[CHAR1_SLOT];
const isegchar = char1 - this.buf32[CHAR0_SLOT];
let i = lsegchar;
do {
this.buf[char1++] = this.buf[--i];
} while ( i !== 0 );
this.buf32[CHAR1_SLOT] = char1;
return (lsegchar << 24) | isegchar;
}
growBuf(trieGrow, charGrow) {
const char0 = Math.max(
(this.buf32[TRIE1_SLOT] + trieGrow + PAGE_SIZE-1) & ~(PAGE_SIZE-1),
this.buf32[CHAR0_SLOT]
);
const char1 = char0 + this.buf32[CHAR1_SLOT] - this.buf32[CHAR0_SLOT];
const bufLen = Math.max(
(char1 + charGrow + PAGE_SIZE-1) & ~(PAGE_SIZE-1),
this.buf.length
);
this.resizeBuf(bufLen, char0);
}
shrinkBuf() {
// Can't shrink WebAssembly.Memory
if ( this.wasmMemory !== null ) { return; }
const char0 = this.buf32[TRIE1_SLOT] + 24;
const char1 = char0 + this.buf32[CHAR1_SLOT] - this.buf32[CHAR0_SLOT];
const bufLen = char1 + 256;
this.resizeBuf(bufLen, char0);
}
resizeBuf(bufLen, char0) {
bufLen = bufLen + PAGE_SIZE-1 & ~(PAGE_SIZE-1);
if (
bufLen === this.buf.length &&
char0 === this.buf32[CHAR0_SLOT]
) {
return;
}
const charDataLen = this.buf32[CHAR1_SLOT] - this.buf32[CHAR0_SLOT];
if ( this.wasmMemory !== null ) {
const pageCount = (bufLen >>> 16) - (this.buf.byteLength >>> 16);
if ( pageCount > 0 ) {
this.wasmMemory.grow(pageCount);
this.buf = new Uint8Array(this.wasmMemory.buffer);
this.buf32 = new Uint32Array(this.wasmMemory.buffer);
}
} else if ( bufLen !== this.buf.length ) {
const newBuf = new Uint8Array(bufLen);
newBuf.set(
new Uint8Array(
this.buf.buffer,
0,
this.buf32[TRIE1_SLOT]
),
0
);
newBuf.set(
new Uint8Array(
this.buf.buffer,
this.buf32[CHAR0_SLOT],
charDataLen
),
char0
);
this.buf = newBuf;
this.buf32 = new Uint32Array(this.buf.buffer);
this.buf32[CHAR0_SLOT] = char0;
this.buf32[CHAR1_SLOT] = char0 + charDataLen;
}
if ( char0 !== this.buf32[CHAR0_SLOT] ) {
this.buf.set(
new Uint8Array(
this.buf.buffer,
this.buf32[CHAR0_SLOT],
charDataLen
),
char0
);
this.buf32[CHAR0_SLOT] = char0;
this.buf32[CHAR1_SLOT] = char0 + charDataLen;
}
}
async initWASM() {
const module = await HNTrieContainer.enableWASM();
if ( module instanceof WebAssembly.Module === false ) { return false; }
if ( this.wasmInstancePromise !== null ) {
return true;
}
const memory = new WebAssembly.Memory({ initial: 2 });
this.wasmInstancePromise = WebAssembly.instantiate(
module,
{
imports: {
memory,
growBuf: this.growBuf.bind(this, 24, 256)
}
}
);
const instance = await this.wasmInstancePromise;
this.wasmMemory = memory;
const curPageCount = memory.buffer.byteLength >>> 16;
const newPageCount = this.buf.byteLength + PAGE_SIZE-1 >>> 16;
if ( newPageCount > curPageCount ) {
memory.grow(newPageCount - curPageCount);
}
const buf = new Uint8Array(memory.buffer);
buf.set(this.buf);
this.buf = buf;
this.buf32 = new Uint32Array(this.buf.buffer);
this.matches = this.matchesWASM = instance.exports.matches;
this.add = this.addWASM = instance.exports.add;
return true;
}
// Code below is to attempt to load a WASM module which implements:
//
// - HNTrieContainer.add()
// - HNTrieContainer.matches()
//
// The WASM module is entirely optional, the JS implementations will be
// used should the WASM module be unavailable for whatever reason.
static async enableWASM() {
if ( HNTrieContainer.wasmModulePromise === undefined ) {
HNTrieContainer.wasmModulePromise = null;
if (
typeof WebAssembly !== 'object' ||
typeof WebAssembly.compileStreaming !== 'function'
) {
return null;
}
// Soft-dependency on vAPI so that the code here can be used
// outside of uMatrix (i.e. tests, benchmarks)
if ( typeof vAPI === 'object' && vAPI.canWASM !== true ) {
return null;
}
// The wasm module will work only if CPU is natively little-endian,
// as we use native uint32 array in our js code.
const uint32s = new Uint32Array(1);
const uint8s = new Uint8Array(uint32s.buffer);
uint32s[0] = 1;
if ( uint8s[0] !== 1 ) { return null; }
HNTrieContainer.wasmModulePromise = fetch(
workingDir + 'wasm/hntrie.wasm',
{ mode: 'same-origin' }
).then(
WebAssembly.compileStreaming
);
}
if ( HNTrieContainer.wasmModulePromise === null ) { return null; }
let module = null;
try {
module = await HNTrieContainer.wasmModulePromise;
} catch(ex) {
HNTrieContainer.wasmModulePromise = null;
}
return module;
}
};
HNTrieContainer.prototype.matches = HNTrieContainer.prototype.matchesJS;
HNTrieContainer.prototype.matchesWASM = null;
HNTrieContainer.prototype.add = HNTrieContainer.prototype.addJS;
HNTrieContainer.prototype.addWASM = null;
/*******************************************************************************
Class to hold reference to a specific trie
*/
HNTrieContainer.prototype.HNTrieRef = class {
constructor(container, iroot, addCount, addedCount) {
this.container = container;
this.iroot = iroot;
this.addCount = addCount;
this.addedCount = addedCount;
this.needle = '';
this.last = -1;
}
add(hn) {
this.addCount += 1;
if ( this.container.setNeedle(hn).add(this.iroot) > 0 ) {
this.last = -1;
this.needle = '';
this.addedCount += 1;
return true;
}
return false;
}
addJS(hn) {
this.addCount += 1;
if ( this.container.setNeedle(hn).addJS(this.iroot) > 0 ) {
this.last = -1;
this.needle = '';
this.addedCount += 1;
return true;
}
return false;
}
addWASM(hn) {
this.addCount += 1;
if ( this.container.setNeedle(hn).addWASM(this.iroot) > 0 ) {
this.last = -1;
this.needle = '';
this.addedCount += 1;
return true;
}
return false;
}
matches(needle) {
if ( needle !== this.needle ) {
this.needle = needle;
this.last = this.container.setNeedle(needle).matches(this.iroot);
}
return this.last;
}
matchesJS(needle) {
if ( needle !== this.needle ) {
this.needle = needle;
this.last = this.container.setNeedle(needle).matchesJS(this.iroot);
}
return this.last;
}
matchesWASM(needle) {
if ( needle !== this.needle ) {
this.needle = needle;
this.last = this.container.setNeedle(needle).matchesWASM(this.iroot);
}
return this.last;
}
dump() {
let hostnames = Array.from(this);
if ( String.prototype.padStart instanceof Function ) {
const maxlen = Math.min(
hostnames.reduce((maxlen, hn) => Math.max(maxlen, hn.length), 0),
64
);
hostnames = hostnames.map(hn => hn.padStart(maxlen));
}
for ( const hn of hostnames ) {
console.log(hn);
}
}
[Symbol.iterator]() {
return {
value: undefined,
done: false,
next: function() {
if ( this.icell === 0 ) {
if ( this.forks.length === 0 ) {
this.value = undefined;
this.done = true;
return this;
}
this.charPtr = this.forks.pop();
this.icell = this.forks.pop();
}
for (;;) {
const idown = this.container.buf32[this.icell+0];
if ( idown !== 0 ) {
this.forks.push(idown, this.charPtr);
}
const v = this.container.buf32[this.icell+2];
let i0 = this.container.buf32[CHAR0_SLOT] + (v & 0x00FFFFFF);
const i1 = i0 + (v >>> 24);
while ( i0 < i1 ) {
this.charPtr -= 1;
this.charBuf[this.charPtr] = this.container.buf[i0];
i0 += 1;
}
this.icell = this.container.buf32[this.icell+1];
if ( this.icell === 0 ) {
return this.toHostname();
}
if ( this.container.buf32[this.icell+2] === 0 ) {
this.icell = this.container.buf32[this.icell+1];
return this.toHostname();
}
}
},
toHostname: function() {
this.value = this.textDecoder.decode(
new Uint8Array(this.charBuf.buffer, this.charPtr)
);
return this;
},
container: this.container,
icell: this.iroot,
charBuf: new Uint8Array(256),
charPtr: 256,
forks: [],
textDecoder: new TextDecoder()
};
}
};
HNTrieContainer.prototype.HNTrieRef.prototype.last = -1;
HNTrieContainer.prototype.HNTrieRef.prototype.needle = '';
/******************************************************************************/
µMatrix.HNTrieContainer = HNTrieContainer;
// end of local namespace
// *****************************************************************************
}

200
src/js/hosts-files.js

@ -25,18 +25,19 @@
/******************************************************************************/
(function() {
{
// >>>>> start of local scope
/******************************************************************************/
var listDetails = {},
lastUpdateTemplateString = vAPI.i18n('hostsFilesLastUpdate'),
hostsFilesSettingsHash,
reValidExternalList = /^[a-z-]+:\/\/\S*\/\S+$/m;
const lastUpdateTemplateString = vAPI.i18n('hostsFilesLastUpdate');
const reValidExternalList = /^[a-z-]+:\/\/\S*\/\S+$/m;
let listDetails = {};
let hostsFilesSettingsHash;
/******************************************************************************/
vAPI.messaging.addListener(function onMessage(msg) {
vAPI.broadcastListener.add(msg => {
switch ( msg.what ) {
case 'assetUpdated':
updateAssetStatus(msg);
@ -58,27 +59,27 @@ vAPI.messaging.addListener(function onMessage(msg) {
/******************************************************************************/
var renderNumber = function(value) {
const renderNumber = function(value) {
return value.toLocaleString();
};
/******************************************************************************/
var renderHostsFiles = function(soft) {
var listEntryTemplate = uDom('#templates .listEntry'),
listStatsTemplate = vAPI.i18n('hostsFilesPerFileStats'),
renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString,
reExternalHostFile = /^https?:/;
const renderHostsFiles = function(soft) {
const listEntryTemplate = uDom('#templates .listEntry');
const listStatsTemplate = vAPI.i18n('hostsFilesPerFileStats');
const renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString;
const reExternalHostFile = /^https?:/;
// Assemble a pretty list name if possible
var listNameFromListKey = function(collection, listKey) {
const listNameFromListKey = function(collection, listKey) {
let list = collection.get(listKey);
return list && list.title || listKey;
};
var liFromListEntry = function(collection, listKey, li) {
var entry = collection.get(listKey),
elem;
const liFromListEntry = function(collection, listKey, li) {
const entry = collection.get(listKey);
let elem;
if ( !li ) {
li = listEntryTemplate.clone().nodeAt(0);
}
@ -106,7 +107,7 @@ var renderHostsFiles = function(soft) {
elem.checked = entry.selected === true;
}
elem = li.querySelector('span.counts');
var text = '';
let text = '';
if ( !isNaN(+entry.entryUsedCount) && !isNaN(+entry.entryCount) ) {
text = listStatsTemplate
.replace('{{used}}', renderNumber(entry.selected ? entry.entryUsedCount : 0))
@ -114,8 +115,8 @@ var renderHostsFiles = function(soft) {
}
elem.textContent = text;
// https://github.com/chrisaljoudi/uBlock/issues/104
var asset = listDetails.cache[listKey] || {};
var remoteURL = asset.remoteURL;
const asset = listDetails.cache[listKey] || {};
const remoteURL = asset.remoteURL;
li.classList.toggle(
'unsecure',
typeof remoteURL === 'string' && remoteURL.lastIndexOf('http:', 0) === 0
@ -132,50 +133,50 @@ var renderHostsFiles = function(soft) {
li.classList.remove('discard');
return li;
};
var onRenderAssetFiles = function(collection, listSelector) {
const onRenderAssetFiles = function(collection, listSelector) {
// Incremental rendering: this will allow us to easily discard unused
// DOM list entries.
uDom(listSelector + ' .listEntry:not(.notAnAsset)').addClass('discard');
var assetKeys = Array.from(collection.keys());
const assetKeys = Array.from(collection.keys());
// Sort works this way:
// - Send /^https?:/ items at the end (custom hosts file URL)
assetKeys.sort(function(a, b) {
let ea = collection.get(a),
eb = collection.get(b);
const ea = collection.get(a);
const eb = collection.get(b);
if ( ea.submitter !== eb.submitter ) {
return ea.submitter !== 'user' ? -1 : 1;
}
let ta = ea.title || a,
tb = eb.title || b;
const ta = ea.title || a;
const tb = eb.title || b;
if ( reExternalHostFile.test(ta) === reExternalHostFile.test(tb) ) {
return ta.localeCompare(tb);
}
return reExternalHostFile.test(tb) ? -1 : 1;
});
let ulList = document.querySelector(listSelector),
liLast = ulList.querySelector('.notAnAsset');
const ulList = document.querySelector(listSelector);
const liLast = ulList.querySelector('.notAnAsset');
for ( let i = 0; i < assetKeys.length; i++ ) {
let liReuse = i < ulList.childElementCount ?
ulList.children[i] :
null;
let liReuse = i < ulList.childElementCount
? ulList.children[i]
: null;
if (
liReuse !== null &&
liReuse.classList.contains('notAnAsset')
) {
liReuse = null;
}
let liEntry = liFromListEntry(collection, assetKeys[i], liReuse);
const liEntry = liFromListEntry(collection, assetKeys[i], liReuse);
if ( liEntry.parentElement === null ) {
ulList.insertBefore(liEntry, liLast);
}
}
};
var onAssetDataReceived = function(details) {
const onAssetDataReceived = function(details) {
// Preprocess.
details.hosts = new Map(details.hosts);
details.recipes = new Map(details.recipes);
@ -213,16 +214,16 @@ var renderHostsFiles = function(soft) {
renderWidgets();
};
vAPI.messaging.send(
'hosts-files.js',
{ what: 'getAssets' },
onAssetDataReceived
);
vAPI.messaging.send('dashboard', {
what: 'getAssets',
}).then(details => {
onAssetDataReceived(details);
});
};
/******************************************************************************/
var renderWidgets = function() {
const renderWidgets = function() {
uDom('#buttonUpdate').toggleClass('disabled', document.querySelector('body:not(.updating) .assets .listEntry.obsolete > input[type="checkbox"]:checked') === null);
uDom('#buttonPurgeAll').toggleClass('disabled', document.querySelector('.assets .listEntry.cached') === null);
uDom('#buttonApply').toggleClass('disabled', hostsFilesSettingsHash === hashFromCurrentFromSettings());
@ -230,8 +231,8 @@ var renderWidgets = function() {
/******************************************************************************/
var updateAssetStatus = function(details) {
var li = document.querySelector('.assets .listEntry[data-listkey="' + details.key + '"]');
const updateAssetStatus = function(details) {
const li = document.querySelector('.assets .listEntry[data-listkey="' + details.key + '"]');
if ( li === null ) { return; }
li.classList.toggle('failed', !!details.failed);
li.classList.toggle('obsolete', !details.cached);
@ -255,12 +256,12 @@ var updateAssetStatus = function(details) {
**/
var hashFromCurrentFromSettings = function() {
let listHash = [],
listEntries = document.querySelectorAll(
'.assets .listEntry[data-listkey]:not(.toRemove)'
);
for ( let liEntry of listEntries ) {
const hashFromCurrentFromSettings = function() {
const listHash = [];
const listEntries = document.querySelectorAll(
'.assets .listEntry[data-listkey]:not(.toRemove)'
);
for ( const liEntry of listEntries ) {
if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) {
listHash.push(liEntry.getAttribute('data-listkey'));
}
@ -289,7 +290,7 @@ var hashFromCurrentFromSettings = function() {
/******************************************************************************/
var textFromTextarea = function(textarea) {
const textFromTextarea = function(textarea) {
if ( typeof textarea === 'string' ) {
textarea = document.querySelector(textarea);
}
@ -298,15 +299,15 @@ var textFromTextarea = function(textarea) {
/******************************************************************************/
var onHostsFilesSettingsChanged = function() {
const onHostsFilesSettingsChanged = function() {
renderWidgets();
};
/******************************************************************************/
var onRemoveExternalAsset = function(ev) {
var liEntry = uDom(this).ancestors('[data-listkey]'),
listKey = liEntry.attr('data-listkey');
const onRemoveExternalAsset = function(ev) {
const liEntry = uDom(this).ancestors('[data-listkey]');
const listKey = liEntry.attr('data-listkey');
if ( listKey ) {
liEntry.toggleClass('toRemove');
renderWidgets();
@ -316,13 +317,16 @@ var onRemoveExternalAsset = function(ev) {
/******************************************************************************/
var onPurgeClicked = function() {
var button = uDom(this),
liEntry = button.ancestors('[data-listkey]'),
listKey = liEntry.attr('data-listkey');
const onPurgeClicked = function(ev) {
const button = uDom(ev.target);
const liEntry = button.ancestors('[data-listkey]');
const listKey = liEntry.attr('data-listkey');
if ( !listKey ) { return; }
vAPI.messaging.send('hosts-files.js', { what: 'purgeCache', assetKey: listKey });
vAPI.messaging.send('dashboard', {
what: 'purgeCache',
assetKey: listKey,
});
liEntry.addClass('obsolete');
liEntry.removeClass('cached');
@ -333,9 +337,9 @@ var onPurgeClicked = function() {
/******************************************************************************/
var selectAssets = function(callback) {
var prepareChanges = function(listSelector) {
var out = {
const selectAssets = function() {
const prepareChanges = function(listSelector) {
const out = {
toSelect: [],
toImport: '',
toRemove: [],
@ -345,13 +349,13 @@ var selectAssets = function(callback) {
}
};
let root = document.querySelector(listSelector);
const root = document.querySelector(listSelector);
// Lists to select or remove
let liEntries = root.querySelectorAll(
const liEntries = root.querySelectorAll(
'.listEntry[data-listkey]:not(.notAnAsset)'
);
for ( let liEntry of liEntries ) {
for ( const liEntry of liEntries ) {
if ( liEntry.classList.contains('toRemove') ) {
out.toRemove.push(liEntry.getAttribute('data-listkey'));
} else if ( liEntry.querySelector('input[type="checkbox"]:checked') ) {
@ -360,11 +364,11 @@ var selectAssets = function(callback) {
}
// External hosts files to import
let input = root.querySelector(
const input = root.querySelector(
'.toImport > input[type="checkbox"]:checked'
);
if ( input !== null ) {
let textarea = root.querySelector('.toImport textarea');
const textarea = root.querySelector('.toImport textarea');
out.toImport = textarea.value.trim();
textarea.value = '';
input.checked = false;
@ -379,29 +383,26 @@ var selectAssets = function(callback) {
return out;
};
vAPI.messaging.send(
'hosts-files.js',
{
what: 'selectAssets',
hosts: prepareChanges('#hosts'),
recipes: prepareChanges('#recipes')
},
callback
);
hostsFilesSettingsHash = hashFromCurrentFromSettings();
return vAPI.messaging.send('dashboard', {
what: 'selectAssets',
hosts: prepareChanges('#hosts'),
recipes: prepareChanges('#recipes'),
});
};
/******************************************************************************/
var buttonApplyHandler = function() {
const buttonApplyHandler = function() {
uDom('#buttonApply').removeClass('enabled');
selectAssets(function(response) {
if ( response && response.hostsChanged ) {
vAPI.messaging.send('hosts-files.js', { what: 'reloadHostsFiles' });
selectAssets().then(response => {
if ( response instanceof Object === false ) { return; }
if ( response.hostsChanged ) {
vAPI.messaging.send('dashboard', { what: 'reloadHostsFiles' });
}
if ( response && response.recipesChanged ) {
vAPI.messaging.send('hosts-files.js', { what: 'reloadRecipeFiles' });
if ( response.recipesChanged ) {
vAPI.messaging.send('dashboard', { what: 'reloadRecipeFiles' });
}
});
renderWidgets();
@ -409,11 +410,11 @@ var buttonApplyHandler = function() {
/******************************************************************************/
var buttonUpdateHandler = function() {
const buttonUpdateHandler = function() {
uDom('#buttonUpdate').removeClass('enabled');
selectAssets(function() {
selectAssets().then(( ) => {
document.body.classList.add('updating');
vAPI.messaging.send('hosts-files.js', { what: 'forceUpdateAssets' });
vAPI.messaging.send('dashboard', { what: 'forceUpdateAssets' });
renderWidgets();
});
renderWidgets();
@ -421,28 +422,23 @@ var buttonUpdateHandler = function() {
/******************************************************************************/
var buttonPurgeAllHandler = function() {
const buttonPurgeAllHandler = function() {
uDom('#buttonPurgeAll').removeClass('enabled');
vAPI.messaging.send(
'hosts-files.js',
{ what: 'purgeAllCaches' },
function() {
renderHostsFiles(true);
}
);
vAPI.messaging.send('dashboard', {
what: 'purgeAllCaches',
}).then(( ) => {
renderHostsFiles(true);
});
};
/******************************************************************************/
var autoUpdateCheckboxChanged = function() {
vAPI.messaging.send(
'hosts-files.js',
{
what: 'userSettings',
name: 'autoUpdate',
value: this.checked
}
);
const autoUpdateCheckboxChanged = function(ev) {
vAPI.messaging.send('dashboard', {
what: 'userSettings',
name: 'autoUpdate',
value: ev.target.checked,
});
};
/******************************************************************************/
@ -460,5 +456,5 @@ renderHostsFiles();
/******************************************************************************/
})();
// <<<<< end of local scope
}

9
src/js/httpsb.js

@ -19,14 +19,12 @@
Home: https://github.com/gorhill/uMatrix
*/
/* global chrome, µMatrix */
'use strict';
/******************************************************************************/
(function() {
var µm = µMatrix;
{
const µm = µMatrix;
µm.pMatrix = new µm.Matrix();
µm.pMatrix.setSwitch('matrix-off', 'about-scheme', 1);
µm.pMatrix.setSwitch('matrix-off', 'chrome-extension-scheme', 1);
@ -42,6 +40,7 @@
// Global rules
µm.pMatrix.setSwitch('referrer-spoof', '*', 1);
µm.pMatrix.setSwitch('noscript-spoof', '*', 1);
µm.pMatrix.setSwitch('cname-reveal', '*', 1);
µm.pMatrix.setCell('*', '*', '*', µm.Matrix.Red);
µm.pMatrix.setCell('*', '*', 'css', µm.Matrix.Green);
µm.pMatrix.setCell('*', '*', 'image', µm.Matrix.Green);
@ -52,7 +51,7 @@
µm.tMatrix = new µm.Matrix();
µm.tMatrix.assign(µm.pMatrix);
})();
}
/******************************************************************************/

228
src/js/i18n.js

@ -19,16 +19,15 @@
Home: https://github.com/gorhill/uMatrix
*/
/* global vAPI, uDom */
'use strict';
/******************************************************************************/
// This file should always be included at the end of the `body` tag, so as
// to ensure all i18n targets are already loaded.
(function() {
'use strict';
{
// >>>>> start of local scope
/******************************************************************************/
@ -41,28 +40,18 @@
// No HTML entities are allowed, there is code to handle existing HTML
// entities already present in translation files until they are all gone.
var reSafeTags = /^([\s\S]*?)<(b|blockquote|code|em|i|kbd|span|sup)>(.+?)<\/\2>([\s\S]*)$/,
reSafeInput = /^([\s\S]*?)<(input type="[^"]+")>(.*?)([\s\S]*)$/,
reInput = /^input type=(['"])([a-z]+)\1$/,
reSafeLink = /^([\s\S]*?)<(a href=['"]https?:\/\/[^'" <>]+['"])>(.+?)<\/a>([\s\S]*)$/,
reLink = /^a href=(['"])(https?:\/\/[^'"]+)\1$/;
const reSafeTags = /^([\s\S]*?)<(b|code|em|i|span)>(.+?)<\/\2>([\s\S]*)$/;
const reSafeLink = /^([\s\S]*?)<(a href=['"]https:\/\/[^'" <>]+['"])>(.+?)<\/a>([\s\S]*)$/;
const reLink = /^a href=(['"])(https:\/\/[^'"]+)\1$/;
var safeTextToTagNode = function(text) {
var matches, node;
const safeTextToTagNode = function(text) {
if ( text.lastIndexOf('a ', 0) === 0 ) {
matches = reLink.exec(text);
const matches = reLink.exec(text);
if ( matches === null ) { return null; }
node = document.createElement('a');
const node = document.createElement('a');
node.setAttribute('href', matches[2]);
return node;
}
if ( text.lastIndexOf('input ', 0) === 0 ) {
matches = reInput.exec(text);
if ( matches === null ) { return null; }
node = document.createElement('input');
node.setAttribute('type', matches[2]);
return node;
}
// Firefox extension validator warns if using a variable as argument for
// document.createElement().
switch ( text ) {
@ -87,92 +76,162 @@ var safeTextToTagNode = function(text) {
}
};
var safeTextToTextNode = function(text) {
// TODO: remove once no more HTML entities in translation files.
if ( text.indexOf('&') !== -1 ) {
text = text.replace(/&ldquo;/g, '“')
.replace(/&rdquo;/g, '”')
.replace(/&lsquo;/g, '‘')
.replace(/&rsquo;/g, '’');
}
return document.createTextNode(text);
};
const safeTextToTextNode = (( ) => {
const entities = new Map([
// TODO: Remove quote entities once no longer present in translation
// files. Other entities must stay.
[ '&ldquo;', '“' ],
[ '&rdquo;', '”' ],
[ '&lsquo;', '‘' ],
[ '&rsquo;', '’' ],
[ '&lt;', '<' ],
[ '&gt;', '>' ],
]);
const decodeEntities = match => {
return entities.get(match) || match;
};
return function(text) {
if ( text.indexOf('&') !== -1 ) {
text = text.replace(/&[a-z]+;/g, decodeEntities);
}
return document.createTextNode(text);
};
})();
var safeTextToDOM = function(text, parent) {
const safeTextToDOM = function(text, parent) {
if ( text === '' ) { return; }
// Fast path (most common).
if ( text.indexOf('<') === -1 ) {
return parent.appendChild(safeTextToTextNode(text));
parent.appendChild(safeTextToTextNode(text));
return;
}
// Slow path.
// `<p>` no longer allowed. Code below can be remove once all <p>'s are
// `<p>` no longer allowed. Code below can be removed once all <p>'s are
// gone from translation files.
text = text.replace(/^<p>|<\/p>/g, '')
.replace(/<p>/g, '\n\n');
// Parse allowed HTML tags.
var matches,
matches1 = reSafeTags.exec(text),
matches2 = reSafeLink.exec(text);
if ( matches1 !== null && matches2 !== null ) {
matches = matches1.index < matches2.index ? matches1 : matches2;
} else if ( matches1 !== null ) {
matches = matches1;
} else if ( matches2 !== null ) {
matches = matches2;
} else {
matches = reSafeInput.exec(text);
}
let matches = reSafeTags.exec(text);
if ( matches === null ) {
parent.appendChild(safeTextToTextNode(text));
return;
matches = reSafeLink.exec(text);
if ( matches === null ) {
parent.appendChild(safeTextToTextNode(text));
return;
}
}
safeTextToDOM(matches[1], parent);
var node = safeTextToTagNode(matches[2]) || parent;
const fragment = document.createDocumentFragment();
safeTextToDOM(matches[1], fragment);
let node = safeTextToTagNode(matches[2]);
safeTextToDOM(matches[3], node);
parent.appendChild(node);
safeTextToDOM(matches[4], parent);
fragment.appendChild(node);
safeTextToDOM(matches[4], fragment);
parent.appendChild(fragment);
};
/******************************************************************************/
vAPI.i18n.safeTemplateToDOM = function(id, dict, parent) {
if ( parent === undefined ) {
parent = document.createDocumentFragment();
}
let textin = vAPI.i18n(id);
if ( textin === '' ) {
return parent;
}
if ( textin.indexOf('{{') === -1 ) {
safeTextToDOM(textin, parent);
return parent;
}
const re = /\{\{\w+\}\}/g;
let textout = '';
for (;;) {
let match = re.exec(textin);
if ( match === null ) {
textout += textin;
break;
}
textout += textin.slice(0, match.index);
let prop = match[0].slice(2, -2);
if ( dict.hasOwnProperty(prop) ) {
textout += dict[prop].replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
} else {
textout += prop;
}
textin = textin.slice(re.lastIndex);
}
safeTextToDOM(textout, parent);
return parent;
};
/******************************************************************************/
// Helper to deal with the i18n'ing of HTML files.
vAPI.i18n.render = function(context) {
var docu = document,
root = context || docu,
elems, n, i, elem, text;
elems = root.querySelectorAll('[data-i18n]');
n = elems.length;
for ( i = 0; i < n; i++ ) {
elem = elems[i];
text = vAPI.i18n(elem.getAttribute('data-i18n'));
const docu = document;
const root = context || docu;
for ( const elem of root.querySelectorAll('[data-i18n]') ) {
let text = vAPI.i18n(elem.getAttribute('data-i18n'));
if ( !text ) { continue; }
// TODO: remove once it's all replaced with <input type="...">
if ( text.indexOf('{') !== -1 ) {
text = text.replace(/\{\{input:([^}]+)\}\}/g, '<input type="$1">');
if ( text.indexOf('{{') === -1 ) {
safeTextToDOM(text, elem);
continue;
}
// Handle selector-based placeholders: these placeholders tell where
// existing child DOM element are to be positioned relative to the
// localized text nodes.
const parts = text.split(/(\{\{[^}]+\}\})/);
const fragment = document.createDocumentFragment();
let textBefore = '';
for ( let part of parts ) {
if ( part === '' ) { continue; }
if ( part.startsWith('{{') && part.endsWith('}}') ) {
// TODO: remove detection of ':' once it no longer appears
// in translation files.
const pos = part.indexOf(':');
if ( pos !== -1 ) {
part = part.slice(0, pos) + part.slice(-2);
}
const node = elem.querySelector(part.slice(2, -2));
if ( node !== null ) {
safeTextToDOM(textBefore, fragment);
fragment.appendChild(node);
textBefore = '';
continue;
}
}
textBefore += part;
}
if ( textBefore !== '' ) {
safeTextToDOM(textBefore, fragment);
}
safeTextToDOM(text, elem);
elem.appendChild(fragment);
}
uDom('[title]', context).forEach(function(elem) {
var title = vAPI.i18n(elem.attr('title'));
if ( title ) {
elem.attr('title', title);
}
});
uDom('[placeholder]', context).forEach(function(elem) {
elem.attr('placeholder', vAPI.i18n(elem.attr('placeholder')));
});
uDom('[data-i18n-tip]', context).forEach(function(elem) {
elem.attr(
'data-tip',
vAPI.i18n(elem.attr('data-i18n-tip'))
.replace(/<br>/g, '\n')
.replace(/\n{3,}/g, '\n\n')
for ( const elem of root.querySelectorAll('[data-i18n-title]') ) {
const text = vAPI.i18n(elem.getAttribute('data-i18n-title'));
if ( !text ) { continue; }
elem.setAttribute('title', text);
}
for ( const elem of root.querySelectorAll('[placeholder]') ) {
elem.setAttribute(
'placeholder',
vAPI.i18n(elem.getAttribute('placeholder'))
);
});
}
for ( const elem of root.querySelectorAll('[data-i18n-tip]') ) {
const text = vAPI.i18n(elem.getAttribute('data-i18n-tip'))
.replace(/<br>/g, '\n')
.replace(/\n{3,}/g, '\n\n');
elem.setAttribute('data-tip', text);
if ( elem.getAttribute('aria-label') === 'data-tip' ) {
elem.setAttribute('aria-label', text);
}
}
};
vAPI.i18n.render();
@ -180,7 +239,7 @@ vAPI.i18n.render();
/******************************************************************************/
vAPI.i18n.renderElapsedTimeToString = function(tstamp) {
var value = (Date.now() - tstamp) / 60000;
let value = (Date.now() - tstamp) / 60000;
if ( value < 2 ) {
return vAPI.i18n('elapsedOneMinuteAgo');
}
@ -203,6 +262,7 @@ vAPI.i18n.renderElapsedTimeToString = function(tstamp) {
/******************************************************************************/
})();
// <<<<< end of local scope
}
/******************************************************************************/

4
src/js/liquid-dict.js

@ -23,7 +23,7 @@
/******************************************************************************/
µMatrix.LiquidDict = (function() {
µMatrix.LiquidDict = (( ) => {
/******************************************************************************/
@ -164,7 +164,7 @@ LiquidDict.prototype.reset = function() {
/******************************************************************************/
let selfieVersion = 1;
const selfieVersion = 1;
LiquidDict.prototype.toSelfie = function() {
this.freeze();

2722
src/js/logger-ui.js
File diff suppressed because it is too large
View File

36
src/js/logger.js

@ -1,6 +1,6 @@
/*******************************************************************************
uMatrix - a browser extension to block requests.
uBlock Origin - a browser extension to block requests.
Copyright (C) 2015-present Raymond Hill
This program is free software: you can redistribute it and/or modify
@ -26,58 +26,57 @@
µMatrix.logger = (function() {
let LogEntry = function(details) {
this.init(details);
};
LogEntry.prototype.init = function(details) {
this.tstamp = Date.now();
this.details = JSON.stringify(details);
};
let buffer = null;
let lastReadTime = 0;
let writePtr = 0;
// After 60 seconds without being read, a buffer will be considered
// unused, and thus removed from memory.
let logBufferObsoleteAfter = 30 * 1000;
const logBufferObsoleteAfter = 30 * 1000;
let janitor = function() {
const janitor = ( ) => {
if (
buffer !== null &&
lastReadTime < (Date.now() - logBufferObsoleteAfter)
) {
api.enabled = false;
buffer = null;
writePtr = 0;
api.ownerId = undefined;
api.enabled = false;
vAPI.messaging.broadcast({ what: 'loggerDisabled' });
}
if ( buffer !== null ) {
vAPI.setTimeout(janitor, logBufferObsoleteAfter);
}
};
let api = {
const boxEntry = function(details) {
if ( details.tstamp === undefined ) {
details.tstamp = Date.now();
}
return JSON.stringify(details);
};
const api = {
enabled: false,
ownerId: undefined,
writeOne: function(details) {
if ( buffer === null ) { return; }
if ( writePtr === buffer.length ) {
buffer.push(new LogEntry(details));
buffer.push(boxEntry(details));
} else {
buffer[writePtr].init(details);
buffer[writePtr] = boxEntry(details);
}
writePtr += 1;
},
readAll: function(ownerId) {
this.ownerId = ownerId;
this.enabled = true;
if ( buffer === null ) {
this.enabled = true;
buffer = [];
vAPI.setTimeout(janitor, logBufferObsoleteAfter);
}
let out = buffer.slice(0, writePtr);
const out = buffer.slice(0, writePtr);
writePtr = 0;
lastReadTime = Date.now();
return out;
@ -85,6 +84,7 @@
};
return api;
})();
/******************************************************************************/

210
src/js/lz4.js

@ -0,0 +1,210 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2018-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* global lz4BlockCodec */
'use strict';
/*******************************************************************************
Experimental support for storage compression.
For background information on the topic, see:
https://github.com/uBlockOrigin/uBlock-issues/issues/141#issuecomment-407737186
**/
µMatrix.lz4Codec = (function() { // >>>> Start of private namespace
/******************************************************************************/
let lz4CodecInstance;
let pendingInitialization;
let textEncoder, textDecoder;
let ttlCount = 0;
let ttlTimer;
let ttlDelay = 60000;
const init = function() {
ttlDelay = µMatrix.rawSettings.autoUpdateAssetFetchPeriod * 1000 + 15000;
if ( lz4CodecInstance === null ) {
return Promise.resolve(null);
}
if ( lz4CodecInstance !== undefined ) {
return Promise.resolve(lz4CodecInstance);
}
if ( pendingInitialization === undefined ) {
let flavor;
if ( µMatrix.rawSettings.disableWebAssembly === true ) {
flavor = 'js';
}
pendingInitialization = lz4BlockCodec.createInstance(flavor)
.then(instance => {
lz4CodecInstance = instance;
pendingInitialization = undefined;
});
}
return pendingInitialization;
};
// We can't shrink memory usage of lz4 codec instances, and in the
// current case memory usage can grow to a significant amount given
// that a single contiguous memory buffer is required to accommodate
// both input and output data. Thus a time-to-live implementation
// which will cause the wasm instance to be forgotten after enough
// time elapse without the instance being used.
const destroy = function() {
//if ( lz4CodecInstance !== undefined ) {
// console.info(
// 'uBO: freeing lz4-block-codec instance (%s KB)',
// lz4CodecInstance.bytesInUse() >>> 10
// );
//}
lz4CodecInstance = undefined;
textEncoder = textDecoder = undefined;
ttlCount = 0;
ttlTimer = undefined;
};
const ttlManage = function(count) {
if ( ttlTimer !== undefined ) {
clearTimeout(ttlTimer);
ttlTimer = undefined;
}
ttlCount += count;
if ( ttlCount > 0 ) { return; }
if ( lz4CodecInstance === null ) { return; }
ttlTimer = vAPI.setTimeout(destroy, ttlDelay);
};
const uint8ArrayFromBlob = function(key, data) {
if ( data instanceof Blob === false ) {
return Promise.resolve({ key, data });
}
return new Promise(resolve => {
let blobReader = new FileReader();
blobReader.onloadend = ev => {
resolve({ key, data: new Uint8Array(ev.target.result) });
};
blobReader.readAsArrayBuffer(data);
});
};
const encodeValue = function(key, value) {
if ( !lz4CodecInstance ) { return; }
//let t0 = window.performance.now();
if ( textEncoder === undefined ) {
textEncoder = new TextEncoder();
}
let inputArray = textEncoder.encode(value);
let inputSize = inputArray.byteLength;
let outputArray = lz4CodecInstance.encodeBlock(inputArray, 8);
if ( outputArray instanceof Uint8Array === false ) { return; }
outputArray[0] = 0x18;
outputArray[1] = 0x4D;
outputArray[2] = 0x22;
outputArray[3] = 0x04;
outputArray[4] = (inputSize >>> 0) & 0xFF;
outputArray[5] = (inputSize >>> 8) & 0xFF;
outputArray[6] = (inputSize >>> 16) & 0xFF;
outputArray[7] = (inputSize >>> 24) & 0xFF;
//console.info(
// 'uBO: [%s] compressed %d KB => %d KB (%s%%) in %s ms',
// key,
// inputArray.byteLength >> 10,
// outputArray.byteLength >> 10,
// (outputArray.byteLength / inputArray.byteLength * 100).toFixed(0),
// (window.performance.now() - t0).toFixed(1)
//);
return outputArray;
};
const decodeValue = function(key, inputArray) {
if ( !lz4CodecInstance ) { return; }
//let t0 = window.performance.now();
if (
inputArray[0] !== 0x18 || inputArray[1] !== 0x4D ||
inputArray[2] !== 0x22 || inputArray[3] !== 0x04
) {
return;
}
let outputSize =
(inputArray[4] << 0) | (inputArray[5] << 8) |
(inputArray[6] << 16) | (inputArray[7] << 24);
let outputArray = lz4CodecInstance.decodeBlock(inputArray, 8, outputSize);
if ( outputArray instanceof Uint8Array === false ) { return; }
if ( textDecoder === undefined ) {
textDecoder = new TextDecoder();
}
let value = textDecoder.decode(outputArray);
//console.info(
// 'uBO: [%s] decompressed %d KB => %d KB (%s%%) in %s ms',
// key,
// inputArray.byteLength >>> 10,
// outputSize >>> 10,
// (inputArray.byteLength / outputSize * 100).toFixed(0),
// (window.performance.now() - t0).toFixed(1)
//);
return value;
};
return {
encode: function(key, dataIn) {
if ( typeof dataIn !== 'string' || dataIn.length < 4096 ) {
return Promise.resolve({ key, data: dataIn });
}
ttlManage(1);
return init().then(( ) => {
ttlManage(-1);
let dataOut = encodeValue(key, dataIn) || dataIn;
if ( dataOut instanceof Uint8Array ) {
dataOut = new Blob([ dataOut ]);
}
return { key, data: dataOut || dataIn };
});
},
decode: function(key, dataIn) {
if ( dataIn instanceof Blob === false ) {
return Promise.resolve({ key, data: dataIn });
}
ttlManage(1);
return Promise.all([
init(),
uint8ArrayFromBlob(key, dataIn)
]).then(results => {
ttlManage(-1);
let result = results[1];
return {
key: result.key,
data: decodeValue(result.key, result.data) || result.data
};
});
},
relinquish: function() {
ttlDelay = 1;
ttlManage(0);
},
};
/******************************************************************************/
})(); // <<<< End of private namespace

56
src/js/main-blocked.js

@ -25,17 +25,18 @@
/******************************************************************************/
(function() {
{
// >>>>> start of local scope
/******************************************************************************/
let details = {};
(function() {
let matches = /details=([^&]+)/.exec(window.location.search);
(( ) => {
const matches = /details=([^&]+)/.exec(window.location.search);
if ( matches === null ) { return; }
try {
details = JSON.parse(atob(matches[1]));
details = JSON.parse(decodeURIComponent(matches[1]));
} catch(ex) {
}
})();
@ -51,15 +52,15 @@ uDom('.what').text(details.url);
// Code below originally imported from:
// https://github.com/gorhill/uBlock/blob/master/src/js/document-blocked.js
(function() {
let reURL = /^https?:\/\//;
(( ) => {
const reURL = /^https?:\/\//;
let liFromParam = function(name, value) {
const liFromParam = function(name, value) {
if ( value === '' ) {
value = name;
name = '';
}
let li = document.createElement('li');
const li = document.createElement('li');
let span = document.createElement('span');
span.textContent = name;
li.appendChild(span);
@ -68,7 +69,7 @@ uDom('.what').text(details.url);
}
span = document.createElement('span');
if ( reURL.test(value) ) {
let a = document.createElement('a');
const a = document.createElement('a');
a.href = a.textContent = value;
span.appendChild(a);
} else {
@ -78,7 +79,7 @@ uDom('.what').text(details.url);
return li;
};
let safeDecodeURIComponent = function(s) {
const safeDecodeURIComponent = function(s) {
try {
s = decodeURIComponent(s);
} catch (ex) {
@ -86,30 +87,30 @@ uDom('.what').text(details.url);
return s;
};
let renderParams = function(parentNode, rawURL) {
let a = document.createElement('a');
const renderParams = function(parentNode, rawURL) {
const a = document.createElement('a');
a.href = rawURL;
if ( a.search.length === 0 ) { return false; }
let pos = rawURL.indexOf('?');
let li = liFromParam(
const pos = rawURL.indexOf('?');
const li = liFromParam(
vAPI.i18n('mainBlockedNoParamsPrompt'),
rawURL.slice(0, pos)
);
parentNode.appendChild(li);
let params = a.search.slice(1).split('&');
for ( var i = 0; i < params.length; i++ ) {
let param = params[i];
const params = a.search.slice(1).split('&');
for ( let i = 0; i < params.length; i++ ) {
const param = params[i];
let pos = param.indexOf('=');
if ( pos === -1 ) {
pos = param.length;
}
let name = safeDecodeURIComponent(param.slice(0, pos));
let value = safeDecodeURIComponent(param.slice(pos + 1));
li = liFromParam(name, value);
const name = safeDecodeURIComponent(param.slice(0, pos));
const value = safeDecodeURIComponent(param.slice(pos + 1));
const li = liFromParam(name, value);
if ( reURL.test(value) ) {
let ul = document.createElement('ul');
const ul = document.createElement('ul');
renderParams(ul, value);
li.appendChild(ul);
}
@ -118,17 +119,17 @@ uDom('.what').text(details.url);
return true;
};
let hasParams = renderParams(uDom.nodeFromId('parsed'), details.url);
const hasParams = renderParams(uDom.nodeFromId('parsed'), details.url);
if ( hasParams === false ) { return; }
let theURLNode = document.getElementById('theURL');
const theURLNode = document.getElementById('theURL');
theURLNode.classList.add('hasParams');
theURLNode.classList.toggle(
'collapsed',
vAPI.localStorage.getItem('document-blocked-collapse-url') === 'true'
);
let toggleCollapse = function() {
const toggleCollapse = function() {
vAPI.localStorage.setItem(
'document-blocked-collapse-url',
theURLNode.classList.toggle('collapsed').toString()
@ -163,8 +164,8 @@ vAPI.messaging.send('main-blocked.js', {
what: 'mustBlock',
scope: details.hn,
hostname: details.hn,
type: details.type
}, response => {
type: details.type,
}).then(response => {
if ( response === false ) {
window.location.replace(details.url);
}
@ -172,6 +173,7 @@ vAPI.messaging.send('main-blocked.js', {
/******************************************************************************/
})();
// <<<<< end of local scope
}
/******************************************************************************/

310
src/js/matrix.js

@ -25,16 +25,16 @@
/******************************************************************************/
µMatrix.Matrix = (function() {
µMatrix.Matrix = (( ) => {
/******************************************************************************/
var µm = µMatrix;
var selfieVersion = 1;
const µm = µMatrix;
const selfieVersion = 1;
/******************************************************************************/
var Matrix = function() {
const Matrix = function() {
this.reset();
this.sourceRegister = '';
this.decomposedSourceRegister = [''];
@ -60,7 +60,7 @@ Matrix.GrayIndirect = Matrix.Gray | Matrix.Indirect;
/******************************************************************************/
var typeBitOffsets = new Map([
const typeBitOffsets = new Map([
[ '*', 0 ],
[ 'doc', 2 ],
[ 'cookie', 4 ],
@ -68,49 +68,50 @@ var typeBitOffsets = new Map([
[ 'image', 8 ],
[ 'media', 10 ],
[ 'script', 12 ],
[ 'xhr', 14 ],
[ 'fetch', 14 ],
[ 'frame', 16 ],
[ 'other', 18 ]
[ 'other', 18 ],
]);
var stateToNameMap = new Map([
const stateToNameMap = new Map([
[ 1, 'block' ],
[ 2, 'allow' ],
[ 3, 'inherit' ]
[ 3, 'inherit' ],
]);
var nameToStateMap = {
const nameToStateMap = {
'block': 1,
'allow': 2,
'noop': 2,
'inherit': 3
'inherit': 3,
};
var switchBitOffsets = new Map([
const switchBitOffsets = new Map([
[ 'matrix-off', 0 ],
[ 'https-strict', 2 ],
/* 4 is now unused, formerly assigned to UA spoofing */
[ 'referrer-spoof', 6 ],
[ 'noscript-spoof', 8 ],
[ 'no-workers', 10 ]
[ 'no-workers', 10 ],
[ 'cname-reveal', 12 ],
]);
var switchStateToNameMap = new Map([
const switchStateToNameMap = new Map([
[ 1, 'true' ],
[ 2, 'false' ]
[ 2, 'false' ],
]);
var nameToSwitchStateMap = new Map([
const nameToSwitchStateMap = new Map([
[ 'true', 1 ],
[ 'false', 2 ]
[ 'false', 2 ],
]);
/******************************************************************************/
Matrix.columnHeaderIndices = (function() {
var out = new Map(),
i = 0;
for ( var type of typeBitOffsets.keys() ) {
Matrix.columnHeaderIndices = (( ) => {
const out = new Map();
let i = 0;
for ( const type of typeBitOffsets.keys() ) {
out.set(type, i++);
}
return out;
@ -122,14 +123,14 @@ Matrix.switchNames = new Set(switchBitOffsets.keys());
/******************************************************************************/
// For performance purpose, as simple tests as possible
var reHostnameVeryCoarse = /[g-z_-]/;
var reIPv4VeryCoarse = /\.\d+$/;
const reHostnameVeryCoarse = /[g-z_-]/;
const reIPv4VeryCoarse = /\.\d+$/;
// http://tools.ietf.org/html/rfc5952
// 4.3: "MUST be represented in lowercase"
// Also: http://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers
var isIPAddress = function(hostname) {
const isIPAddress = function(hostname) {
if ( reHostnameVeryCoarse.test(hostname) ) {
return false;
}
@ -141,19 +142,19 @@ var isIPAddress = function(hostname) {
/******************************************************************************/
var punycodeIf = function(hn) {
const punycodeIf = function(hn) {
return reNotASCII.test(hn) ? punycode.toASCII(hn) : hn;
};
var unpunycodeIf = function(hn) {
const unpunycodeIf = function(hn) {
return hn.indexOf('xn--') !== -1 ? punycode.toUnicode(hn) : hn;
};
var reNotASCII = /[^\x20-\x7F]/;
const reNotASCII = /[^\x20-\x7F]/;
/******************************************************************************/
var toBroaderHostname = function(hostname) {
const toBroaderHostname = function(hostname) {
if ( hostname === '*' ) { return ''; }
if ( isIPAddress(hostname) ) {
return toBroaderIPAddress(hostname);
@ -165,12 +166,12 @@ var toBroaderHostname = function(hostname) {
return hostname.slice(pos + 1);
};
var toBroaderIPAddress = function(ipaddress) {
const toBroaderIPAddress = function(ipaddress) {
// Can't broaden IPv6 (for now)
if ( ipaddress.charAt(0) === '[' ) {
return '*';
}
var pos = ipaddress.lastIndexOf('.');
const pos = ipaddress.lastIndexOf('.');
return pos !== -1 ? ipaddress.slice(0, pos) : '*';
};
@ -182,8 +183,12 @@ Matrix.toBroaderHostname = toBroaderHostname;
// speed. If desHostname is 1st-party to srcHostname, the domain is returned,
// otherwise the empty string.
var extractFirstPartyDesDomain = function(srcHostname, desHostname) {
if ( srcHostname === '*' || desHostname === '*' || desHostname === '1st-party' ) {
const extractFirstPartyDesDomain = function(srcHostname, desHostname) {
if (
srcHostname === '*' ||
desHostname === '*' ||
desHostname === '1st-party'
) {
return '';
}
var µmuri = µm.URI;
@ -229,9 +234,9 @@ Matrix.prototype.modified = function() {
Matrix.prototype.decomposeSource = function(srcHostname) {
if ( srcHostname === this.sourceRegister ) { return; }
var hn = srcHostname;
let hn = srcHostname;
this.decomposedSourceRegister[0] = this.sourceRegister = hn;
var i = 1;
let i = 1;
for (;;) {
hn = toBroaderHostname(hn);
this.decomposedSourceRegister[i++] = hn;
@ -245,25 +250,24 @@ Matrix.prototype.decomposeSource = function(srcHostname) {
// a live matrix.
Matrix.prototype.assign = function(other) {
var k, entry;
// Remove rules not in other
for ( k of this.rules.keys() ) {
for ( const k of this.rules.keys() ) {
if ( other.rules.has(k) === false ) {
this.rules.delete(k);
}
}
// Remove switches not in other
for ( k of this.switches.keys() ) {
for ( const k of this.switches.keys() ) {
if ( other.switches.has(k) === false ) {
this.switches.delete(k);
}
}
// Add/change rules in other
for ( entry of other.rules ) {
for ( const entry of other.rules ) {
this.rules.set(entry[0], entry[1]);
}
// Add/change switches in other
for ( entry of other.switches ) {
for ( const entry of other.switches ) {
this.switches.set(entry[0], entry[1]);
}
this.modified();
@ -277,14 +281,14 @@ Matrix.prototype.assign = function(other) {
// If value is undefined, the switch is removed
Matrix.prototype.setSwitch = function(switchName, srcHostname, newVal) {
var bitOffset = switchBitOffsets.get(switchName);
const bitOffset = switchBitOffsets.get(switchName);
if ( bitOffset === undefined ) {
return false;
}
if ( newVal === this.evaluateSwitch(switchName, srcHostname) ) {
return false;
}
var bits = this.switches.get(srcHostname) || 0;
let bits = this.switches.get(srcHostname) || 0;
bits &= ~(3 << bitOffset);
bits |= newVal << bitOffset;
if ( bits === 0 ) {
@ -299,13 +303,13 @@ Matrix.prototype.setSwitch = function(switchName, srcHostname, newVal) {
/******************************************************************************/
Matrix.prototype.setCell = function(srcHostname, desHostname, type, state) {
var bitOffset = typeBitOffsets.get(type),
k = srcHostname + ' ' + desHostname,
oldBitmap = this.rules.get(k);
const bitOffset = typeBitOffsets.get(type);
const k = srcHostname + ' ' + desHostname;
let oldBitmap = this.rules.get(k);
if ( oldBitmap === undefined ) {
oldBitmap = 0;
}
var newBitmap = oldBitmap & ~(3 << bitOffset) | (state << bitOffset);
const newBitmap = oldBitmap & ~(3 << bitOffset) | (state << bitOffset);
if ( newBitmap === oldBitmap ) {
return false;
}
@ -321,15 +325,11 @@ Matrix.prototype.setCell = function(srcHostname, desHostname, type, state) {
/******************************************************************************/
Matrix.prototype.blacklistCell = function(srcHostname, desHostname, type) {
var r = this.evaluateCellZ(srcHostname, desHostname, type);
if ( r === 1 ) {
return false;
}
let r = this.evaluateCellZ(srcHostname, desHostname, type);
if ( r === 1 ) { return false; }
this.setCell(srcHostname, desHostname, type, 0);
r = this.evaluateCellZ(srcHostname, desHostname, type);
if ( r === 1 ) {
return true;
}
if ( r === 1 ) { return true; }
this.setCell(srcHostname, desHostname, type, 1);
return true;
};
@ -337,15 +337,11 @@ Matrix.prototype.blacklistCell = function(srcHostname, desHostname, type) {
/******************************************************************************/
Matrix.prototype.whitelistCell = function(srcHostname, desHostname, type) {
var r = this.evaluateCellZ(srcHostname, desHostname, type);
if ( r === 2 ) {
return false;
}
let r = this.evaluateCellZ(srcHostname, desHostname, type);
if ( r === 2 ) { return false; }
this.setCell(srcHostname, desHostname, type, 0);
r = this.evaluateCellZ(srcHostname, desHostname, type);
if ( r === 2 ) {
return true;
}
if ( r === 2 ) { return true; }
this.setCell(srcHostname, desHostname, type, 2);
return true;
};
@ -353,15 +349,11 @@ Matrix.prototype.whitelistCell = function(srcHostname, desHostname, type) {
/******************************************************************************/
Matrix.prototype.graylistCell = function(srcHostname, desHostname, type) {
var r = this.evaluateCellZ(srcHostname, desHostname, type);
if ( r === 0 || r === 3 ) {
return false;
}
let r = this.evaluateCellZ(srcHostname, desHostname, type);
if ( r === 0 || r === 3 ) { return false; }
this.setCell(srcHostname, desHostname, type, 0);
r = this.evaluateCellZ(srcHostname, desHostname, type);
if ( r === 0 || r === 3 ) {
return true;
}
if ( r === 0 || r === 3 ) { return true; }
this.setCell(srcHostname, desHostname, type, 3);
return true;
};
@ -369,11 +361,9 @@ Matrix.prototype.graylistCell = function(srcHostname, desHostname, type) {
/******************************************************************************/
Matrix.prototype.evaluateCell = function(srcHostname, desHostname, type) {
var key = srcHostname + ' ' + desHostname;
var bitmap = this.rules.get(key);
if ( bitmap === undefined ) {
return 0;
}
const key = srcHostname + ' ' + desHostname;
const bitmap = this.rules.get(key);
if ( bitmap === undefined ) { return 0; }
return bitmap >> typeBitOffsets.get(type) & 3;
};
@ -382,12 +372,12 @@ Matrix.prototype.evaluateCell = function(srcHostname, desHostname, type) {
Matrix.prototype.evaluateCellZ = function(srcHostname, desHostname, type) {
this.decomposeSource(srcHostname);
var bitOffset = typeBitOffsets.get(type),
s, v, i = 0;
const bitOffset = typeBitOffsets.get(type);
let i = 0;
for (;;) {
s = this.decomposedSourceRegister[i++];
const s = this.decomposedSourceRegister[i++];
if ( s === '' ) { break; }
v = this.rules.get(s + ' ' + desHostname);
let v = this.rules.get(s + ' ' + desHostname);
if ( v !== undefined ) {
v = v >> bitOffset & 3;
if ( v !== 0 ) {
@ -398,7 +388,7 @@ Matrix.prototype.evaluateCellZ = function(srcHostname, desHostname, type) {
// srcHostname is '*' at this point
// Preset blacklisted hostnames are blacklisted in global scope
if ( type === '*' && µm.ubiquitousBlacklist.test(desHostname) ) {
if ( type === '*' && µm.ubiquitousBlacklistRef.matches(desHostname) !== -1 ) {
return 1;
}
@ -428,17 +418,18 @@ Matrix.prototype.evaluateCellZXY = function(srcHostname, desHostname, type) {
// Specific-hostname specific-type cell
this.specificityRegister = 1;
var r = this.evaluateCellZ(srcHostname, desHostname, type);
let r = this.evaluateCellZ(srcHostname, desHostname, type);
if ( r === 1 ) { return Matrix.RedDirect; }
if ( r === 2 ) { return Matrix.GreenDirect; }
// Specific-hostname any-type cell
this.specificityRegister = 2;
var rl = this.evaluateCellZ(srcHostname, desHostname, '*');
let rl = this.evaluateCellZ(srcHostname, desHostname, '*');
if ( rl === 1 ) { return Matrix.RedIndirect; }
var d = desHostname;
var firstPartyDesDomain = extractFirstPartyDesDomain(srcHostname, desHostname);
let d = desHostname;
const firstPartyDesDomain =
extractFirstPartyDesDomain(srcHostname, desHostname);
// Ancestor cells, up to 1st-party destination domain
if ( firstPartyDesDomain !== '' ) {
@ -504,13 +495,11 @@ Matrix.prototype.evaluateCellZXY = function(srcHostname, desHostname, type) {
return this.rootValue;
};
// https://www.youtube.com/watch?v=4C5ZkwrnVfM
/******************************************************************************/
Matrix.prototype.evaluateRowZXY = function(srcHostname, desHostname) {
let out = [];
for ( let type of typeBitOffsets.keys() ) {
const out = [];
for ( const type of typeBitOffsets.keys() ) {
out.push(this.evaluateCellZXY(srcHostname, desHostname, type));
}
return out;
@ -537,18 +526,14 @@ Matrix.prototype.desHostnameFromRule = function(rule) {
/******************************************************************************/
Matrix.prototype.setSwitchZ = function(switchName, srcHostname, newState) {
var bitOffset = switchBitOffsets.get(switchName);
if ( bitOffset === undefined ) {
return false;
}
var state = this.evaluateSwitchZ(switchName, srcHostname);
if ( newState === state ) {
return false;
}
const bitOffset = switchBitOffsets.get(switchName);
if ( bitOffset === undefined ) { return false; }
let state = this.evaluateSwitchZ(switchName, srcHostname);
if ( newState === state ) { return false; }
if ( newState === undefined ) {
newState = !state;
}
var bits = this.switches.get(srcHostname) || 0;
let bits = this.switches.get(srcHostname) || 0;
bits &= ~(3 << bitOffset);
if ( bits === 0 ) {
this.switches.delete(srcHostname);
@ -585,21 +570,20 @@ Matrix.prototype.evaluateSwitch = function(switchName, srcHostname) {
/******************************************************************************/
Matrix.prototype.evaluateSwitchZ = function(switchName, srcHostname) {
var bitOffset = switchBitOffsets.get(switchName);
const bitOffset = switchBitOffsets.get(switchName);
if ( bitOffset === undefined ) { return false; }
this.decomposeSource(srcHostname);
var s, bits, i = 0;
let i = 0;
for (;;) {
s = this.decomposedSourceRegister[i++];
const s = this.decomposedSourceRegister[i++];
if ( s === '' ) { break; }
bits = this.switches.get(s) || 0;
let bits = this.switches.get(s) || 0;
if ( bits === 0 ) { continue; }
bits = bits >> bitOffset & 3;
if ( bits !== 0 ) {
bits = bits >> bitOffset & 3;
if ( bits !== 0 ) {
return bits === 1;
}
return bits === 1;
}
}
return false;
@ -607,15 +591,15 @@ Matrix.prototype.evaluateSwitchZ = function(switchName, srcHostname) {
/******************************************************************************/
Matrix.prototype.extractAllSourceHostnames = (function() {
var cachedResult = new Set();
var matrixId = 0;
var readTime = 0;
Matrix.prototype.extractAllSourceHostnames = (( ) => {
const cachedResult = new Set();
let matrixId = 0;
let readTime = 0;
return function() {
if ( matrixId !== this.id || readTime !== this.modifiedTime ) {
cachedResult.clear();
for ( var rule of this.rules.keys() ) {
for ( const rule of this.rules.keys() ) {
cachedResult.add(rule.slice(0, rule.indexOf(' ')));
}
matrixId = this.id;
@ -627,11 +611,8 @@ Matrix.prototype.extractAllSourceHostnames = (function() {
/******************************************************************************/
// https://github.com/gorhill/uMatrix/issues/759
// Backward compatibility: 'plugin' => 'media'
Matrix.prototype.partsFromLine = function(line) {
let fields = line.split(/\s+/);
const fields = line.split(/\s+/);
if ( fields.length < 3 ) { return; }
// Switches
@ -649,7 +630,9 @@ Matrix.prototype.partsFromLine = function(line) {
if ( fields.length < 4 ) { return; }
fields[0] = punycodeIf(fields[0]);
fields[1] = punycodeIf(fields[1]);
if ( fields[2] === 'plugin' ) { fields[2] = 'media'; }
if ( this.renamedRules.has(fields[2]) ) {
fields[2] = this.renamedRules.get(fields[2]);
}
if ( typeBitOffsets.get(fields[2]) === undefined ) { return; }
if ( nameToStateMap.hasOwnProperty(fields[3]) === false ) { return; }
fields[3] = nameToStateMap[fields[3]];
@ -658,11 +641,15 @@ Matrix.prototype.partsFromLine = function(line) {
};
Matrix.prototype.reSwitchRule = /^[0-9a-z-]+:$/;
Matrix.prototype.renamedRules = new Map([
[ 'plugin', 'media' ],
[ 'xhr', 'fetch' ],
]);
/******************************************************************************/
Matrix.prototype.fromArray = function(lines, append) {
let matrix = append === true ? this : new Matrix();
const matrix = append === true ? this : new Matrix();
for ( let line of lines ) {
matrix.addFromLine(line);
}
@ -673,12 +660,12 @@ Matrix.prototype.fromArray = function(lines, append) {
};
Matrix.prototype.toArray = function() {
let out = [];
for ( let rule of this.rules.keys() ) {
let srcHostname = this.srcHostnameFromRule(rule);
let desHostname = this.desHostnameFromRule(rule);
const out = [];
for ( const rule of this.rules.keys() ) {
const srcHostname = this.srcHostnameFromRule(rule);
const desHostname = this.desHostnameFromRule(rule);
for ( let type of typeBitOffsets.keys() ) {
let val = this.evaluateCell(srcHostname, desHostname, type);
const val = this.evaluateCell(srcHostname, desHostname, type);
if ( val === 0 ) { continue; }
out.push(
unpunycodeIf(srcHostname) + ' ' +
@ -688,9 +675,9 @@ Matrix.prototype.toArray = function() {
);
}
}
for ( let srcHostname of this.switches.keys() ) {
for ( let switchName of switchBitOffsets.keys() ) {
let val = this.evaluateSwitch(switchName, srcHostname);
for ( const srcHostname of this.switches.keys() ) {
for ( const switchName of switchBitOffsets.keys() ) {
const val = this.evaluateSwitch(switchName, srcHostname);
if ( val === 0 ) { continue; }
out.push(
switchName + ': ' +
@ -705,8 +692,8 @@ Matrix.prototype.toArray = function() {
/******************************************************************************/
Matrix.prototype.fromString = function(text, append) {
let matrix = append === true ? this : new Matrix();
let textEnd = text.length;
const matrix = append === true ? this : new Matrix();
const textEnd = text.length;
let lineBeg = 0;
while ( lineBeg < textEnd ) {
@ -719,7 +706,7 @@ Matrix.prototype.fromString = function(text, append) {
}
let line = text.slice(lineBeg, lineEnd).trim();
lineBeg = lineEnd + 1;
let pos = line.indexOf('# ');
const pos = line.indexOf('# ');
if ( pos !== -1 ) {
line = line.slice(0, pos).trim();
}
@ -741,30 +728,28 @@ Matrix.prototype.toString = function() {
/******************************************************************************/
Matrix.prototype.addFromLine = function(line) {
let fields = this.partsFromLine(line);
if ( fields !== undefined ) {
// Switches
if ( fields.length === 3 ) {
return this.setSwitch(fields[0], fields[1], fields[2]);
}
// Rules
if ( fields.length === 4 ) {
return this.setCell(fields[0], fields[1], fields[2], fields[3]);
}
const fields = this.partsFromLine(line);
if ( fields === undefined ) { return; }
// Switches
if ( fields.length === 3 ) {
return this.setSwitch(fields[0], fields[1], fields[2]);
}
// Rules
if ( fields.length === 4 ) {
return this.setCell(fields[0], fields[1], fields[2], fields[3]);
}
};
Matrix.prototype.removeFromLine = function(line) {
let fields = this.partsFromLine(line);
if ( fields !== undefined ) {
// Switches
if ( fields.length === 3 ) {
return this.setSwitch(fields[0], fields[1], 0);
}
// Rules
if ( fields.length === 4 ) {
return this.setCell(fields[0], fields[1], fields[2], 0);
}
const fields = this.partsFromLine(line);
if ( fields === undefined ) { return; }
// Switches
if ( fields.length === 3 ) {
return this.setSwitch(fields[0], fields[1], 0);
}
// Rules
if ( fields.length === 4 ) {
return this.setCell(fields[0], fields[1], fields[2], 0);
}
};
@ -789,13 +774,11 @@ Matrix.prototype.toSelfie = function() {
/******************************************************************************/
Matrix.prototype.diff = function(other, srcHostname, desHostnames) {
var out = [];
var desHostname, type;
var switchName, i, thisVal, otherVal;
const out = [];
for (;;) {
for ( switchName of switchBitOffsets.keys() ) {
thisVal = this.evaluateSwitch(switchName, srcHostname);
otherVal = other.evaluateSwitch(switchName, srcHostname);
for ( const switchName of switchBitOffsets.keys() ) {
const thisVal = this.evaluateSwitch(switchName, srcHostname);
const otherVal = other.evaluateSwitch(switchName, srcHostname);
if ( thisVal !== otherVal ) {
out.push({
'what': switchName,
@ -803,12 +786,12 @@ Matrix.prototype.diff = function(other, srcHostname, desHostnames) {
});
}
}
i = desHostnames.length;
let i = desHostnames.length;
while ( i-- ) {
desHostname = desHostnames[i];
for ( type of typeBitOffsets.keys() ) {
thisVal = this.evaluateCell(srcHostname, desHostname, type);
otherVal = other.evaluateCell(srcHostname, desHostname, type);
const desHostname = desHostnames[i];
for ( const type of typeBitOffsets.keys() ) {
const thisVal = this.evaluateCell(srcHostname, desHostname, type);
const otherVal = other.evaluateCell(srcHostname, desHostname, type);
if ( thisVal === otherVal ) { continue; }
out.push({
'what': 'rule',
@ -827,18 +810,15 @@ Matrix.prototype.diff = function(other, srcHostname, desHostnames) {
/******************************************************************************/
Matrix.prototype.applyDiff = function(diff, from) {
var changed = false;
var i = diff.length;
var action, val;
while ( i-- ) {
action = diff[i];
let changed = false;
for ( const action of diff ) {
if ( action.what === 'rule' ) {
val = from.evaluateCell(action.src, action.des, action.type);
const val = from.evaluateCell(action.src, action.des, action.type);
changed = this.setCell(action.src, action.des, action.type, val) || changed;
continue;
}
if ( switchBitOffsets.has(action.what) ) {
val = from.evaluateSwitch(action.what, action.src);
const val = from.evaluateSwitch(action.what, action.src);
changed = this.setSwitch(action.what, action.src, val) || changed;
continue;
}
@ -848,19 +828,19 @@ Matrix.prototype.applyDiff = function(diff, from) {
Matrix.prototype.copyRuleset = function(entries, from, deep) {
let changed = false;
for ( let entry of entries ) {
for ( const entry of entries ) {
let srcHn = entry.srcHn;
for (;;) {
if (
entry.switchName !== undefined &&
switchBitOffsets.has(entry.switchName)
) {
let val = from.evaluateSwitch(entry.switchName, srcHn);
const val = from.evaluateSwitch(entry.switchName, srcHn);
if ( this.setSwitch(entry.switchName, srcHn, val) ) {
changed = true;
}
} else if ( entry.desHn && entry.type ) {
let val = from.evaluateCell(srcHn, entry.desHn, entry.type);
const val = from.evaluateCell(srcHn, entry.desHn, entry.type);
if ( this.setCell(srcHn, entry.desHn, entry.type, val) ) {
changed = true;
}
@ -879,8 +859,6 @@ return Matrix;
/******************************************************************************/
// https://www.youtube.com/watch?v=wlNrQGmj6oQ
})();
/******************************************************************************/

776
src/js/messaging.js
File diff suppressed because it is too large
View File

133
src/js/pagestats.js

@ -23,42 +23,39 @@
/******************************************************************************/
µMatrix.pageStoreFactory = (function() {
µMatrix.pageStoreFactory = (( ) => {
/******************************************************************************/
var µm = µMatrix;
const µm = µMatrix;
/******************************************************************************/
var BlockedCollapsibles = function() {
this.boundPruneAsyncCallback = this.pruneAsyncCallback.bind(this);
this.blocked = new Map();
this.hash = 0;
this.timer = null;
this.tOrigin = Date.now();
};
BlockedCollapsibles.prototype = {
shelfLife: 10 * 1000,
const BlockedCollapsibles = class {
constructor() {
this.boundPruneAsyncCallback = this.pruneAsyncCallback.bind(this);
this.blocked = new Map();
this.hash = 0;
this.timer = null;
this.tOrigin = Date.now();
}
add: function(type, url, isSpecific) {
add(type, url, isSpecific) {
if ( this.blocked.size === 0 ) { this.pruneAsync(); }
let tStamp = Date.now() - this.tOrigin;
// The following "trick" is to encode the specifity into the lsb of the
// time stamp so as to avoid to have to allocate a memory structure to
// store both time stamp and specificity.
if ( isSpecific ) {
tStamp |= 0x00000001;
tStamp |= 1;
} else {
tStamp &= 0xFFFFFFFE;
tStamp &= ~1;
}
this.blocked.set(type + ' ' + url, tStamp);
this.hash += 1;
},
}
reset: function() {
reset() {
this.blocked.clear();
this.hash = 0;
if ( this.timer !== null ) {
@ -66,23 +63,23 @@ BlockedCollapsibles.prototype = {
this.timer = null;
}
this.tOrigin = Date.now();
},
}
pruneAsync: function() {
pruneAsync() {
if ( this.timer === null ) {
this.timer = vAPI.setTimeout(
this.boundPruneAsyncCallback,
this.shelfLife * 2
);
}
},
}
pruneAsyncCallback: function() {
pruneAsyncCallback() {
this.timer = null;
let tObsolete = Date.now() - this.tOrigin - this.shelfLife;
for ( let entry of this.blocked ) {
if ( entry[1] <= tObsolete ) {
this.blocked.delete(entry[0]);
const tObsolete = Date.now() - this.tOrigin - this.shelfLife;
for ( const [ key, tStamp ] of this.blocked ) {
if ( tStamp <= tObsolete ) {
this.blocked.delete(key);
}
}
if ( this.blocked.size !== 0 ) {
@ -93,6 +90,8 @@ BlockedCollapsibles.prototype = {
}
};
BlockedCollapsibles.prototype.shelfLife = 10 * 1000;
/******************************************************************************/
// Ref: Given a URL, returns a (somewhat) unique 32-bit value
@ -100,19 +99,15 @@ BlockedCollapsibles.prototype = {
// http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-reference-source
// The rest is custom, suited for uMatrix.
var PageStore = function(tabContext) {
this.hostnameTypeCells = new Map();
this.domains = new Set();
this.blockedCollapsibles = new BlockedCollapsibles();
this.init(tabContext);
};
PageStore.prototype = {
collapsibleTypes: new Set([ 'image' ]),
pageStoreJunkyard: [],
const PageStore = class {
constructor(tabContext) {
this.hostnameTypeCells = new Map();
this.domains = new Set();
this.blockedCollapsibles = new BlockedCollapsibles();
this.init(tabContext);
}
init: function(tabContext) {
init(tabContext) {
this.tabId = tabContext.tabId;
this.rawURL = tabContext.rawURL;
this.pageUrl = tabContext.normalURL;
@ -130,13 +125,14 @@ PageStore.prototype = {
this.hasMixedContent = false;
this.hasNoscriptTags = false;
this.hasWebWorkers = false;
this.hasHostnameAliases = false;
this.incinerationTimer = null;
this.mtxContentModifiedTime = 0;
this.mtxCountModifiedTime = 0;
return this;
},
}
dispose: function() {
dispose() {
this.tabId = '';
this.rawURL = '';
this.pageUrl = '';
@ -154,9 +150,9 @@ PageStore.prototype = {
if ( this.pageStoreJunkyard.length < 8 ) {
this.pageStoreJunkyard.push(this);
}
},
}
cacheBlockedCollapsible: function(type, url, specificity) {
cacheBlockedCollapsible(type, url, specificity) {
if ( this.collapsibleTypes.has(type) ) {
this.blockedCollapsibles.add(
type,
@ -164,20 +160,20 @@ PageStore.prototype = {
specificity !== 0 && specificity < 5
);
}
},
}
lookupBlockedCollapsibles: function(request, response) {
var tabContext = µm.tabContextManager.lookup(this.tabId);
lookupBlockedCollapsibles(request, response) {
const tabContext = µm.tabContextManager.lookup(this.tabId);
if ( tabContext === null ) { return; }
if (
Array.isArray(request.toFilter) &&
request.toFilter.length !== 0
) {
let roothn = tabContext.rootHostname,
hnFromURI = µm.URI.hostnameFromURI,
tMatrix = µm.tMatrix;
for ( let entry of request.toFilter ) {
const roothn = tabContext.rootHostname;
const hnFromURI = vAPI.hostnameFromURI;
const tMatrix = µm.tMatrix;
for ( const entry of request.toFilter ) {
if ( tMatrix.mustBlock(roothn, hnFromURI(entry.url), entry.type) ) {
this.blockedCollapsibles.add(
entry.type,
@ -191,19 +187,19 @@ PageStore.prototype = {
if ( this.blockedCollapsibles.hash === response.hash ) { return; }
response.hash = this.blockedCollapsibles.hash;
let collapseBlacklisted = µm.userSettings.collapseBlacklisted,
collapseBlocked = µm.userSettings.collapseBlocked,
blockedResources = response.blockedResources;
const collapseBlacklisted = µm.userSettings.collapseBlacklisted;
const collapseBlocked = µm.userSettings.collapseBlocked;
const blockedResources = response.blockedResources;
for ( let entry of this.blockedCollapsibles.blocked ) {
for ( const entry of this.blockedCollapsibles.blocked ) {
blockedResources.push([
entry[0],
collapseBlocked || collapseBlacklisted && (entry[1] & 1) !== 0
]);
}
},
}
recordRequest: function(type, url, block) {
recordRequest(type, url, block) {
if ( this.tabId <= 0 ) { return; }
if ( block ) {
@ -216,19 +212,19 @@ PageStore.prototype = {
// - remember which hostname/type were seen
// - count the number of distinct URLs for any given
// hostname-type pair
var hostname = µm.URI.hostnameFromURI(url),
key = hostname + ' ' + type,
uids = this.hostnameTypeCells.get(key);
const hostname = vAPI.hostnameFromURI(url);
const key = hostname + ' ' + type;
let uids = this.hostnameTypeCells.get(key);
if ( uids === undefined ) {
this.hostnameTypeCells.set(key, (uids = new Set()));
} else if ( uids.size > 99 ) {
return;
}
var uid = this.uidFromURL(url);
const uid = this.uidFromURL(url);
if ( uids.has(uid) ) { return; }
uids.add(uid);
µm.updateBadgeAsync(this.tabId);
µm.updateToolbarIcon(this.tabId);
this.mtxCountModifiedTime = Date.now();
@ -237,24 +233,27 @@ PageStore.prototype = {
this.allHostnamesString += hostname + ' ';
this.mtxContentModifiedTime = Date.now();
}
},
}
uidFromURL: function(uri) {
var hint = 0x811c9dc5,
i = uri.length;
uidFromURL(uri) {
let hint = 0x811c9dc5;
let i = uri.length;
while ( i-- ) {
hint ^= uri.charCodeAt(i) | 0;
hint += (hint<<1) + (hint<<4) + (hint<<7) + (hint<<8) + (hint<<24) | 0;
hint ^= uri.charCodeAt(i);
hint += (hint<<1) + (hint<<4) + (hint<<7) + (hint<<8) + (hint<<24);
hint >>>= 0;
}
return hint;
}
};
PageStore.prototype.collapsibleTypes = new Set([ 'image' ]);
PageStore.prototype.pageStoreJunkyard = [];
/******************************************************************************/
return function pageStoreFactory(tabContext) {
var entry = PageStore.prototype.pageStoreJunkyard.pop();
const entry = PageStore.prototype.pageStoreJunkyard.pop();
if ( entry ) {
return entry.init(tabContext);
}

1117
src/js/popup.js
File diff suppressed because it is too large
View File

112
src/js/raw-settings.js

@ -19,23 +19,34 @@
Home: https://github.com/gorhill/uBlock
*/
/* global uDom */
/* global CodeMirror, uDom, uBlockDashboard */
'use strict';
/******************************************************************************/
(function() {
{
// >>>>> start of local scope
/******************************************************************************/
var messaging = vAPI.messaging;
var cachedData = '';
var rawSettingsInput = uDom.nodeFromId('rawSettings');
const cmEditor = new CodeMirror(
document.getElementById('rawSettings'),
{
autofocus: true,
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true
}
);
uBlockDashboard.patchCodeMirrorEditor(cmEditor);
let cachedData = '';
/******************************************************************************/
var hashFromRawSettings = function(raw) {
const hashFromRawSettings = function(raw) {
return raw.trim().replace(/\s+/g, '|');
};
@ -43,63 +54,63 @@ var hashFromRawSettings = function(raw) {
// This is to give a visual hint that the content of user blacklist has changed.
var rawSettingsChanged = (function () {
var timer = null;
const rawSettingsChanged = (( ) => {
let timer;
var handler = function() {
timer = null;
var changed =
hashFromRawSettings(rawSettingsInput.value) !== cachedData;
uDom.nodeFromId('rawSettingsApply').disabled = !changed;
const handler = function() {
timer = undefined;
const changed =
hashFromRawSettings(cmEditor.getValue()) !== cachedData;
uDom.nodeFromId('rawSettingsApply').disabled = changed === false;
CodeMirror.commands.save = changed ? applyChanges : function(){};
};
return function() {
if ( timer !== null ) {
if ( timer !== undefined ) {
clearTimeout(timer);
}
timer = vAPI.setTimeout(handler, 100);
};
})();
cmEditor.on('changes', rawSettingsChanged);
/******************************************************************************/
function renderRawSettings() {
var onRead = function(raw) {
cachedData = hashFromRawSettings(raw);
var pretty = [],
whitespaces = ' ',
lines = raw.split('\n'),
max = 0,
pos,
i, n = lines.length;
for ( i = 0; i < n; i++ ) {
pos = lines[i].indexOf(' ');
if ( pos > max ) {
max = pos;
}
}
for ( i = 0; i < n; i++ ) {
pos = lines[i].indexOf(' ');
pretty.push(whitespaces.slice(0, max - pos) + lines[i]);
}
rawSettingsInput.value = pretty.join('\n') + '\n';
rawSettingsChanged();
rawSettingsInput.focus();
};
messaging.send('dashboard', { what: 'readRawSettings' }, onRead);
}
const renderRawSettings = async function(first) {
const raw = await vAPI.messaging.send('dashboard', {
what: 'readRawSettings'
});
cachedData = hashFromRawSettings(raw);
const lines = raw.split('\n');
const n = lines.length;
let max = 0;
for ( let i = 0; i < n; i++ ) {
const pos = lines[i].indexOf(' ');
if ( pos > max ) { max = pos; }
}
const pretty = [];
for ( let i = 0; i < n; i++ ) {
const pos = lines[i].indexOf(' ');
pretty.push(' '.repeat(max - pos) + lines[i]);
}
pretty.push('');
cmEditor.setValue(pretty.join('\n'));
if ( first ) {
cmEditor.clearHistory();
}
rawSettingsChanged();
cmEditor.focus();
};
/******************************************************************************/
var applyChanges = function() {
messaging.send(
'dashboard',
{
what: 'writeRawSettings',
content: rawSettingsInput.value
},
renderRawSettings
);
const applyChanges = async function() {
await vAPI.messaging.send('dashboard', {
what: 'writeRawSettings',
content: cmEditor.getValue(),
});
renderRawSettings();
};
/******************************************************************************/
@ -108,8 +119,9 @@ var applyChanges = function() {
uDom('#rawSettings').on('input', rawSettingsChanged);
uDom('#rawSettingsApply').on('click', applyChanges);
renderRawSettings();
renderRawSettings(true);
/******************************************************************************/
})();
// <<<<< end of local scope
}

114
src/js/settings.js

@ -25,37 +25,38 @@
/******************************************************************************/
(function() {
{
// >>>>> start of local scope
/******************************************************************************/
var cachedSettings = {};
let cachedSettings = {};
/******************************************************************************/
function changeUserSettings(name, value) {
vAPI.messaging.send('settings.js', {
const changeUserSettings = function(name, value) {
vAPI.messaging.send('dashboard', {
what: 'userSettings',
name: name,
value: value
name,
value
});
}
};
/******************************************************************************/
function changeMatrixSwitch(name, state) {
vAPI.messaging.send('settings.js', {
const changeMatrixSwitch = function(switchName, state) {
vAPI.messaging.send('dashboard', {
what: 'setMatrixSwitch',
switchName: name,
state: state
switchName,
state
});
}
};
/******************************************************************************/
function onChangeValueHandler(elem, setting, min, max) {
var oldVal = cachedSettings.userSettings[setting];
var newVal = Math.round(parseFloat(elem.value));
const onChangeValueHandler = function (elem, setting, min, max) {
const oldVal = cachedSettings.userSettings[setting];
let newVal = Math.round(parseFloat(elem.value));
if ( typeof newVal !== 'number' ) {
newVal = oldVal;
} else {
@ -66,11 +67,11 @@ function onChangeValueHandler(elem, setting, min, max) {
if ( newVal !== oldVal ) {
changeUserSettings(setting, newVal);
}
}
};
/******************************************************************************/
function prepareToDie() {
const prepareToDie = function() {
onChangeValueHandler(
uDom.nodeFromId('deleteUnusedSessionCookiesAfter'),
'deleteUnusedSessionCookiesAfter',
@ -81,12 +82,12 @@ function prepareToDie() {
'clearBrowserCacheAfter',
15, 1440
);
}
};
/******************************************************************************/
function onInputChanged(ev) {
var target = ev.target;
const onInputChanged = function(ev) {
const target = ev.target;
switch ( target.id ) {
case 'displayTextSize':
@ -133,63 +134,60 @@ function onInputChanged(ev) {
default:
break;
}
}
};
/******************************************************************************/
function synchronizeWidgets() {
var e1, e2;
e1 = uDom.nodeFromId('collapseBlocked');
e2 = uDom.nodeFromId('collapseBlacklisted');
const synchronizeWidgets = function() {
const e1 = uDom.nodeFromId('collapseBlocked');
const e2 = uDom.nodeFromId('collapseBlacklisted');
if ( e1.checked ) {
e2.setAttribute('disabled', '');
} else {
e2.removeAttribute('disabled');
}
}
};
/******************************************************************************/
vAPI.messaging.send(
'settings.js',
{ what: 'getUserSettings' },
function onSettingsReceived(settings) {
// Cache copy
cachedSettings = settings;
vAPI.messaging.send('dashboard', {
what: 'getUserSettings'
}).then(settings => {
// Cache copy
cachedSettings = settings;
var userSettings = settings.userSettings;
var matrixSwitches = settings.matrixSwitches;
const userSettings = settings.userSettings;
const matrixSwitches = settings.matrixSwitches;
uDom('[data-setting-bool]').forEach(function(elem){
elem.prop('checked', userSettings[elem.prop('id')] === true);
});
uDom('[data-setting-bool]').forEach(function(elem){
elem.prop('checked', userSettings[elem.prop('id')] === true);
});
uDom('[data-matrix-switch]').forEach(function(elem){
var switchName = elem.attr('data-matrix-switch');
if ( typeof switchName === 'string' && switchName !== '' ) {
elem.prop('checked', matrixSwitches[switchName] === true);
}
});
uDom('[data-matrix-switch]').forEach(function(elem){
const switchName = elem.attr('data-matrix-switch');
if ( typeof switchName === 'string' && switchName !== '' ) {
elem.prop('checked', matrixSwitches[switchName] === true);
}
});
uDom.nodeFromId('displayTextSize').value =
parseInt(userSettings.displayTextSize, 10) || 14;
uDom.nodeFromId('displayTextSize').value =
parseInt(userSettings.displayTextSize, 10) || 14;
uDom.nodeFromId('popupScopeLevel').value = userSettings.popupScopeLevel;
uDom.nodeFromId('deleteUnusedSessionCookiesAfter').value =
userSettings.deleteUnusedSessionCookiesAfter;
uDom.nodeFromId('clearBrowserCacheAfter').value =
userSettings.clearBrowserCacheAfter;
uDom.nodeFromId('popupScopeLevel').value = userSettings.popupScopeLevel;
uDom.nodeFromId('deleteUnusedSessionCookiesAfter').value =
userSettings.deleteUnusedSessionCookiesAfter;
uDom.nodeFromId('clearBrowserCacheAfter').value =
userSettings.clearBrowserCacheAfter;
synchronizeWidgets();
synchronizeWidgets();
document.addEventListener('change', onInputChanged);
document.addEventListener('change', onInputChanged);
// https://github.com/gorhill/httpswitchboard/issues/197
uDom(window).on('beforeunload', prepareToDie);
}
);
// https://github.com/gorhill/httpswitchboard/issues/197
uDom(window).on('beforeunload', prepareToDie);
});
/******************************************************************************/
})();
// <<<<< end of local scope
}

120
src/js/start.js

@ -19,37 +19,56 @@
Home: https://github.com/gorhill/uMatrix
*/
// ORDER IS IMPORTANT
/******************************************************************************/
// Load everything
(function() {
'use strict';
/******************************************************************************/
var µm = µMatrix;
/******************************************************************************/
var processCallbackQueue = function(queue, callback) {
var processOne = function() {
var fn = queue.pop();
if ( fn ) {
fn(processOne);
} else if ( typeof callback === 'function' ) {
callback();
(async ( ) => {
const µm = µMatrix;
await Promise.all([
µm.loadPublicSuffixList(),
µm.loadUserSettings(),
]);
log.info(`PSL and user settings ready ${Date.now()-vAPI.T0} ms after launch`);
{
let trieDetails;
try {
trieDetails = JSON.parse(
vAPI.localStorage.getItem('ubiquitousBlacklist.trieDetails')
);
} catch(ex) {
}
};
processOne();
};
/******************************************************************************/
µm.ubiquitousBlacklist = new µm.HNTrieContainer(trieDetails);
µm.ubiquitousBlacklist.initWASM();
}
log.info(`Ubiquitous block container ready ${Date.now()-vAPI.T0} ms after launch`);
await Promise.all([
µm.loadRawSettings(),
µm.loadMatrix(),
µm.loadHostsFiles(),
]);
log.info(`Ubiquitous block rules ready ${Date.now()-vAPI.T0} ms after launch`);
{
const pageStore =
µm.pageStoreFactory(µm.tabContextManager.mustLookup(vAPI.noTabId));
pageStore.title = vAPI.i18n('statsPageDetailedBehindTheScenePage');
µm.pageStores.set(vAPI.noTabId, pageStore);
}
const tabs = await vAPI.tabs.query({ url: '<all_urls>' });
if ( Array.isArray(tabs) ) {
for ( const tab of tabs ) {
µm.tabContextManager.push(tab.id, tab.url, 'newURL');
µm.bindTabToPageStats(tab.id);
µm.setPageStoreTitle(tab.id, tab.title);
}
}
log.info(`Tab stores ready ${Date.now()-vAPI.T0} ms after launch`);
var onAllDone = function() {
µm.webRequest.start();
µm.loadRecipes();
@ -59,57 +78,6 @@ var onAllDone = function() {
// asset updater.
µm.assets.addObserver(µm.assetObserver.bind(µm));
µm.scheduleAssetUpdater(µm.userSettings.autoUpdate ? 7 * 60 * 1000 : 0);
vAPI.cloud.start([ 'myRulesPane' ]);
};
/******************************************************************************/
var onPSLReady = function() {
// TODO: Promisify
let count = 4;
const countdown = ( ) => {
count -= 1;
if ( count !== 0 ) { return; }
onAllDone();
};
µm.loadRawSettings(countdown);
µm.loadMatrix(countdown);
µm.loadHostsFiles(countdown);
vAPI.tabs.getAll(tabs => {
const pageStore =
µm.pageStoreFactory(µm.tabContextManager.mustLookup(vAPI.noTabId));
pageStore.title = vAPI.i18n('statsPageDetailedBehindTheScenePage');
µm.pageStores.set(vAPI.noTabId, pageStore);
if ( Array.isArray(tabs) ) {
for ( const tab of tabs ) {
µm.tabContextManager.push(tab.id, tab.url, 'newURL');
}
}
countdown();
});
};
/******************************************************************************/
processCallbackQueue(µm.onBeforeStartQueue, function() {
// TODO: Promisify
let count = 2;
const countdown = ( ) => {
count -= 1;
if ( count !== 0 ) { return; }
onPSLReady();
};
µm.publicSuffixList.load(countdown);
µm.loadUserSettings(countdown);
});
/******************************************************************************/
})();
/******************************************************************************/

908
src/js/storage.js
File diff suppressed because it is too large
View File

549
src/js/tab.js

@ -19,16 +19,16 @@
Home: https://github.com/gorhill/uMatrix
*/
'use strict';
/******************************************************************************/
/******************************************************************************/
(function() {
'use strict';
(( ) => {
/******************************************************************************/
var µm = µMatrix;
const µm = µMatrix;
// https://github.com/gorhill/httpswitchboard/issues/303
// Some kind of trick going on here:
@ -152,35 +152,9 @@ housekeep itself.
*/
µm.tabContextManager = (function() {
let tabContexts = new Map();
let urlToTabIds = {
associations: new Map(),
associate: function(tabId, url) {
let tabIds = this.associations.get(url);
if ( tabIds === undefined ) {
this.associations.set(url, (tabIds = []));
} else {
let i = tabIds.indexOf(tabId);
if ( i !== -1 ) {
tabIds.splice(i, 1);
}
}
tabIds.push(tabId);
},
dissociate: function(tabId, url) {
let tabIds = this.associations.get(url);
if ( tabIds === undefined ) { return; }
let i = tabIds.indexOf(tabId);
if ( i !== -1 ) {
tabIds.splice(i, 1);
}
if ( tabIds.length === 0 ) {
this.associations.delete(url);
}
}
};
µMatrix.tabContextManager = (( ) => {
const µm = µMatrix;
const tabContexts = new Map();
// https://github.com/chrisaljoudi/uBlock/issues/1001
// This is to be used as last-resort fallback in case a tab is found to not
@ -188,27 +162,32 @@ housekeep itself.
let mostRecentRootDocURL = '';
let mostRecentRootDocURLTimestamp = 0;
let gcPeriod = 31 * 60 * 1000; // every 31 minutes
const onTabCreated = async function(/* createDetails */) {
};
const gcPeriod = 10 * 60 * 1000;
// A pushed entry is removed from the stack unless it is committed with
// a set time.
let StackEntry = function(url, commit) {
const StackEntry = function(url, commit) {
this.url = url;
this.committed = commit;
this.tstamp = Date.now();
};
let TabContext = function(tabId) {
const TabContext = function(tabId) {
this.tabId = tabId;
this.stack = [];
this.rawURL =
this.normalURL =
this.scheme =
this.origin =
this.rootHostname =
this.rootDomain = '';
this.secure = false;
this.commitTimer = null;
this.gcTimer = null;
this.onGCBarrier = false;
this.netFiltering = true;
this.netFilteringReadTime = 0;
tabContexts.set(tabId, this);
};
@ -219,36 +198,44 @@ housekeep itself.
clearTimeout(this.gcTimer);
this.gcTimer = null;
}
urlToTabIds.dissociate(this.tabId, this.rawURL);
tabContexts.delete(this.tabId);
};
TabContext.prototype.onTab = function(tab) {
if ( tab ) {
this.gcTimer = vAPI.setTimeout(this.onGC.bind(this), gcPeriod);
this.gcTimer = vAPI.setTimeout(( ) => this.onGC(), gcPeriod);
} else {
this.destroy();
}
};
TabContext.prototype.onGC = function() {
this.gcTimer = null;
TabContext.prototype.onGC = async function() {
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { return; }
vAPI.tabs.get(this.tabId, this.onTab.bind(this));
// https://github.com/gorhill/uBlock/issues/1713
// For unknown reasons, Firefox's setTimeout() will sometimes
// causes the callback function to be called immediately, bypassing
// the main event loop. For now this should prevent uBO from crashing
// as a result of the bad setTimeout() behavior.
if ( this.onGCBarrier ) { return; }
this.onGCBarrier = true;
this.gcTimer = null;
const tab = await vAPI.tabs.get(this.tabId);
this.onTab(tab);
this.onGCBarrier = false;
};
// https://github.com/gorhill/uBlock/issues/248
// Stack entries have to be committed to stick. Non-committed stack
// entries are removed after a set delay.
TabContext.prototype.onCommit = function() {
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { return; }
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) {
return;
}
this.commitTimer = null;
// Remove uncommitted entries at the top of the stack.
let i = this.stack.length;
while ( i-- ) {
if ( this.stack[i].committed ) {
break;
}
if ( this.stack[i].committed ) { break; }
}
// https://github.com/gorhill/uBlock/issues/300
// If no committed entry was found, fall back on the bottom-most one
@ -261,7 +248,6 @@ housekeep itself.
if ( i < this.stack.length ) {
this.stack.length = i;
this.update();
µm.bindTabToPageStats(this.tabId, 'newURL');
}
};
@ -270,76 +256,103 @@ housekeep itself.
// want to flush it.
TabContext.prototype.autodestroy = function() {
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { return; }
this.gcTimer = vAPI.setTimeout(this.onGC.bind(this), gcPeriod);
this.gcTimer = vAPI.setTimeout(( ) => this.onGC(), gcPeriod);
};
// Update just force all properties to be updated to match the most recent
// root URL.
TabContext.prototype.update = function() {
urlToTabIds.dissociate(this.tabId, this.rawURL);
this.netFilteringReadTime = 0;
if ( this.stack.length === 0 ) {
this.rawURL = this.normalURL = this.scheme =
this.rootHostname = this.rootDomain = '';
this.secure = false;
this.rawURL =
this.normalURL =
this.origin =
this.rootHostname =
this.rootDomain = '';
return;
}
this.rawURL = this.stack[this.stack.length - 1].url;
const stackEntry = this.stack[this.stack.length - 1];
this.rawURL = stackEntry.url;
this.normalURL = µm.normalizePageURL(this.tabId, this.rawURL);
this.scheme = µm.URI.schemeFromURI(this.rawURL);
this.rootHostname = µm.URI.hostnameFromURI(this.normalURL);
this.rootDomain = µm.URI.domainFromHostname(this.rootHostname) || this.rootHostname;
this.secure = µm.URI.isSecureScheme(this.scheme);
urlToTabIds.associate(this.tabId, this.rawURL);
this.origin = µm.URI.originFromURI(this.normalURL);
this.rootHostname = µm.URI.hostnameFromURI(this.origin);
this.rootDomain =
µm.URI.domainFromHostname(this.rootHostname) ||
this.rootHostname;
};
// Called whenever a candidate root URL is spotted for the tab.
TabContext.prototype.push = function(url, context) {
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { return; }
let committed = context !== undefined;
let count = this.stack.length;
let topEntry = this.stack[count - 1];
if ( topEntry && topEntry.url === url ) {
if ( committed ) {
topEntry.committed = true;
}
TabContext.prototype.push = function(url) {
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) {
return;
}
const count = this.stack.length;
if ( count !== 0 && this.stack[count - 1].url === url ) {
return;
}
this.stack.push(new StackEntry(url));
this.update();
if ( this.commitTimer !== null ) {
clearTimeout(this.commitTimer);
}
if ( committed ) {
this.stack = [new StackEntry(url, true)];
} else {
this.stack.push(new StackEntry(url));
this.commitTimer = vAPI.setTimeout(this.onCommit.bind(this), 1000);
this.commitTimer = vAPI.setTimeout(( ) => this.onCommit(), 500);
};
// This tells that the url is definitely the one to be associated with the
// tab, there is no longer any ambiguity about which root URL is really
// sitting in which tab.
TabContext.prototype.commit = function(url) {
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { return; }
if ( this.stack.length !== 0 ) {
const top = this.stack[this.stack.length - 1];
if ( top.url === url && top.committed ) { return false; }
}
this.stack = [new StackEntry(url, true)];
this.update();
µm.bindTabToPageStats(this.tabId, context);
return true;
};
TabContext.prototype.getNetFilteringSwitch = function() {
if ( this.netFilteringReadTime > µm.netWhitelistModifyTime ) {
return this.netFiltering;
}
// https://github.com/chrisaljoudi/uBlock/issues/1078
// Use both the raw and normalized URLs.
this.netFiltering = µm.getNetFilteringSwitch(this.normalURL);
if (
this.netFiltering &&
this.rawURL !== this.normalURL &&
this.rawURL !== ''
) {
this.netFiltering = µm.getNetFilteringSwitch(this.rawURL);
}
this.netFilteringReadTime = Date.now();
return this.netFiltering;
};
// These are to be used for the API of the tab context manager.
let push = function(tabId, url, context) {
const push = function(tabId, url) {
let entry = tabContexts.get(tabId);
if ( entry === undefined ) {
entry = new TabContext(tabId);
entry.autodestroy();
}
entry.push(url, context);
entry.push(url);
mostRecentRootDocURL = url;
mostRecentRootDocURLTimestamp = Date.now();
return entry;
};
// Find a tab context for a specific tab.
const lookup = function(tabId) {
return tabContexts.get(tabId) || null;
};
// Find a tab context for a specific tab. If none is found, attempt to
// fix this. When all fail, the behind-the-scene context is returned.
let mustLookup = function(tabId, url) {
let entry;
if ( url !== undefined ) {
entry = push(tabId, url);
} else {
entry = tabContexts.get(tabId);
}
const mustLookup = function(tabId) {
const entry = tabContexts.get(tabId);
if ( entry !== undefined ) {
return entry;
}
@ -347,7 +360,10 @@ housekeep itself.
// Google Hangout popup opens without a root frame. So for now we will
// just discard that best-guess root frame if it is too far in the
// future, at which point it ceases to be a "best guess".
if ( mostRecentRootDocURL !== '' && mostRecentRootDocURLTimestamp + 500 < Date.now() ) {
if (
mostRecentRootDocURL !== '' &&
mostRecentRootDocURLTimestamp + 500 < Date.now()
) {
mostRecentRootDocURL = '';
}
// https://github.com/chrisaljoudi/uBlock/issues/1001
@ -367,67 +383,139 @@ housekeep itself.
return tabContexts.get(vAPI.noTabId);
};
let lookup = function(tabId) {
return tabContexts.get(tabId) || null;
const commit = function(tabId, url) {
let entry = tabContexts.get(tabId);
if ( entry === undefined ) {
entry = push(tabId, url);
} else {
entry.commit(url);
}
return entry;
};
let tabIdFromURL = function(url) {
let tabIds = urlToTabIds.associations.get(url);
if ( tabIds === undefined ) { return -1; }
return tabIds[tabIds.length - 1];
const exists = function(tabId) {
return tabContexts.get(tabId) !== undefined;
};
// Behind-the-scene tab context
(function() {
let entry = new TabContext(vAPI.noTabId);
{
const entry = new TabContext(vAPI.noTabId);
entry.stack.push(new StackEntry('', true));
entry.rawURL = '';
entry.normalURL = µm.normalizePageURL(entry.tabId);
entry.rootHostname = µm.URI.hostnameFromURI(entry.normalURL);
entry.rootDomain = µm.URI.domainFromHostname(entry.rootHostname) || entry.rootHostname;
})();
// https://github.com/gorhill/uMatrix/issues/513
// Force a badge update here, it could happen that all the subsequent
// network requests are already in the page store, which would cause
// the badge to no be updated for these network requests.
vAPI.tabs.onNavigation = function(details) {
let tabId = details.tabId;
if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
push(tabId, details.url, 'newURL');
µm.updateBadgeAsync(tabId);
};
// https://github.com/gorhill/uMatrix/issues/872
// `changeInfo.url` may not always be available (Firefox).
entry.origin = µm.URI.originFromURI(entry.normalURL);
entry.rootHostname = µm.URI.hostnameFromURI(entry.origin);
entry.rootDomain = µm.URI.domainFromHostname(entry.rootHostname);
}
vAPI.tabs.onUpdated = function(tabId, changeInfo, tab) {
if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
if ( typeof tab.url !== 'string' || tab.url === '' ) { return; }
let url = changeInfo.url || tab.url;
if ( url ) {
push(tabId, url, 'updateURL');
// Context object, typically to be used to feed filtering engines.
const contextJunkyard = [];
const Context = class {
constructor(tabId) {
this.init(tabId);
}
init(tabId) {
const tabContext = lookup(tabId);
this.rootHostname = tabContext.rootHostname;
this.rootDomain = tabContext.rootDomain;
this.pageHostname =
this.pageDomain =
this.requestURL =
this.origin =
this.requestHostname =
this.requestDomain = '';
return this;
}
dispose() {
contextJunkyard.push(this);
}
};
vAPI.tabs.onClosed = function(tabId) {
µm.unbindTabFromPageStats(tabId);
let entry = tabContexts.get(tabId);
if ( entry instanceof TabContext ) {
entry.destroy();
const createContext = function(tabId) {
if ( contextJunkyard.length ) {
return contextJunkyard.pop().init(tabId);
}
return new Context(tabId);
};
return {
push: push,
lookup: lookup,
mustLookup: mustLookup,
tabIdFromURL: tabIdFromURL
push,
commit,
lookup,
mustLookup,
exists,
createContext,
onTabCreated,
};
})();
vAPI.tabs.registerListeners();
/******************************************************************************/
/******************************************************************************/
vAPI.Tabs = class extends vAPI.Tabs {
onActivated(details) {
super.onActivated(details);
if ( vAPI.isBehindTheSceneTabId(details.tabId) ) { return; }
// https://github.com/uBlockOrigin/uBlock-issues/issues/680
µMatrix.updateToolbarIcon(details.tabId);
//µMatrix.contextMenu.update(details.tabId);
}
onClosed(tabId) {
super.onClosed(tabId);
if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
µMatrix.unbindTabFromPageStats(tabId);
//µMatrix.contextMenu.update();
}
onCreated(details) {
super.onCreated(details);
µMatrix.tabContextManager.onTabCreated(details);
}
// When the DOM content of root frame is loaded, this means the tab
// content has changed.
//
// The webRequest.onBeforeRequest() won't be called for everything
// else than http/https. Thus, in such case, we will bind the tab as
// early as possible in order to increase the likelihood of a context
// properly setup if network requests are fired from within the tab.
// Example: Chromium + case #6 at
// http://raymondhill.net/ublock/popup.html
onNavigation(details) {
super.onNavigation(details);
const µm = µMatrix;
if ( details.frameId === 0 ) {
µm.tabContextManager.commit(details.tabId, details.url);
µm.bindTabToPageStats(details.tabId, 'tabCommitted');
}
if ( µm.canInjectScriptletsNow ) {
const pageStore = µm.pageStoreFromTabId(details.tabId);
if ( pageStore !== null && pageStore.getNetFilteringSwitch() ) {
µm.scriptletFilteringEngine.injectNow(details);
}
}
}
// It may happen the URL in the tab changes, while the page's document
// stays the same (for instance, Google Maps). Without this listener,
// the extension icon won't be properly refreshed.
onUpdated(tabId, changeInfo, tab) {
super.onUpdated(tabId, changeInfo, tab);
if ( typeof tab.url !== 'string' || tab.url === '' ) { return; }
if ( typeof changeInfo.url === 'string' && changeInfo.url !== '' ) {
µMatrix.tabContextManager.commit(tabId, changeInfo.url);
µMatrix.bindTabToPageStats(tabId, 'tabUpdated');
}
if ( typeof changeInfo.title === 'string' && changeInfo.title !== '' ) {
µMatrix.setPageStoreTitle(tabId, changeInfo.title);
}
}
};
vAPI.tabs = new vAPI.Tabs();
/******************************************************************************/
/******************************************************************************/
@ -435,12 +523,12 @@ vAPI.tabs.registerListeners();
// Create an entry for the tab if it doesn't exist
µm.bindTabToPageStats = function(tabId, context) {
this.updateBadgeAsync(tabId);
this.updateToolbarIcon(tabId);
// Do not create a page store for URLs which are of no interests
// Example: dev console
let tabContext = this.tabContextManager.lookup(tabId);
if ( tabContext === null ) { return; }
const tabContext = this.tabContextManager.lookup(tabId);
if ( tabContext === null ) { return null; }
// rhill 2013-11-24: Never ever rebind behind-the-scene
// virtual tab.
@ -449,7 +537,7 @@ vAPI.tabs.registerListeners();
return this.pageStores.get(tabId);
}
let normalURL = tabContext.normalURL;
const normalURL = tabContext.normalURL;
let pageStore = this.pageStores.get(tabId);
// The previous page URL, if any, associated with the tab
@ -460,18 +548,17 @@ vAPI.tabs.registerListeners();
}
// https://github.com/gorhill/uMatrix/issues/37
// Just rebind whenever possible: the URL changed, but the document
// maybe is the same.
// Example: Google Maps, Github
// Just rebind whenever possible: the URL changed, but the document
// maybe is the same.
// Example: Google Maps, Github
// https://github.com/gorhill/uMatrix/issues/72
// Need to double-check that the new scope is same as old scope
// Need to double-check that the new scope is same as old scope
if (
context === 'updateURL' &&
context === 'tabUpdated' &&
pageStore.pageHostname === tabContext.rootHostname
) {
pageStore.rawURL = tabContext.rawURL;
pageStore.pageUrl = normalURL;
this.updateTitle(tabId);
this.pageStoresToken = Date.now();
return pageStore;
}
@ -486,7 +573,6 @@ vAPI.tabs.registerListeners();
pageStore = this.pageStoreFactory(tabContext);
}
this.pageStores.set(tabId, pageStore);
this.updateTitle(tabId);
this.pageStoresToken = Date.now();
return pageStore;
@ -497,7 +583,7 @@ vAPI.tabs.registerListeners();
µm.unbindTabFromPageStats = function(tabId) {
if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
let pageStore = this.pageStores.get(tabId);
const pageStore = this.pageStores.get(tabId);
if ( pageStore === undefined ) { return; }
this.pageStores.delete(tabId);
@ -513,11 +599,13 @@ vAPI.tabs.registerListeners();
this.pageStoreCemetery.set(tabId, (pageStoreCrypt = new Map()));
}
let pageURL = pageStore.pageUrl;
const pageURL = pageStore.pageUrl;
pageStoreCrypt.set(pageURL, pageStore);
pageStore.incinerationTimer = vAPI.setTimeout(
this.incineratePageStore.bind(this, tabId, pageURL),
( ) => {
this.incineratePageStore(tabId, pageURL);
},
4 * 60 * 1000
);
};
@ -580,122 +668,86 @@ vAPI.tabs.registerListeners();
/******************************************************************************/
µm.setPageStoreTitle = function(tabId, title) {
const pageStore = this.pageStoreFromTabId(tabId);
if ( pageStore === null ) { return; }
if ( title === pageStore.title ) { return; }
pageStore.title = title;
this.pageStoresToken = Date.now();
};
/******************************************************************************/
µm.forceReload = function(tabId, bypassCache) {
vAPI.tabs.reload(tabId, bypassCache);
};
/******************************************************************************/
µm.updateBadgeAsync = (function() {
let tabIdToTimer = new Map();
µMatrix.updateToolbarIcon = (( ) => {
const µm = µMatrix;
const tabIdToDetails = new Map();
let updateBadge = function(tabId) {
tabIdToTimer.delete(tabId);
const updateBadge = tabId => {
let parts = tabIdToDetails.get(tabId);
tabIdToDetails.delete(tabId);
let badge = '';
let color = '#666';
let iconId = 'off';
let badgeStr = '';
let pageStore = this.pageStoreFromTabId(tabId);
let pageStore = µm.pageStoreFromTabId(tabId);
if ( pageStore !== null ) {
let total = pageStore.perLoadAllowedRequestCount +
pageStore.perLoadBlockedRequestCount;
if ( total ) {
let squareSize = 19;
let greenSize = squareSize * Math.sqrt(
const totalBlocked = pageStore.perLoadBlockedRequestCount;
const total = pageStore.perLoadAllowedRequestCount + totalBlocked;
const squareSize = 19;
if ( total !== 0 ) {
const greenSize = squareSize * Math.sqrt(
pageStore.perLoadAllowedRequestCount / total
);
iconId = greenSize < squareSize/2 ?
iconId = greenSize < squareSize / 2 ?
Math.ceil(greenSize) :
Math.floor(greenSize);
} else {
iconId = squareSize;
}
if (
this.userSettings.iconBadgeEnabled &&
pageStore.perLoadBlockedRequestCount !== 0
) {
badgeStr = this.formatCount(pageStore.perLoadBlockedRequestCount);
if ( totalBlocked !== 0 && (parts & 0b0010) !== 0 ) {
badge = µm.formatCount(totalBlocked);
}
}
vAPI.setIcon(
tabId,
'img/browsericons/icon19-' + iconId + '.png',
{ text: badgeStr, color: '#666' }
);
};
return function(tabId) {
if ( tabIdToTimer.has(tabId) ) { return; }
if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
tabIdToTimer.set(
tabId,
vAPI.setTimeout(updateBadge.bind(this, tabId), 750)
);
};
})();
/******************************************************************************/
µm.updateTitle = (function() {
let tabIdToTimer = new Map();
let tabIdToTryCount = new Map();
let delay = 499;
let tryNoMore = function(tabId) {
tabIdToTryCount.delete(tabId);
};
let tryAgain = function(tabId) {
let count = tabIdToTryCount.get(tabId);
if ( count === undefined ) { return false; }
if ( count === 1 ) {
tabIdToTryCount.delete(tabId);
return false;
}
tabIdToTryCount.set(tabId, count - 1);
tabIdToTimer.set(
tabId,
vAPI.setTimeout(updateTitle.bind(µm, tabId), delay)
);
return true;
};
var onTabReady = function(tabId, tab) {
if ( !tab ) {
return tryNoMore(tabId);
}
var pageStore = this.pageStoreFromTabId(tabId);
if ( pageStore === null ) {
return tryNoMore(tabId);
}
if ( !tab.title && tryAgain(tabId) ) {
return;
}
// https://github.com/gorhill/uMatrix/issues/225
// Sometimes title changes while page is loading.
var settled = tab.title && tab.title === pageStore.title;
pageStore.title = tab.title || tab.url || '';
this.pageStoresToken = Date.now();
if ( settled || !tryAgain(tabId) ) {
tryNoMore(tabId);
// https://www.reddit.com/r/uBlockOrigin/comments/d33d37/
if ( µm.userSettings.iconBadgeEnabled === false ) {
parts |= 0b1000;
}
};
var updateTitle = function(tabId) {
tabIdToTimer.delete(tabId);
vAPI.tabs.get(tabId, onTabReady.bind(this, tabId));
vAPI.setIcon(tabId, {
parts,
src: `/img/browsericons/icon19-${iconId}.png`,
badge,
color
});
};
return function(tabId) {
// parts: bit 0 = icon
// bit 1 = badge text
// bit 2 = badge color
// bit 3 = hide badge
return function(tabId, newParts = 0b0111) {
if ( typeof tabId !== 'number' ) { return; }
if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
let timer = tabIdToTimer.get(tabId);
if ( timer !== undefined ) {
clearTimeout(timer);
}
tabIdToTimer.set(
tabId,
vAPI.setTimeout(updateTitle.bind(this, tabId), delay)
);
tabIdToTryCount.set(tabId, 5);
const currentParts = tabIdToDetails.get(tabId);
if ( currentParts === newParts ) { return; }
if ( currentParts === undefined ) {
self.requestIdleCallback(
( ) => updateBadge(tabId),
{ timeout: 701 }
);
} else {
newParts |= currentParts;
}
tabIdToDetails.set(tabId, newParts);
};
})();
@ -704,28 +756,25 @@ vAPI.tabs.registerListeners();
// Stale page store entries janitor
// https://github.com/chrisaljoudi/uBlock/issues/455
(function() {
var cleanupPeriod = 7 * 60 * 1000;
var cleanupSampleAt = 0;
var cleanupSampleSize = 11;
var cleanup = function() {
var vapiTabs = vAPI.tabs;
var tabIds = Array.from(µm.pageStores.keys()).sort();
var checkTab = function(tabId) {
vapiTabs.get(tabId, function(tab) {
if ( !tab ) {
µm.unbindTabFromPageStats(tabId);
}
{
const cleanupPeriod = 7 * 60 * 1000;
const cleanupSampleSize = 11;
let cleanupSampleAt = 0;
const cleanup = function() {
const tabIds = Array.from(µm.pageStores.keys()).sort();
const checkTab = function(tabId) {
vAPI.tabs.get(tabId).then(tab => {
if ( tab instanceof Object ) { return; }
µm.unbindTabFromPageStats(tabId);
});
};
if ( cleanupSampleAt >= tabIds.length ) {
cleanupSampleAt = 0;
}
var tabId;
var n = Math.min(cleanupSampleAt + cleanupSampleSize, tabIds.length);
for ( var i = cleanupSampleAt; i < n; i++ ) {
tabId = tabIds[i];
const n = Math.min(cleanupSampleAt + cleanupSampleSize, tabIds.length);
for ( let i = cleanupSampleAt; i < n; i++ ) {
const tabId = tabIds[i];
if ( vAPI.isBehindTheSceneTabId(tabId) ) { continue; }
checkTab(tabId);
}
@ -735,7 +784,7 @@ vAPI.tabs.registerListeners();
};
vAPI.setTimeout(cleanup, cleanupPeriod);
})();
}
/******************************************************************************/

543
src/js/traffic.js

@ -25,45 +25,56 @@
// Start isolation from global scope
µMatrix.webRequest = (function() {
µMatrix.webRequest = (( ) => {
/******************************************************************************/
// Intercept and filter web requests according to white and black lists.
var onBeforeRootFrameRequestHandler = function(details) {
let µm = µMatrix;
let desURL = details.url;
let desHn = µm.URI.hostnameFromURI(desURL);
let type = requestTypeNormalizer[details.type] || 'other';
let tabId = details.tabId;
µm.tabContextManager.push(tabId, desURL);
let tabContext = µm.tabContextManager.mustLookup(tabId);
let srcHn = tabContext.rootHostname;
const onBeforeRootFrameRequestHandler = function(fctxt) {
const µm = µMatrix;
const desURL = fctxt.url;
const desHn = fctxt.getHostname();
const type = fctxt.type;
const tabId = fctxt.tabId;
const srcHn = fctxt.getTabHostname();
// Disallow request as per matrix?
let blocked = µm.mustBlock(srcHn, desHn, type);
let pageStore = µm.pageStoreFromTabId(tabId);
pageStore.recordRequest(type, desURL, blocked);
pageStore.perLoadAllowedRequestCount = 0;
pageStore.perLoadBlockedRequestCount = 0;
µm.logger.writeOne({ tabId, srcHn, desHn, desURL, type, blocked });
const blocked = µm.mustBlock(srcHn, desHn, type);
const pageStore = µm.bindTabToPageStats(tabId);
if ( pageStore !== null ) {
pageStore.recordRequest(type, desURL, blocked);
pageStore.perLoadAllowedRequestCount = 0;
pageStore.perLoadBlockedRequestCount = 0;
pageStore.perLoadBlockedReferrerCount = 0;
if ( blocked !== true ) {
µm.cookieHunter.recordPageCookies(pageStore);
}
if ( fctxt.aliasURL !== undefined ) {
pageStore.hasHostnameAliases = true;
}
}
if ( µm.logger.enabled ) {
fctxt.setRealm('network').setFilter(blocked).toLogger();
}
// Not blocked
if ( !blocked ) {
let redirectURL = maybeRedirectRootFrame(desHn, desURL);
if ( redirectURL !== desURL ) {
return { redirectUrl: redirectURL };
if ( blocked !== true ) {
const redirectUrl = maybeRedirectRootFrame(desHn, desURL);
if ( redirectUrl !== desURL ) {
return { redirectUrl };
}
if ( µm.tMatrix.evaluateSwitchZ('cname-reveal', srcHn) === false ) {
return { cancel: false };
}
µm.cookieHunter.recordPageCookies(pageStore);
return;
}
// Blocked
let query = btoa(JSON.stringify({ url: desURL, hn: desHn, type, why: '?' }));
const query = encodeURIComponent(
JSON.stringify({ url: desURL, hn: desHn, type, why: '?' })
);
vAPI.tabs.replace(tabId, vAPI.getURL('main-blocked.html?details=') + query);
@ -74,18 +85,18 @@ var onBeforeRootFrameRequestHandler = function(details) {
// https://twitter.com/thatcks/status/958776519765225473
var maybeRedirectRootFrame = function(hostname, url) {
let µm = µMatrix;
const maybeRedirectRootFrame = function(hostname, url) {
const µm = µMatrix;
if ( µm.rawSettings.enforceEscapedFragment !== true ) { return url; }
let block1pScripts = µm.mustBlock(hostname, hostname, 'script');
let reEscapedFragment = /[?&]_escaped_fragment_=/;
const block1pScripts = µm.mustBlock(hostname, hostname, 'script');
const reEscapedFragment = /[?&]_escaped_fragment_=/;
if ( reEscapedFragment.test(url) ) {
return block1pScripts ? url : url.replace(reEscapedFragment, '#!') ;
}
if ( block1pScripts === false ) { return url; }
let pos = url.indexOf('#!');
const pos = url.indexOf('#!');
if ( pos === -1 ) { return url; }
let separator = url.lastIndexOf('?', pos) === -1 ? '?' : '&';
const separator = url.lastIndexOf('?', pos) === -1 ? '?' : '&';
return url.slice(0, pos) +
separator + '_escaped_fragment_=' +
url.slice(pos + 2);
@ -95,21 +106,24 @@ var maybeRedirectRootFrame = function(hostname, url) {
// Intercept and filter web requests according to white and black lists.
var onBeforeRequestHandler = function(details) {
let µm = µMatrix,
µmuri = µm.URI,
desURL = details.url,
desScheme = µmuri.schemeFromURI(desURL);
const onBeforeRequestHandler = function(details) {
const µm = µMatrix;
const fctxt = µm.filteringContext.fromWebrequestDetails(details);
const µmuri = µm.URI;
const desURL = fctxt.url;
const desScheme = µmuri.schemeFromURI(desURL);
if ( µmuri.isNetworkScheme(desScheme) === false ) { return; }
if ( µmuri.isNetworkScheme(desScheme) === false ) {
return { cancel: false };
}
let type = requestTypeNormalizer[details.type] || 'other';
const type = fctxt.type;
// https://github.com/gorhill/httpswitchboard/issues/303
// Wherever the main doc comes from, create a receiver page URL: synthetize
// one if needed.
if ( type === 'doc' && details.parentFrameId === -1 ) {
return onBeforeRootFrameRequestHandler(details);
return onBeforeRootFrameRequestHandler(fctxt);
}
// Re-classify orphan HTTP requests as behind-the-scene requests. There is
@ -119,30 +133,11 @@ var onBeforeRequestHandler = function(details) {
// to scope on unknown scheme? Etc.
// https://github.com/gorhill/httpswitchboard/issues/191
// https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
let tabContext = µm.tabContextManager.mustLookup(details.tabId),
tabId = tabContext.tabId,
srcHn = tabContext.rootHostname,
desHn = µmuri.hostnameFromURI(desURL),
docURL = details.documentUrl,
specificity = 0;
if ( docURL !== undefined ) {
// Extract context from initiator for behind-the-scene requests.
if ( tabId < 0 ) {
srcHn = µmuri.hostnameFromURI(µm.normalizePageURL(0, docURL));
}
// https://github.com/uBlockOrigin/uMatrix-issues/issues/72
// Workaround of weird Firefox behavior: when a service worker exists
// for a site, the `doc` requests when loading a page from that site
// are not being made: this potentially prevents uMatrix to properly
// keep track of the context in which requests are made.
else if (
details.parentFrameId === -1 &&
docURL !== tabContext.rawURL
) {
srcHn = µmuri.hostnameFromURI(µm.normalizePageURL(0, docURL));
}
}
const tabContext = µm.tabContextManager.mustLookup(details.tabId);
const tabId = fctxt.tabId;
const srcHn = fctxt.getTabHostname();
const desHn = fctxt.getHostname();
let specificity = 0;
let blocked = µm.tMatrix.mustBlock(srcHn, desHn, type);
if ( blocked ) {
@ -155,7 +150,7 @@ var onBeforeRequestHandler = function(details) {
// processing has already been performed, and that a synthetic URL has
// been constructed for logging purpose. Use this synthetic URL if
// it is available.
let pageStore = µm.mustPageStoreFromTabId(tabId);
const pageStore = µm.mustPageStoreFromTabId(tabId);
// Enforce strict secure connection?
if ( tabContext.secure && µmuri.isSecureScheme(desScheme) === false ) {
@ -165,14 +160,22 @@ var onBeforeRequestHandler = function(details) {
}
}
if ( fctxt.aliasURL !== undefined ) {
pageStore.hasHostnameAliases = true;
}
pageStore.recordRequest(type, desURL, blocked);
if ( µm.logger.enabled ) {
µm.logger.writeOne({ tabId, srcHn, desHn, desURL, type, blocked });
fctxt.setRealm('network').setFilter(blocked).toLogger();
}
if ( blocked ) {
pageStore.cacheBlockedCollapsible(type, desURL, specificity);
return { 'cancel': true };
return { cancel: true };
}
if ( µm.tMatrix.evaluateSwitchZ('cname-reveal', srcHn) === false ) {
return { cancel: false };
}
};
@ -180,36 +183,15 @@ var onBeforeRequestHandler = function(details) {
// Sanitize outgoing headers as per user settings.
var onBeforeSendHeadersHandler = function(details) {
let µm = µMatrix,
µmuri = µm.URI,
desURL = details.url,
desScheme = µmuri.schemeFromURI(desURL);
const onBeforeSendHeadersHandler = function(details) {
const µm = µMatrix;
const µmuri = µm.URI;
const fctxt = µm.filteringContext.fromWebrequestDetails(details);
// Ignore non-network schemes
if ( µmuri.isNetworkScheme(desScheme) === false ) { return; }
// Re-classify orphan HTTP requests as behind-the-scene requests. There is
// not much else which can be done, because there are URLs
// which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
// as this would lead to complications with no obvious solution, like how
// to scope on unknown scheme? Etc.
// https://github.com/gorhill/httpswitchboard/issues/191
// https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
const tabId = details.tabId;
const pageStore = µm.mustPageStoreFromTabId(tabId);
const desHn = µmuri.hostnameFromURI(desURL);
const requestType = requestTypeNormalizer[details.type] || 'other';
const requestHeaders = details.requestHeaders;
// https://github.com/uBlockOrigin/uMatrix-issues/issues/155
// https://github.com/uBlockOrigin/uMatrix-issues/issues/159
// TODO: import all filtering context improvements from uBO.
const srcHn = tabId < 0 ||
details.parentFrameId < 0 ||
details.parentFrameId === 0 && details.type === 'sub_frame'
? µmuri.hostnameFromURI(details.documentUrl) || pageStore.pageHostname
: pageStore.pageHostname;
if ( µmuri.isNetworkScheme(µmuri.schemeFromURI(fctxt.url)) === false ) {
return;
}
// https://github.com/gorhill/httpswitchboard/issues/342
// Is this hyperlink auditing?
@ -232,45 +214,19 @@ var onBeforeSendHeadersHandler = function(details) {
// With hyperlink-auditing, removing header(s) is pointless, the whole
// request must be cancelled.
let headerIndex = headerIndexFromName('ping-to', requestHeaders);
if ( headerIndex !== -1 ) {
let headerValue = requestHeaders[headerIndex].value;
if ( headerValue !== '' ) {
let blocked = µm.userSettings.processHyperlinkAuditing;
pageStore.recordRequest('other', desURL + '{Ping-To:' + headerValue + '}', blocked);
µm.logger.writeOne({ tabId, srcHn, desHn, desURL, type: 'ping', blocked });
if ( blocked ) {
µm.hyperlinkAuditingFoiledCounter += 1;
return { 'cancel': true };
}
}
if ( onBeforeSendPing(fctxt, details) ) {
return { cancel: true };
}
// If we reach this point, request is not blocked, so what is left to do
// is to sanitize headers.
let modified = false;
// Process `Cookie` header.
headerIndex = headerIndexFromName('cookie', requestHeaders);
if (
headerIndex !== -1 &&
µm.mustBlock(srcHn, desHn, 'cookie')
) {
if ( onBeforeSendCookie(fctxt, details) ) {
modified = true;
let headerValue = requestHeaders[headerIndex].value;
requestHeaders.splice(headerIndex, 1);
µm.cookieHeaderFoiledCounter++;
if ( requestType === 'doc' ) {
pageStore.perLoadBlockedRequestCount++;
µm.logger.writeOne({
tabId,
srcHn,
header: { name: 'COOKIE', value: headerValue },
change: -1
});
}
}
// Process `Referer` header.
@ -293,50 +249,132 @@ var onBeforeSendHeadersHandler = function(details) {
// https://github.com/gorhill/uMatrix/issues/773
// For non-GET requests, remove `Referer` header instead of spoofing it.
headerIndex = headerIndexFromName('referer', requestHeaders);
if ( headerIndex !== -1 ) {
let headerValue = requestHeaders[headerIndex].value;
if ( headerValue !== '' ) {
let toDomain = µmuri.domainFromHostname(desHn);
if ( toDomain !== '' && toDomain !== µmuri.domainFromURI(headerValue) ) {
pageStore.has3pReferrer = true;
if ( µm.tMatrix.evaluateSwitchZ('referrer-spoof', srcHn) ) {
modified = true;
let newValue;
if ( details.method === 'GET' ) {
newValue = requestHeaders[headerIndex].value =
desScheme + '://' + desHn + '/';
} else {
requestHeaders.splice(headerIndex, 1);
}
if ( pageStore.perLoadBlockedReferrerCount === 0 ) {
pageStore.perLoadBlockedRequestCount += 1;
µm.logger.writeOne({
tabId,
srcHn,
header: { name: 'REFERER', value: headerValue },
change: -1
});
if ( newValue !== undefined ) {
µm.logger.writeOne({
tabId,
srcHn,
header: { name: 'REFERER', value: newValue },
change: +1
});
}
}
pageStore.perLoadBlockedReferrerCount += 1;
}
}
}
if ( onBeforeSendReferrer(fctxt, details) ) {
modified = true;
}
if ( modified !== true ) { return; }
µm.updateBadgeAsync(tabId);
µm.updateToolbarIcon(fctxt.tabId);
return { requestHeaders: requestHeaders };
return { requestHeaders: details.requestHeaders };
};
/******************************************************************************/
const onBeforeSendPing = function(fctxt, details) {
const requestHeaders = details.requestHeaders;
const iHeader = headerIndexFromName('ping-to', requestHeaders);
if ( iHeader === -1 ) { return false; }
const headerValue = requestHeaders[iHeader].value;
if ( headerValue === '' ) { return false; }
const µm = µMatrix;
const blocked = µm.userSettings.processHyperlinkAuditing;
const pageStore = µm.mustPageStoreFromTabId(fctxt.tabId);
pageStore.recordRequest(
'other',
fctxt.url + '{Ping-To:' + headerValue + '}',
blocked
);
if ( µm.logger.enabled ) {
fctxt.setRealm('network')
.setType('ping')
.setFilter(blocked)
.toLogger();
}
if ( blocked === false ) { return false; }
µm.hyperlinkAuditingFoiledCounter += 1;
return true;
};
/******************************************************************************/
const onBeforeSendCookie = function(fctxt, details) {
const requestHeaders = details.requestHeaders;
const iHeader = headerIndexFromName('cookie', requestHeaders);
if ( iHeader === -1 ) { return false; }
const µm = µMatrix;
const blocked = µm.mustBlock(
fctxt.getTabHostname(),
fctxt.getHostname(),
'cookie'
);
if ( blocked === false ) { return false; }
const headerValue = requestHeaders[iHeader].value;
requestHeaders.splice(iHeader, 1);
µm.cookieHeaderFoiledCounter++;
if ( fctxt.type === 'doc' ) {
const pageStore = µm.mustPageStoreFromTabId(fctxt.tabId);
pageStore.perLoadBlockedRequestCount++;
if ( µm.logger.enabled ) {
fctxt.setRealm('network')
.setType('COOKIE')
.setFilter({ value: headerValue, change: -1 })
.toLogger();
}
}
return true;
};
/******************************************************************************/
const onBeforeSendReferrer = function(fctxt, details) {
const requestHeaders = details.requestHeaders;
const iHeader = headerIndexFromName('referer', requestHeaders);
if ( iHeader === -1 ) { return false; }
const referrer = requestHeaders[iHeader].value;
if ( referrer === '' ) { return false; }
const toDomain = vAPI.domainFromHostname(fctxt.getHostname());
if ( toDomain === '' || toDomain === vAPI.domainFromURI(referrer) ) {
return false;
}
const µm = µMatrix;
const pageStore = µm.mustPageStoreFromTabId(fctxt.tabId);
pageStore.has3pReferrer = true;
const mustSpoof =
µm.tMatrix.evaluateSwitchZ('referrer-spoof', fctxt.getTabHostname());
if ( mustSpoof === false ) { return false; }
let spoofedReferrer;
if ( details.method === 'GET' ) {
spoofedReferrer = requestHeaders[iHeader].value =
fctxt.originFromURI(fctxt.url) + '/';
} else {
requestHeaders.splice(iHeader, 1);
}
if ( pageStore.perLoadBlockedReferrerCount === 0 ) {
pageStore.perLoadBlockedRequestCount += 1;
if ( µm.logger.enabled ) {
fctxt.setRealm('network')
.setType('REFERER')
.setFilter({ value: referrer, change: -1 })
.toLogger();
if ( spoofedReferrer !== undefined ) {
fctxt.setRealm('network')
.setType('REFERER')
.setFilter({ value: spoofedReferrer, change: +1 })
.toLogger();
}
}
}
pageStore.perLoadBlockedReferrerCount += 1;
return true;
};
/******************************************************************************/
@ -349,32 +387,26 @@ var onBeforeSendHeadersHandler = function(details) {
// This fixes:
// https://github.com/gorhill/httpswitchboard/issues/35
var onHeadersReceivedHandler = function(details) {
// Ignore schemes other than 'http...'
let µm = µMatrix,
tabId = details.tabId,
requestURL = details.url,
requestType = requestTypeNormalizer[details.type] || 'other',
headers = details.responseHeaders;
const onHeadersReceivedHandler = function(details) {
const µm = µMatrix;
const fctxt = µm.filteringContext.fromWebrequestDetails(details);
const requestType = fctxt.type;
const headers = details.responseHeaders;
// https://github.com/gorhill/uMatrix/issues/145
// Check if the main_frame is a download
if ( requestType === 'doc' ) {
µm.tabContextManager.push(tabId, requestURL);
let contentType = typeFromHeaders(headers);
const contentType = typeFromHeaders(headers);
if ( contentType !== undefined ) {
details.type = contentType;
return onBeforeRootFrameRequestHandler(details);
return onBeforeRootFrameRequestHandler(fctxt);
}
}
let tabContext = µm.tabContextManager.lookup(tabId);
if ( tabContext === null ) { return; }
let csp = [],
cspReport = [],
srcHn = tabContext.rootHostname,
desHn = µm.URI.hostnameFromURI(requestURL);
const csp = [];
const cspReport = [];
const srcHn = fctxt.getTabHostname();
const desHn = fctxt.getHostname();
// Inline script tags.
if ( µm.mustBlock(srcHn, desHn, 'script' ) ) {
@ -401,7 +433,7 @@ var onHeadersReceivedHandler = function(details) {
// them here.
if ( csp.length !== 0 ) {
let cspRight = csp.join(', ');
const cspRight = csp.join(', ');
let cspTotal = cspRight;
if ( µm.cantMergeCSPHeaders ) {
let i = headerIndexFromName(
@ -417,18 +449,16 @@ var onHeadersReceivedHandler = function(details) {
name: 'Content-Security-Policy',
value: cspTotal
});
if ( requestType === 'doc' ) {
µm.logger.writeOne({
tabId,
srcHn,
header: { name: 'CSP', value: cspRight },
change: +1
});
if ( µm.logger.enabled && requestType === 'doc' ) {
fctxt.setRealm('network')
.setType('CSP')
.setFilter({ value: cspRight, change: +1 })
.toLogger();
}
}
if ( cspReport.length !== 0 ) {
let cspRight = cspReport.join(', ');
const cspRight = cspReport.join(', ');
let cspTotal = cspRight;
if ( µm.cantMergeCSPHeaders ) {
let i = headerIndexFromName(
@ -469,8 +499,8 @@ window.addEventListener('webextFlavor', function() {
// Caller must ensure headerName is normalized to lower case.
var headerIndexFromName = function(headerName, headers) {
var i = headers.length;
const headerIndexFromName = function(headerName, headers) {
let i = headers.length;
while ( i-- ) {
if ( headers[i].name.toLowerCase() === headerName ) {
return i;
@ -483,33 +513,16 @@ var headerIndexFromName = function(headerName, headers) {
// Extract request type from content headers.
let typeFromHeaders = function(headers) {
let i = headerIndexFromName('content-type', headers);
const typeFromHeaders = function(headers) {
const i = headerIndexFromName('content-type', headers);
if ( i === -1 ) { return; }
let mime = headers[i].value.toLowerCase();
const mime = headers[i].value.toLowerCase();
if ( mime.startsWith('image/') ) { return 'image'; }
if ( mime.startsWith('video/') || mime.startsWith('audio/') ) {
return 'media';
}
};
/******************************************************************************/
var requestTypeNormalizer = {
'font' : 'css',
'image' : 'image',
'imageset' : 'image',
'main_frame' : 'doc',
'media' : 'media',
'object' : 'media',
'other' : 'other',
'script' : 'script',
'stylesheet' : 'css',
'sub_frame' : 'frame',
'websocket' : 'xhr',
'xmlhttprequest': 'xhr'
};
/*******************************************************************************
Use a `http-equiv` `meta` tag to enforce CSP directives for documents
@ -517,11 +530,11 @@ var requestTypeNormalizer = {
handler to be called).
Idea borrowed from NoScript:
https://github.com/hackademix/noscript/commit/6e80d3f130773fc9a9123c5c4c2e97d63e90fa2a
https://github.com/hackademix/noscript/commit/6e80d3f13077
**/
(function() {
(( ) => {
if (
typeof self.browser !== 'object' ||
typeof browser.contentScripts !== 'object'
@ -529,7 +542,7 @@ var requestTypeNormalizer = {
return;
}
let csRules = [
const csRules = [
{
name: 'script',
file: '/js/contentscript-no-inline-script.js',
@ -539,7 +552,7 @@ var requestTypeNormalizer = {
},
];
let csSwitches = [
const csSwitches = [
{
name: 'no-workers',
file: '/js/contentscript-no-workers.js',
@ -549,7 +562,7 @@ var requestTypeNormalizer = {
},
];
let register = function(entry) {
const register = function(entry) {
if ( entry.pending !== undefined ) { return; }
entry.pending = browser.contentScripts.register({
js: [ { file: entry.file } ],
@ -569,16 +582,16 @@ var requestTypeNormalizer = {
);
};
let unregister = function(entry) {
const unregister = function(entry) {
if ( entry.registered === undefined ) { return; }
entry.registered.unregister();
entry.registered = undefined;
};
let handler = function(ev) {
let matrix = ev && ev.detail;
const handler = function(ev) {
const matrix = ev && ev.detail;
if ( matrix !== µMatrix.tMatrix ) { return; }
for ( let cs of csRules ) {
for ( const cs of csRules ) {
cs.mustRegister = matrix.mustBlock('file-scheme', 'file-scheme', cs.name);
if ( cs.mustRegister === (cs.registered !== undefined) ) { continue; }
if ( cs.mustRegister ) {
@ -587,7 +600,7 @@ var requestTypeNormalizer = {
unregister(cs);
}
}
for ( let cs of csSwitches ) {
for ( const cs of csSwitches ) {
cs.mustRegister = matrix.evaluateSwitchZ(cs.name, 'file-scheme');
if ( cs.mustRegister === (cs.registered !== undefined) ) { continue; }
if ( cs.mustRegister ) {
@ -603,58 +616,52 @@ var requestTypeNormalizer = {
/******************************************************************************/
const start = (function() {
if (
vAPI.net.onBeforeReady instanceof Object &&
(
vAPI.net.onBeforeReady.experimental !== true ||
µMatrix.rawSettings.suspendTabsUntilReady
)
) {
vAPI.net.onBeforeReady.start();
}
return {
start: (( ) => {
vAPI.net = new vAPI.Net();
return function() {
vAPI.net.addListener(
'onBeforeRequest',
onBeforeRequestHandler,
{ },
[ 'blocking' ]
);
// https://github.com/uBlockOrigin/uMatrix-issues/issues/74#issuecomment-450687707
// https://groups.google.com/a/chromium.org/forum/#!topic/chromium-extensions/vYIaeezZwfQ
// Chromium 72+: use `extraHeaders` to keep the ability to access
// the `Cookie`, `Referer` headers.
const beforeSendHeadersExtra = [ 'blocking', 'requestHeaders' ];
const wrObsho = browser.webRequest.OnBeforeSendHeadersOptions;
if (
wrObsho instanceof Object &&
wrObsho.hasOwnProperty('EXTRA_HEADERS')
vAPI.net.canSuspend() &&
µMatrix.rawSettings.suspendTabsUntilReady !== 'no' ||
vAPI.net.canSuspend() !== true &&
µMatrix.rawSettings.suspendTabsUntilReady === 'yes'
) {
beforeSendHeadersExtra.push(wrObsho.EXTRA_HEADERS);
}
vAPI.net.addListener(
'onBeforeSendHeaders',
onBeforeSendHeadersHandler,
{ },
beforeSendHeadersExtra
);
vAPI.net.addListener(
'onHeadersReceived',
onHeadersReceivedHandler,
{ types: [ 'main_frame', 'sub_frame' ] },
[ 'blocking', 'responseHeaders' ]
);
if ( vAPI.net.onBeforeReady instanceof Object ) {
vAPI.net.onBeforeReady.stop(onBeforeRequestHandler);
vAPI.net.suspend(true);
}
};
})();
return { start };
return function() {
vAPI.net.setSuspendableListener(onBeforeRequestHandler);
// https://github.com/uBlockOrigin/uMatrix-issues/issues/74#issuecomment-450687707
// https://groups.google.com/a/chromium.org/forum/#!topic/chromium-extensions/vYIaeezZwfQ
// Chromium 72+: use `extraHeaders` to keep the ability to access
// the `Cookie`, `Referer` headers.
const beforeSendHeadersExtra = [ 'blocking', 'requestHeaders' ];
const wrObsho = browser.webRequest.OnBeforeSendHeadersOptions;
if (
wrObsho instanceof Object &&
wrObsho.hasOwnProperty('EXTRA_HEADERS')
) {
beforeSendHeadersExtra.push(wrObsho.EXTRA_HEADERS);
}
vAPI.net.addListener(
'onBeforeSendHeaders',
onBeforeSendHeadersHandler,
{ },
beforeSendHeadersExtra
);
vAPI.net.addListener(
'onHeadersReceived',
onHeadersReceivedHandler,
{
types: [ 'main_frame', 'sub_frame' ],
urls: [ 'http://*/*', 'https://*/*' ],
},
[ 'blocking', 'responseHeaders' ]
);
vAPI.net.unsuspend(true);
};
})(),
};
/******************************************************************************/

171
src/js/uritools.js

@ -19,8 +19,6 @@
Home: https://github.com/gorhill/uMatrix
*/
/* global publicSuffixList, punycode */
'use strict';
/*******************************************************************************
@ -44,13 +42,11 @@ Naming convention from https://en.wikipedia.org/wiki/URI_scheme#Examples
// <http://jsperf.com/old-uritools-vs-new-uritools>
// Performance improvements welcomed.
// jsperf: <http://jsperf.com/old-uritools-vs-new-uritools>
var reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/;
const reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/;
// Derived
var reSchemeFromURI = /^[^:\/?#]+:/;
var reAuthorityFromURI = /^(?:[^:\/?#]+:)?(\/\/[^\/?#]+)/;
var reCommonHostnameFromURL = /^https?:\/\/([0-9a-z_][0-9a-z._-]*[0-9a-z])\//;
var reMustNormalizeHostname = /[^0-9a-z._-]/;
const reSchemeFromURI = /^[a-z][0-9a-z+.-]+:/;
const reOriginFromURI = /^(?:[^:\/?#]+:)\/\/[^\/?#]+/;
// These are to parse authority field, not parsed by above official regex
// IPv6 is seen as an exception: a non-compatible IPv6 is first tried, and
@ -60,15 +56,10 @@ var reMustNormalizeHostname = /[^0-9a-z._-]/;
// https://github.com/gorhill/httpswitchboard/issues/211
// "While a hostname may not contain other characters, such as the
// "underscore character (_), other DNS names may contain the underscore"
var reHostPortFromAuthority = /^(?:[^@]*@)?([^:]*)(:\d*)?$/;
var reIPv6PortFromAuthority = /^(?:[^@]*@)?(\[[0-9a-f:]*\])(:\d*)?$/i;
var reHostFromNakedAuthority = /^[0-9a-z._-]+[0-9a-z]$/i;
var reHostFromAuthority = /^(?:[^@]*@)?([^:]+)(?::\d*)?$/;
var reIPv6FromAuthority = /^(?:[^@]*@)?(\[[0-9a-f:]+\])(?::\d*)?$/i;
const reHostPortFromAuthority = /^(?:[^@]*@)?([^:]*)(:\d*)?$/;
const reIPv6PortFromAuthority = /^(?:[^@]*@)?(\[[0-9a-f:]*\])(:\d*)?$/i;
// Coarse (but fast) tests
var reIPAddressNaive = /^\d+\.\d+\.\d+\.\d+$|^\[[\da-zA-Z:]+\]$/;
const reHostFromNakedAuthority = /^[0-9a-z._-]+[0-9a-z]$/i;
// Accurate tests
// Source.: http://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp/5284410#5284410
@ -103,7 +94,7 @@ var resetAuthority = function(o) {
// This will be exported
var URI = {
const URI = {
scheme: '',
authority: '',
hostname: '',
@ -226,11 +217,16 @@ URI.assemble = function(bits) {
/******************************************************************************/
URI.originFromURI = function(uri) {
const matches = reOriginFromURI.exec(uri);
return matches !== null ? matches[0].toLowerCase() : '';
};
/******************************************************************************/
URI.schemeFromURI = function(uri) {
var matches = reSchemeFromURI.exec(uri);
if ( matches === null ) {
return '';
}
const matches = reSchemeFromURI.exec(uri);
if ( matches === null ) { return ''; }
return matches[0].slice(0, -1).toLowerCase();
};
@ -252,138 +248,9 @@ URI.reSecureScheme = /^(?:https|wss|ftps)\b/;
/******************************************************************************/
// The most used function, so it better be fast.
// https://github.com/gorhill/uBlock/issues/1559
// See http://en.wikipedia.org/wiki/FQDN
// https://bugzilla.mozilla.org/show_bug.cgi?id=1360285
// Revisit punycode dependency when above issue is fixed in Firefox.
URI.hostnameFromURI = function(uri) {
var matches = reCommonHostnameFromURL.exec(uri);
if ( matches !== null ) { return matches[1]; }
matches = reAuthorityFromURI.exec(uri);
if ( matches === null ) { return ''; }
var authority = matches[1].slice(2);
// Assume very simple authority (most common case for µBlock)
if ( reHostFromNakedAuthority.test(authority) ) {
return authority.toLowerCase();
}
matches = reHostFromAuthority.exec(authority);
if ( matches === null ) {
matches = reIPv6FromAuthority.exec(authority);
if ( matches === null ) { return ''; }
}
var hostname = matches[1];
while ( hostname.endsWith('.') ) {
hostname = hostname.slice(0, -1);
}
if ( reMustNormalizeHostname.test(hostname) ) {
hostname = punycode.toASCII(hostname.toLowerCase());
}
return hostname;
};
/******************************************************************************/
URI.domainFromHostname = function(hostname) {
// Try to skip looking up the PSL database
var entry = domainCache.get(hostname);
if ( entry !== undefined ) {
entry.tstamp = Date.now();
return entry.domain;
}
// Meh.. will have to search it
if ( reIPAddressNaive.test(hostname) === false ) {
return domainCacheAdd(hostname, psl.getDomain(hostname));
}
return domainCacheAdd(hostname, hostname);
};
// It is expected that there is higher-scoped `publicSuffixList` lingering
// somewhere. Cache it. See <https://github.com/gorhill/publicsuffixlist.js>.
var psl = publicSuffixList;
/******************************************************************************/
// Trying to alleviate the worries of looking up too often the domain name from
// a hostname. With a cache, uBlock benefits given that it deals with a
// specific set of hostnames within a narrow time span -- in other words, I
// believe probability of cache hit are high in uBlock.
var domainCache = new Map();
var domainCacheCountLowWaterMark = 75;
var domainCacheCountHighWaterMark = 100;
var domainCacheEntryJunkyard = [];
var domainCacheEntryJunkyardMax = domainCacheCountHighWaterMark - domainCacheCountLowWaterMark;
var DomainCacheEntry = function(domain) {
this.init(domain);
};
DomainCacheEntry.prototype.init = function(domain) {
this.domain = domain;
this.tstamp = Date.now();
return this;
};
DomainCacheEntry.prototype.dispose = function() {
this.domain = '';
if ( domainCacheEntryJunkyard.length < domainCacheEntryJunkyardMax ) {
domainCacheEntryJunkyard.push(this);
}
};
var domainCacheEntryFactory = function(domain) {
var entry = domainCacheEntryJunkyard.pop();
if ( entry ) {
return entry.init(domain);
}
return new DomainCacheEntry(domain);
};
var domainCacheAdd = function(hostname, domain) {
var entry = domainCache.get(hostname);
if ( entry !== undefined ) {
entry.tstamp = Date.now();
} else {
domainCache.set(hostname, domainCacheEntryFactory(domain));
if ( domainCache.size === domainCacheCountHighWaterMark ) {
domainCachePrune();
}
}
return domain;
};
var domainCacheEntrySort = function(a, b) {
return domainCache.get(b).tstamp - domainCache.get(a).tstamp;
};
var domainCachePrune = function() {
var hostnames = Array.from(domainCache.keys())
.sort(domainCacheEntrySort)
.slice(domainCacheCountLowWaterMark);
var i = hostnames.length;
var hostname;
while ( i-- ) {
hostname = hostnames[i];
domainCache.get(hostname).dispose();
domainCache.delete(hostname);
}
};
window.addEventListener('publicSuffixList', function() {
domainCache.clear();
});
/******************************************************************************/
URI.domainFromURI = function(uri) {
if ( !uri ) {
return '';
}
return this.domainFromHostname(this.hostnameFromURI(uri));
};
URI.hostnameFromURI = vAPI.hostnameFromURI;
URI.domainFromHostname = vAPI.domainFromHostname;
URI.domainFromURI = vAPI.domainFromURI;
/******************************************************************************/

240
src/js/user-rules.js

@ -25,21 +25,22 @@
/******************************************************************************/
(function() {
{
// >>>>> start of local scope
/******************************************************************************/
// Move to dashboard-common.js if needed
(function() {
{
let timer;
let resize = function() {
const resize = ( ) => {
timer = undefined;
let child = document.querySelector('.vfill-available');
const child = document.querySelector('.vfill-available');
if ( child === null ) { return; }
let prect = document.documentElement.getBoundingClientRect();
let crect = child.getBoundingClientRect();
let cssHeight = Math.max(prect.bottom - crect.top, 80) + 'px';
const prect = document.documentElement.getBoundingClientRect();
const crect = child.getBoundingClientRect();
const cssHeight = Math.max(prect.bottom - crect.top, 80) + 'px';
if ( child.style.height !== cssHeight ) {
child.style.height = cssHeight;
if ( typeof mergeView !== 'undefined' ) {
@ -48,7 +49,7 @@
}
}
};
let resizeAsync = function(delay) {
const resizeAsync = function(delay) {
if ( timer === undefined ) {
timer = vAPI.setTimeout(
resize,
@ -57,17 +58,17 @@
}
};
window.addEventListener('resize', resizeAsync);
var observer = new MutationObserver(resizeAsync);
const observer = new MutationObserver(resizeAsync);
observer.observe(document.querySelector('.body'), {
childList: true,
subtree: true
});
resizeAsync(1);
})();
}
/******************************************************************************/
var mergeView = new CodeMirror.MergeView(
const mergeView = new CodeMirror.MergeView(
document.querySelector('.codeMirrorMergeContainer'),
{
allowEditingOriginals: true,
@ -84,15 +85,15 @@ mergeView.editor().setOption('styleActiveLine', true);
mergeView.editor().setOption('lineNumbers', false);
mergeView.leftOriginal().setOption('readOnly', 'nocursor');
var unfilteredRules = {
const unfilteredRules = {
orig: { doc: mergeView.leftOriginal(), rules: [] },
edit: { doc: mergeView.editor(), rules: [] }
};
var cleanEditToken = 0;
var cleanEditText = '';
let cleanEditToken = 0;
let cleanEditText = '';
var differ;
let differ;
/******************************************************************************/
@ -100,13 +101,13 @@ var differ;
// https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js#L22
// ... and modified as needed.
var updateOverlay = (function() {
var reFilter;
var mode = {
const updateOverlay = (function() {
let reFilter;
const mode = {
token: function(stream) {
if ( reFilter !== undefined ) {
reFilter.lastIndex = stream.pos;
var match = reFilter.exec(stream.string);
const match = reFilter.exec(stream.string);
if ( match !== null ) {
if ( match.index === stream.pos ) {
stream.pos += match[0].length || 1;
@ -133,36 +134,36 @@ var updateOverlay = (function() {
// - Scroll position preserved
// - Minimum amount of text updated
var rulesToDoc = function(clearHistory) {
for ( var key in unfilteredRules ) {
const rulesToDoc = function(clearHistory) {
for ( const key in unfilteredRules ) {
if ( unfilteredRules.hasOwnProperty(key) === false ) { continue; }
var doc = unfilteredRules[key].doc;
var rules = filterRules(key);
const doc = unfilteredRules[key].doc;
const rules = filterRules(key);
if ( doc.lineCount() === 1 && doc.getValue() === '' || rules.length === 0 ) {
doc.setValue(rules.length !== 0 ? rules.join('\n') : '');
continue;
}
if ( differ === undefined ) { differ = new diff_match_patch(); }
var beforeText = doc.getValue();
var afterText = rules.join('\n');
var diffs = differ.diff_main(beforeText, afterText);
const beforeText = doc.getValue();
const afterText = rules.join('\n');
const diffs = differ.diff_main(beforeText, afterText);
doc.startOperation();
var i = diffs.length,
iedit = beforeText.length;
let i = diffs.length;
let iedit = beforeText.length;
while ( i-- ) {
var diff = diffs[i];
const diff = diffs[i];
if ( diff[0] === 0 ) {
iedit -= diff[1].length;
continue;
}
var end = doc.posFromIndex(iedit);
const end = doc.posFromIndex(iedit);
if ( diff[0] === 1 ) {
doc.replaceRange(diff[1], end, end);
continue;
}
/* diff[0] === -1 */
iedit -= diff[1].length;
var beg = doc.posFromIndex(iedit);
const beg = doc.posFromIndex(iedit);
doc.replaceRange('', beg, end);
}
doc.endOperation();
@ -176,12 +177,12 @@ var rulesToDoc = function(clearHistory) {
/******************************************************************************/
var filterRules = function(key) {
var rules = unfilteredRules[key].rules;
var filter = uDom('#ruleFilter input').val();
const filterRules = function(key) {
const filter = uDom('#ruleFilter input').val();
let rules = unfilteredRules[key].rules;
if ( filter !== '' ) {
rules = rules.slice();
var i = rules.length;
let i = rules.length;
while ( i-- ) {
if ( rules[i].indexOf(filter) === -1 ) {
rules.splice(i, 1);
@ -193,25 +194,20 @@ var filterRules = function(key) {
/******************************************************************************/
var renderRules = (function() {
var firstVisit = true;
return function(details) {
unfilteredRules.orig.rules = details.permanentRules.sort(directiveSort);
unfilteredRules.edit.rules = details.temporaryRules.sort(directiveSort);
rulesToDoc(firstVisit);
if ( firstVisit ) {
firstVisit = false;
mergeView.editor().execCommand('goNextDiff');
}
onTextChanged(true);
};
})();
const renderRules = function(details, firstVisit = false) {
unfilteredRules.orig.rules = details.permanentRules.sort(directiveSort);
unfilteredRules.edit.rules = details.temporaryRules.sort(directiveSort);
rulesToDoc(firstVisit);
if ( firstVisit ) {
mergeView.editor().execCommand('goNextDiff');
}
onTextChanged(true);
};
// Switches before, rules after
var directiveSort = function(a, b) {
var aIsSwitch = a.indexOf(': ') !== -1;
var bIsSwitch = b.indexOf(': ') !== -1;
const directiveSort = function(a, b) {
const aIsSwitch = a.indexOf(': ') !== -1;
const bIsSwitch = b.indexOf(': ') !== -1;
if ( aIsSwitch === bIsSwitch ) {
return a.localeCompare(b);
}
@ -220,17 +216,15 @@ var directiveSort = function(a, b) {
/******************************************************************************/
var applyDiff = function(permanent, toAdd, toRemove) {
vAPI.messaging.send(
'user-rules.js',
{
what: 'modifyRuleset',
permanent: permanent,
toAdd: toAdd,
toRemove: toRemove
},
renderRules
);
const applyDiff = function(permanent, toAdd, toRemove) {
vAPI.messaging.send('dashboard', {
what: 'modifyRuleset',
permanent,
toAdd,
toRemove,
}).then(response => {
renderRules(response);
});
};
/******************************************************************************/
@ -245,20 +239,20 @@ mergeView.options.revertChunk = function(
) {
// https://github.com/gorhill/uBlock/issues/3611
if ( document.body.getAttribute('dir') === 'rtl' ) {
var tmp;
let tmp;
tmp = from; from = to; to = tmp;
tmp = fromStart; fromStart = toStart; toStart = tmp;
tmp = fromEnd; fromEnd = toEnd; toEnd = tmp;
}
if ( typeof fromStart.ch !== 'number' ) { fromStart.ch = 0; }
if ( fromEnd.ch !== 0 ) { fromEnd.line += 1; }
var toAdd = from.getRange(
const 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(
const toRemove = to.getRange(
{ line: toStart.line, ch: 0 },
{ line: toEnd.line, ch: 0 }
);
@ -270,8 +264,8 @@ mergeView.options.revertChunk = function(
// https://github.com/chrisaljoudi/uBlock/issues/757
// Support RequestPolicy rule syntax
var fromRequestPolicy = function(content) {
var matches = /\[origins-to-destinations\]([^\[]+)/.exec(content);
const fromRequestPolicy = function(content) {
const matches = /\[origins-to-destinations\]([^\[]+)/.exec(content);
if ( matches === null || matches.length !== 2 ) { return; }
return matches[1].trim()
.replace(/\|/g, ' ')
@ -282,8 +276,8 @@ var fromRequestPolicy = function(content) {
// https://github.com/gorhill/uMatrix/issues/270
var fromNoScript = function(content) {
var noscript = null;
const fromNoScript = function(content) {
let noscript = null;
try {
noscript = JSON.parse(content);
} catch (e) {
@ -298,17 +292,16 @@ var fromNoScript = function(content) {
) {
return;
}
var out = new Set();
var reBad = /[a-z]+:\w*$/;
var reURL = /[a-z]+:\/\/([0-9a-z.-]+)/;
var directives = noscript.whitelist.split(/\s+/);
var i = directives.length;
var directive, matches;
const out = new Set();
const reBad = /[a-z]+:\w*$/;
const reURL = /[a-z]+:\/\/([0-9a-z.-]+)/;
const directives = noscript.whitelist.split(/\s+/);
let i = directives.length;
while ( i-- ) {
directive = directives[i].trim();
let directive = directives[i].trim();
if ( directive === '' ) { continue; }
if ( reBad.test(directive) ) { continue; }
matches = reURL.exec(directive);
const matches = reURL.exec(directive);
if ( matches !== null ) {
directive = matches[1];
}
@ -321,12 +314,12 @@ var fromNoScript = function(content) {
/******************************************************************************/
var handleImportFilePicker = function() {
var fileReaderOnLoadHandler = function() {
const handleImportFilePicker = function() {
const fileReaderOnLoadHandler = function() {
if ( typeof this.result !== 'string' || this.result === '' ) {
return;
}
var result = fromRequestPolicy(this.result);
let result = fromRequestPolicy(this.result);
if ( result === undefined ) {
result = fromNoScript(this.result);
if ( result === undefined ) {
@ -336,20 +329,20 @@ var handleImportFilePicker = function() {
if ( this.result === '' ) { return; }
applyDiff(false, result, '');
};
var file = this.files[0];
const file = this.files[0];
if ( file === undefined || file.name === '' ) { return; }
if ( file.type.indexOf('text') !== 0 && file.type !== 'application/json') {
return;
}
var fr = new FileReader();
const fr = new FileReader();
fr.onload = fileReaderOnLoadHandler;
fr.readAsText(file);
};
/******************************************************************************/
var startImportFilePicker = function() {
var input = document.getElementById('importFilePicker');
const startImportFilePicker = function() {
const input = document.getElementById('importFilePicker');
// Reset to empty string, this will ensure an change event is properly
// triggered if the user pick a file, even if it is the same as the last
// one picked.
@ -359,26 +352,26 @@ var startImportFilePicker = function() {
/******************************************************************************/
function exportUserRulesToFile() {
const exportUserRulesToFile = function() {
vAPI.download({
url: 'data:text/plain,' + encodeURIComponent(
mergeView.leftOriginal().getValue().trim() + '\n'
),
filename: uDom('[data-i18n="userRulesDefaultFileName"]').text()
});
}
};
/******************************************************************************/
var onFilterChanged = (function() {
var timer,
overlay = null,
last = '';
const onFilterChanged = (function() {
let timer;
let overlay = null;
let last = '';
var process = function() {
const process = function() {
timer = undefined;
if ( mergeView.editor().isClean(cleanEditToken) === false ) { return; }
var filter = uDom('#ruleFilter input').val();
const filter = uDom('#ruleFilter input').val();
if ( filter === last ) { return; }
last = filter;
if ( overlay !== null ) {
@ -402,13 +395,13 @@ var onFilterChanged = (function() {
/******************************************************************************/
var onTextChanged = (function() {
var timer;
const onTextChanged = (function() {
let timer;
var process = function(now) {
const process = function(now) {
timer = undefined;
var isClean = mergeView.editor().isClean(cleanEditToken);
var diff = document.getElementById('diff');
const diff = document.getElementById('diff');
let isClean = mergeView.editor().isClean(cleanEditToken);
if (
now &&
isClean === false &&
@ -419,7 +412,7 @@ var onTextChanged = (function() {
}
diff.classList.toggle('editing', isClean === false);
diff.classList.toggle('dirty', mergeView.leftChunks().length !== 0);
var input = document.querySelector('#ruleFilter input');
const input = document.querySelector('#ruleFilter input');
if ( isClean ) {
input.removeAttribute('disabled');
CodeMirror.commands.save = undefined;
@ -437,16 +430,16 @@ var onTextChanged = (function() {
/******************************************************************************/
var revertAllHandler = function() {
var toAdd = [], toRemove = [];
var left = mergeView.leftOriginal(),
edit = mergeView.editor();
for ( var chunk of mergeView.leftChunks() ) {
var addedLines = left.getRange(
const revertAllHandler = function() {
const toAdd = [], toRemove = [];
const left = mergeView.leftOriginal();
const edit = mergeView.editor();
for ( const chunk of mergeView.leftChunks() ) {
const addedLines = left.getRange(
{ line: chunk.origFrom, ch: 0 },
{ line: chunk.origTo, ch: 0 }
);
var removedLines = edit.getRange(
const removedLines = edit.getRange(
{ line: chunk.editFrom, ch: 0 },
{ line: chunk.editTo, ch: 0 }
);
@ -458,16 +451,16 @@ var revertAllHandler = function() {
/******************************************************************************/
var commitAllHandler = function() {
var toAdd = [], toRemove = [];
var left = mergeView.leftOriginal(),
edit = mergeView.editor();
for ( var chunk of mergeView.leftChunks() ) {
var addedLines = edit.getRange(
const commitAllHandler = function() {
const toAdd = [], toRemove = [];
const left = mergeView.leftOriginal();
const edit = mergeView.editor();
for ( const chunk of mergeView.leftChunks() ) {
const addedLines = edit.getRange(
{ line: chunk.editFrom, ch: 0 },
{ line: chunk.editTo, ch: 0 }
);
var removedLines = left.getRange(
const removedLines = left.getRange(
{ line: chunk.origFrom, ch: 0 },
{ line: chunk.origTo, ch: 0 }
);
@ -479,17 +472,17 @@ var commitAllHandler = function() {
/******************************************************************************/
var editSaveHandler = function() {
var editor = mergeView.editor();
var editText = editor.getValue().trim();
const editSaveHandler = function() {
const editor = mergeView.editor();
const editText = editor.getValue().trim();
if ( editText === cleanEditText ) {
onTextChanged(true);
return;
}
if ( differ === undefined ) { differ = new diff_match_patch(); }
var toAdd = [], toRemove = [];
var diffs = differ.diff_main(cleanEditText, editText);
for ( var diff of diffs ) {
const toAdd = [], toRemove = [];
const diffs = differ.diff_main(cleanEditText, editText);
for ( const diff of diffs ) {
if ( diff[0] === 1 ) {
toAdd.push(diff[1]);
} else if ( diff[0] === -1 ) {
@ -528,9 +521,14 @@ uDom('#ruleFilter input').on('input', onFilterChanged);
// https://groups.google.com/forum/#!topic/codemirror/UQkTrt078Vs
mergeView.editor().on('updateDiff', function() { onTextChanged(); });
vAPI.messaging.send('user-rules.js', { what: 'getRuleset' }, renderRules);
vAPI.messaging.send('dashboard', {
what: 'getRuleset',
}).then(response => {
renderRules(response, true);
});
/******************************************************************************/
})();
// <<<<< end of local scope
}

128
src/js/utils.js

@ -28,13 +28,26 @@
if ( details.shiftKey ) {
this.changeUserSettings(
'alwaysDetachLogger',
!this.userSettings.alwaysDetachLogger
this.userSettings.alwaysDetachLogger === false
);
}
details.popup = this.userSettings.alwaysDetachLogger;
if ( this.userSettings.alwaysDetachLogger ) {
details.popup = this.rawSettings.loggerPopupType;
const url = new URL(vAPI.getURL(details.url));
url.searchParams.set('popup', '1');
details.url = url.href;
let popupLoggerBox;
try {
popupLoggerBox = JSON.parse(
vAPI.localStorage.getItem('popupLoggerBox')
);
} catch(ex) {
}
if ( popupLoggerBox !== undefined ) {
details.box = popupLoggerBox;
}
}
}
details.index = -1;
details.select = true;
vAPI.tabs.open(details);
};
@ -107,3 +120,110 @@
};
/******************************************************************************/
// Custom base64 encoder/decoder
//
// TODO:
// Could expand the LZ4 codec API to be able to return UTF8-safe string
// representation of a compressed buffer, and thus the code below could be
// moved LZ4 codec-side.
// https://github.com/uBlockOrigin/uBlock-issues/issues/461
// Provide a fallback encoding for Chromium 59 and less by issuing a plain
// JSON string. The fallback can be removed once min supported version is
// above 59.
µMatrix.base64 = new (class {
constructor() {
this.valToDigit = new Uint8Array(64);
this.digitToVal = new Uint8Array(128);
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz@%";
for ( let i = 0, n = chars.length; i < n; i++ ) {
const c = chars.charCodeAt(i);
this.valToDigit[i] = c;
this.digitToVal[c] = i;
}
this.magic = 'Base64_1';
}
encode(arrbuf, arrlen) {
const inputLength = (arrlen + 3) >>> 2;
const inbuf = new Uint32Array(arrbuf, 0, inputLength);
const outputLength = this.magic.length + 7 + inputLength * 7;
const outbuf = new Uint8Array(outputLength);
let j = 0;
for ( let i = 0; i < this.magic.length; i++ ) {
outbuf[j++] = this.magic.charCodeAt(i);
}
let v = inputLength;
do {
outbuf[j++] = this.valToDigit[v & 0b111111];
v >>>= 6;
} while ( v !== 0 );
outbuf[j++] = 0x20 /* ' ' */;
for ( let i = 0; i < inputLength; i++ ) {
v = inbuf[i];
do {
outbuf[j++] = this.valToDigit[v & 0b111111];
v >>>= 6;
} while ( v !== 0 );
outbuf[j++] = 0x20 /* ' ' */;
}
if ( typeof TextDecoder === 'undefined' ) {
return JSON.stringify(
Array.from(new Uint32Array(outbuf.buffer, 0, j >>> 2))
);
}
const textDecoder = new TextDecoder();
return textDecoder.decode(new Uint8Array(outbuf.buffer, 0, j));
}
decode(instr, arrbuf) {
if ( instr.charCodeAt(0) === 0x5B /* '[' */ ) {
const inbuf = JSON.parse(instr);
if ( arrbuf instanceof ArrayBuffer === false ) {
return new Uint32Array(inbuf);
}
const outbuf = new Uint32Array(arrbuf);
outbuf.set(inbuf);
return outbuf;
}
if ( instr.startsWith(this.magic) === false ) {
throw new Error('Invalid µBlock.base64 encoding');
}
const inputLength = instr.length;
const outbuf = arrbuf instanceof ArrayBuffer === false
? new Uint32Array(this.decodeSize(instr) >> 2)
: new Uint32Array(arrbuf);
let i = instr.indexOf(' ', this.magic.length) + 1;
if ( i === -1 ) {
throw new Error('Invalid µBlock.base64 encoding');
}
let j = 0;
for (;;) {
if ( i === inputLength ) { break; }
let v = 0, l = 0;
for (;;) {
const c = instr.charCodeAt(i++);
if ( c === 0x20 /* ' ' */ ) { break; }
v += this.digitToVal[c] << l;
l += 6;
}
outbuf[j++] = v;
}
return outbuf;
}
decodeSize(instr) {
if ( instr.startsWith(this.magic) === false ) { return 0; }
let v = 0, l = 0, i = this.magic.length;
for (;;) {
const c = instr.charCodeAt(i++);
if ( c === 0x20 /* ' ' */ ) { break; }
v += this.digitToVal[c] << l;
l += 6;
}
return v << 2;
}
})();
/******************************************************************************/

24
src/js/wasm/README.md

@ -0,0 +1,24 @@
### For code reviewers
All `wasm` files in that directory where created by compiling the
corresponding `wat` file using the command (using `hntrie.wat`/`hntrie.wasm`
as example):
wat2wasm hntrie.wat -o hntrie.wasm
Assuming:
- The command is executed from within the present directory.
### `wat2wasm` tool
The `wat2wasm` tool can be downloaded from an official WebAssembly project:
<https://github.com/WebAssembly/wabt/releases>.
### `wat2wasm` tool online
You can also use the following online `wat2wasm` tool:
<https://webassembly.github.io/wabt/demo/wat2wasm/>.
Just paste the whole content of the `wat` file to compile into the WAT pane.
Click "Download" button to retrieve the resulting `wasm` file.

BIN
src/js/wasm/hntrie.wasm

710
src/js/wasm/hntrie.wat

@ -0,0 +1,710 @@
;;
;; uBlock Origin - a browser extension to block requests.
;; Copyright (C) 2018-present Raymond Hill
;;
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see {http://www.gnu.org/licenses/}.
;;
;; Home: https://github.com/gorhill/uBlock
;; File: hntrie.wat
;; Description: WebAssembly code used by src/js/hntrie.js
;; How to compile: See README.md in this directory.
(module
;;
;; module start
;;
(func $growBuf (import "imports" "growBuf"))
(memory (import "imports" "memory") 1)
;; Trie container
;;
;; Memory layout, byte offset:
;; 0-254: needle being processed
;; 255: length of needle
;; 256-259: offset to start of trie data section (=> trie0)
;; 260-263: offset to end of trie data section (=> trie1)
;; 264-267: offset to start of character data section (=> char0)
;; 268-271: offset to end of character data section (=> char1)
;; 272: start of trie data section
;;
;;
;; Public functions
;;
;;
;; unsigned int matches(icell)
;;
;; Test whether the currently set needle matches the trie at specified trie
;; offset.
;;
(func (export "matches")
(param $iroot i32) ;; offset to root cell of the trie
(result i32) ;; result = match index, -1 = miss
(local $icell i32) ;; offset to the current cell
(local $char0 i32) ;; offset to first character data
(local $ineedle i32) ;; current needle offset
(local $c i32)
(local $v i32)
(local $n i32)
(local $i0 i32)
(local $i1 i32)
;;
i32.const 264 ;; start of char section is stored at addr 264
i32.load
set_local $char0
;; let ineedle = this.buf[255];
i32.const 255 ;; addr of needle is stored at addr 255
i32.load8_u
set_local $ineedle
;; let icell = this.buf32[iroot+0];
get_local $iroot
i32.const 2
i32.shl
i32.load
i32.const 2
i32.shl
tee_local $icell
;; if ( icell === 0 ) { return -1; }
i32.eqz
if
i32.const -1
return
end
;; for (;;) {
block $noSegment loop $nextSegment
;; if ( ineedle === 0 ) { return -1; }
get_local $ineedle
i32.eqz
if
i32.const -1
return
end
;; ineedle -= 1;
get_local $ineedle
i32.const -1
i32.add
tee_local $ineedle
;; let c = this.buf[ineedle];
i32.load8_u
set_local $c
;; for (;;) {
block $foundSegment loop $findSegment
;; v = this.buf32[icell+2];
get_local $icell
i32.load offset=8
tee_local $v
;; i0 = this.char0 + (v & 0x00FFFFFF);
i32.const 0x00FFFFFF
i32.and
get_local $char0
i32.add
tee_local $i0
;; if ( this.buf[i0] === c ) { break; }
i32.load8_u
get_local $c
i32.eq
br_if $foundSegment
;; icell = this.buf32[icell+0];
get_local $icell
i32.load
i32.const 2
i32.shl
tee_local $icell
i32.eqz
if
i32.const -1
return
end
br 0
end end
;; let n = v >>> 24;
get_local $v
i32.const 24
i32.shr_u
tee_local $n
;; if ( n > 1 ) {
i32.const 1
i32.gt_u
if
;; n -= 1;
get_local $n
i32.const -1
i32.add
tee_local $n
;; if ( n > ineedle ) { return -1; }
get_local $ineedle
i32.gt_u
if
i32.const -1
return
end
get_local $i0
i32.const 1
i32.add
tee_local $i0
;; const i1 = i0 + n;
get_local $n
i32.add
set_local $i1
;; do {
loop
;; ineedle -= 1;
get_local $ineedle
i32.const -1
i32.add
tee_local $ineedle
;; if ( this.buf[i0] !== this.buf[ineedle] ) { return -1; }
i32.load8_u
get_local $i0
i32.load8_u
i32.ne
if
i32.const -1
return
end
;; i0 += 1;
get_local $i0
i32.const 1
i32.add
tee_local $i0
;; } while ( i0 < i1 );
get_local $i1
i32.lt_u
br_if 0
end
end
;; icell = this.buf32[icell+1];
get_local $icell
i32.load offset=4
i32.const 2
i32.shl
tee_local $icell
;; if ( icell === 0 ) { break; }
i32.eqz
br_if $noSegment
;; if ( this.buf32[icell+2] === 0 ) {
get_local $icell
i32.load
i32.eqz
if
;; if ( ineedle === 0 || this.buf[ineedle-1] === 0x2E ) {
;; return ineedle;
;; }
get_local $ineedle
i32.eqz
if
i32.const 0
return
end
get_local $ineedle
i32.const -1
i32.add
i32.load8_u
i32.const 0x2E
i32.eq
if
get_local $ineedle
return
end
;; icell = this.buf32[icell+1];
get_local $icell
i32.load offset=4
i32.const 2
i32.shl
set_local $icell
end
br 0
end end
;; return ineedle === 0 || this.buf[ineedle-1] === 0x2E ? ineedle : -1;
get_local $ineedle
i32.eqz
if
i32.const 0
return
end
get_local $ineedle
i32.const -1
i32.add
i32.load8_u
i32.const 0x2E
i32.eq
if
get_local $ineedle
return
end
i32.const -1
)
;;
;; unsigned int add(icell)
;;
;; Add a new hostname to a trie which root cell is passed as argument.
;;
(func (export "add")
(param $iroot i32) ;; index of root cell of the trie
(result i32) ;; result: 0 not added, 1 = added
(local $icell i32) ;; index of current cell in the trie
(local $lhnchar i32) ;; number of characters left to process in hostname
(local $char0 i32) ;; offset to start of character data section
(local $vseg i32) ;; integer value describing a segment
(local $isegchar0 i32) ;; offset to start of current segment's character data
(local $isegchar i32)
(local $lsegchar i32) ;; number of character in current segment
(local $inext i32) ;; index of next cell to process
;;
;; let lhnchar = this.buf[255];
i32.const 255
i32.load8_u
tee_local $lhnchar
;; if ( lhnchar === 0 ) { return 0; }
i32.eqz
if
i32.const 0
return
end
;; if (
;; (this.buf32[HNBIGTRIE_CHAR0_SLOT] - this.buf32[HNBIGTRIE_TRIE1_SLOT]) < 24 ||
;; (this.buf.length - this.buf32[HNBIGTRIE_CHAR1_SLOT]) < 256
;; ) {
;; this.growBuf();
;; }
i32.const 264
i32.load
i32.const 260
i32.load
i32.sub
i32.const 24
i32.lt_u
if
call $growBuf
else
memory.size
i32.const 16
i32.shl
i32.const 268
i32.load
i32.sub
i32.const 256
i32.lt_u
if
call $growBuf
end
end
;; let icell = this.buf32[iroot+0];
get_local $iroot
i32.const 2
i32.shl
tee_local $iroot
i32.load
i32.const 2
i32.shl
tee_local $icell
;; if ( this.buf32[icell+2] === 0 ) {
i32.eqz
if
;; this.buf32[iroot+0] = this.addCell(0, 0, this.addSegment(lhnchar));
;; return 1;
get_local $iroot
i32.const 0
i32.const 0
get_local $lhnchar
call $addSegment
call $addCell
i32.store
i32.const 1
return
end
;; const char0 = this.buf32[HNBIGTRIE_CHAR0_SLOT];
i32.const 264
i32.load
set_local $char0
;; for (;;) {
loop $nextSegment
;; const v = this.buf32[icell+2];
get_local $icell
i32.load offset=8
tee_local $vseg
;; if ( vseg === 0 ) {
i32.eqz
if
;; if ( this.buf[lhnchar-1] === 0x2E /* '.' */ ) { return -1; }
get_local $lhnchar
i32.const -1
i32.add
i32.load8_u
i32.const 0x2E
i32.eq
if
i32.const -1
return
end
;; icell = this.buf32[icell+1];
;; continue;
get_local $icell
i32.load offset=4
i32.const 2
i32.shl
set_local $icell
br $nextSegment
end
;; let isegchar0 = char0 + (vseg & 0x00FFFFFF);
get_local $char0
get_local $vseg
i32.const 0x00FFFFFF
i32.and
i32.add
tee_local $isegchar0
;; if ( this.buf[isegchar0] !== this.buf[lhnchar-1] ) {
i32.load8_u
get_local $lhnchar
i32.const -1
i32.add
i32.load8_u
i32.ne
if
;; inext = this.buf32[icell+0];
get_local $icell
i32.load
i32.const 2
i32.shl
tee_local $inext
;; if ( inext === 0 ) {
i32.eqz
if
;; this.buf32[icell+0] = this.addCell(0, 0, this.addSegment(lhnchar));
get_local $icell
i32.const 0
i32.const 0
get_local $lhnchar
call $addSegment
call $addCell
i32.store
;; return 1;
i32.const 1
return
end
;; icell = inext;
get_local $inext
set_local $icell
br $nextSegment
end
;; let isegchar = 1;
i32.const 1
set_local $isegchar
;; lhnchar -= 1;
get_local $lhnchar
i32.const -1
i32.add
set_local $lhnchar
;; const lsegchar = vseg >>> 24;
get_local $vseg
i32.const 24
i32.shr_u
tee_local $lsegchar
;; if ( lsegchar !== 1 ) {
i32.const 1
i32.ne
if
;; for (;;) {
block $mismatch loop
;; if ( isegchar === lsegchar ) { break; }
get_local $isegchar
get_local $lsegchar
i32.eq
br_if $mismatch
get_local $lhnchar
i32.eqz
br_if $mismatch
;; if ( this.buf[isegchar0+isegchar] !== this.buf[lhnchar-1] ) { break; }
get_local $isegchar0
get_local $isegchar
i32.add
i32.load8_u
get_local $lhnchar
i32.const -1
i32.add
i32.load8_u
i32.ne
br_if $mismatch
;; isegchar += 1;
get_local $isegchar
i32.const 1
i32.add
set_local $isegchar
;; lhnchar -= 1;
get_local $lhnchar
i32.const -1
i32.add
set_local $lhnchar
br 0
end end
end
;; if ( isegchar === lsegchar ) {
get_local $isegchar
get_local $lsegchar
i32.eq
if
;; inext = this.buf32[icell+1];
get_local $icell
i32.load offset=4
i32.const 2
i32.shl
set_local $inext
;; if ( lhnchar === 0 ) {
get_local $lhnchar
i32.eqz
if
;; if ( inext === 0 || this.buf32[inext+2] === 0 ) { return 0; }
get_local $inext
i32.eqz
if
i32.const 0
return
end
get_local $inext
i32.load offset=8
i32.eqz
if
i32.const 0
return
end
;; this.buf32[icell+1] = this.addCell(0, inext, 0);
get_local $icell
i32.const 0
get_local $inext
i32.const 2
i32.shr_u
i32.const 0
call $addCell
i32.store offset=4
else
;; if ( inext !== 0 ) {
get_local $inext
if
;; icell = inext;
get_local $inext
set_local $icell
br $nextSegment
end
;; if ( this.buf[lhnchar-1] === 0x2E /* '.' */ ) { return -1; }
get_local $lhnchar
i32.const -1
i32.add
i32.load8_u
i32.const 0x2E
i32.eq
if
i32.const -1
return
end
;; inext = this.addCell(0, 0, 0);
;; this.buf32[icell+1] = inext;
get_local $icell
i32.const 0
i32.const 0
i32.const 0
call $addCell
tee_local $inext
i32.store offset=4
;; this.buf32[inext+1] = this.addCell(0, 0, this.addSegment(lhnchar));
get_local $inext
i32.const 2
i32.shl
i32.const 0
i32.const 0
get_local $lhnchar
call $addSegment
call $addCell
i32.store offset=4
end
else
;; isegchar0 -= char0;
get_local $icell
get_local $isegchar0
get_local $char0
i32.sub
tee_local $isegchar0
;; this.buf32[icell+2] = isegchar << 24 | isegchar0;
get_local $isegchar
i32.const 24
i32.shl
i32.or
i32.store offset=8
;; inext = this.addCell(
;; 0,
;; this.buf32[icell+1],
;; lsegchar - isegchar << 24 | isegchar0 + isegchar
;; );
;; this.buf32[icell+1] = inext;
get_local $icell
i32.const 0
get_local $icell
i32.load offset=4
get_local $lsegchar
get_local $isegchar
i32.sub
i32.const 24
i32.shl
get_local $isegchar0
get_local $isegchar
i32.add
i32.or
call $addCell
tee_local $inext
i32.store offset=4
;; if ( lhnchar === 0 ) {
get_local $lhnchar
i32.eqz
if
;; this.buf32[icell+1] = this.addCell(0, inext, 0);
get_local $icell
i32.const 0
get_local $inext
i32.const 0
call $addCell
i32.store offset=4
else
;; this.buf32[inext+0] = this.addCell(0, 0, this.addSegment(lhnchar));
get_local $inext
i32.const 2
i32.shl
i32.const 0
i32.const 0
get_local $lhnchar
call $addSegment
call $addCell
i32.store
end
end
;; return 1;
i32.const 1
return
end
;;
i32.const 1
)
;;
;; Private functions
;;
;;
;; unsigned int addCell(idown, iright, vseg)
;;
;; Add a new cell, return cell index.
;;
(func $addCell
(param $idown i32)
(param $iright i32)
(param $vseg i32)
(result i32) ;; result: index of added cell
(local $icell i32)
;;
;; let icell = this.buf32[HNBIGTRIE_TRIE1_SLOT];
;; this.buf32[HNBIGTRIE_TRIE1_SLOT] = icell + 12;
i32.const 260
i32.const 260
i32.load
tee_local $icell
i32.const 12
i32.add
i32.store
;; this.buf32[icell+0] = idown;
get_local $icell
get_local $idown
i32.store
;; this.buf32[icell+1] = iright;
get_local $icell
get_local $iright
i32.store offset=4
;; this.buf32[icell+2] = v;
get_local $icell
get_local $vseg
i32.store offset=8
;; return icell;
get_local $icell
i32.const 2
i32.shr_u
)
;;
;; unsigned int addSegment(lsegchar)
;;
;; Store a segment of characters and return a segment descriptor. The segment
;; is created from the character data in the needle buffer.
;;
(func $addSegment
(param $lsegchar i32)
(result i32) ;; result: segment descriptor
(local $char1 i32) ;; offset to end of character data section
(local $isegchar i32) ;; relative offset to first character of segment
(local $i i32) ;; iterator
;;
;; if ( lsegchar === 0 ) { return 0; }
get_local $lsegchar
i32.eqz
if
i32.const 0
return
end
;; let char1 = this.buf32[HNBIGTRIE_CHAR1_SLOT];
i32.const 268
i32.load
tee_local $char1
;; const isegchar = char1 - this.buf32[HNBIGTRIE_CHAR0_SLOT];
i32.const 264
i32.load
i32.sub
set_local $isegchar
;; let i = lsegchar;
get_local $lsegchar
set_local $i
;; do {
block $endOfSegment loop
;; this.buf[char1++] = this.buf[--i];
get_local $char1
get_local $i
i32.const -1
i32.add
tee_local $i
i32.load8_u
i32.store8
get_local $char1
i32.const 1
i32.add
set_local $char1
;; } while ( i !== 0 );
get_local $i
i32.eqz
br_if $endOfSegment
br 0
end end
;; this.buf32[HNBIGTRIE_CHAR1_SLOT] = char1;
i32.const 268
get_local $char1
i32.store
;; return (lsegchar << 24) | isegchar;
get_local $lsegchar
i32.const 24
i32.shl
get_local $isegchar
i32.or
)
;;
;; module end
;;
)

127
src/lib/codemirror/addon/display/panel.js

@ -0,0 +1,127 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
CodeMirror.defineExtension("addPanel", function(node, options) {
options = options || {};
if (!this.state.panels) initPanels(this);
var info = this.state.panels;
var wrapper = info.wrapper;
var cmWrapper = this.getWrapperElement();
var replace = options.replace instanceof Panel && !options.replace.cleared;
if (options.after instanceof Panel && !options.after.cleared) {
wrapper.insertBefore(node, options.before.node.nextSibling);
} else if (options.before instanceof Panel && !options.before.cleared) {
wrapper.insertBefore(node, options.before.node);
} else if (replace) {
wrapper.insertBefore(node, options.replace.node);
info.panels++;
options.replace.clear();
} else if (options.position == "bottom") {
wrapper.appendChild(node);
} else if (options.position == "before-bottom") {
wrapper.insertBefore(node, cmWrapper.nextSibling);
} else if (options.position == "after-top") {
wrapper.insertBefore(node, cmWrapper);
} else {
wrapper.insertBefore(node, wrapper.firstChild);
}
var height = (options && options.height) || node.offsetHeight;
this._setSize(null, info.heightLeft -= height);
if (!replace) {
info.panels++;
}
if (options.stable && isAtTop(this, node))
this.scrollTo(null, this.getScrollInfo().top + height)
return new Panel(this, node, options, height);
});
function Panel(cm, node, options, height) {
this.cm = cm;
this.node = node;
this.options = options;
this.height = height;
this.cleared = false;
}
Panel.prototype.clear = function() {
if (this.cleared) return;
this.cleared = true;
var info = this.cm.state.panels;
this.cm._setSize(null, info.heightLeft += this.height);
if (this.options.stable && isAtTop(this.cm, this.node))
this.cm.scrollTo(null, this.cm.getScrollInfo().top - this.height)
info.wrapper.removeChild(this.node);
if (--info.panels == 0) removePanels(this.cm);
};
Panel.prototype.changed = function(height) {
var newHeight = height == null ? this.node.offsetHeight : height;
var info = this.cm.state.panels;
this.cm._setSize(null, info.heightLeft -= (newHeight - this.height));
this.height = newHeight;
};
function initPanels(cm) {
var wrap = cm.getWrapperElement();
var style = window.getComputedStyle ? window.getComputedStyle(wrap) : wrap.currentStyle;
var height = parseInt(style.height);
var info = cm.state.panels = {
setHeight: wrap.style.height,
heightLeft: height,
panels: 0,
wrapper: document.createElement("div")
};
wrap.parentNode.insertBefore(info.wrapper, wrap);
var hasFocus = cm.hasFocus();
info.wrapper.appendChild(wrap);
if (hasFocus) cm.focus();
cm._setSize = cm.setSize;
if (height != null) cm.setSize = function(width, newHeight) {
if (newHeight == null) return this._setSize(width, newHeight);
info.setHeight = newHeight;
if (typeof newHeight != "number") {
var px = /^(\d+\.?\d*)px$/.exec(newHeight);
if (px) {
newHeight = Number(px[1]);
} else {
info.wrapper.style.height = newHeight;
newHeight = info.wrapper.offsetHeight;
info.wrapper.style.height = "";
}
}
cm._setSize(width, info.heightLeft += (newHeight - height));
height = newHeight;
};
}
function removePanels(cm) {
var info = cm.state.panels;
cm.state.panels = null;
var wrap = cm.getWrapperElement();
info.wrapper.parentNode.replaceChild(wrap, info.wrapper);
wrap.style.height = info.setHeight;
cm.setSize = cm._setSize;
cm.setSize();
}
function isAtTop(cm, dom) {
for (var sibling = dom.nextSibling; sibling; sibling = sibling.nextSibling)
if (sibling == cm.getWrapperElement()) return true
return false
}
});

122
src/lib/codemirror/addon/scroll/annotatescrollbar.js

@ -0,0 +1,122 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineExtension("annotateScrollbar", function(options) {
if (typeof options == "string") options = {className: options};
return new Annotation(this, options);
});
CodeMirror.defineOption("scrollButtonHeight", 0);
function Annotation(cm, options) {
this.cm = cm;
this.options = options;
this.buttonHeight = options.scrollButtonHeight || cm.getOption("scrollButtonHeight");
this.annotations = [];
this.doRedraw = this.doUpdate = null;
this.div = cm.getWrapperElement().appendChild(document.createElement("div"));
this.div.style.cssText = "position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none";
this.computeScale();
function scheduleRedraw(delay) {
clearTimeout(self.doRedraw);
self.doRedraw = setTimeout(function() { self.redraw(); }, delay);
}
var self = this;
cm.on("refresh", this.resizeHandler = function() {
clearTimeout(self.doUpdate);
self.doUpdate = setTimeout(function() {
if (self.computeScale()) scheduleRedraw(20);
}, 100);
});
cm.on("markerAdded", this.resizeHandler);
cm.on("markerCleared", this.resizeHandler);
if (options.listenForChanges !== false)
cm.on("change", this.changeHandler = function() {
scheduleRedraw(250);
});
}
Annotation.prototype.computeScale = function() {
var cm = this.cm;
var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) /
cm.getScrollerElement().scrollHeight
if (hScale != this.hScale) {
this.hScale = hScale;
return true;
}
};
Annotation.prototype.update = function(annotations) {
this.annotations = annotations;
this.redraw();
};
Annotation.prototype.redraw = function(compute) {
if (compute !== false) this.computeScale();
var cm = this.cm, hScale = this.hScale;
var frag = document.createDocumentFragment(), anns = this.annotations;
var wrapping = cm.getOption("lineWrapping");
var singleLineH = wrapping && cm.defaultTextHeight() * 1.5;
var curLine = null, curLineObj = null;
function getY(pos, top) {
if (curLine != pos.line) {
curLine = pos.line;
curLineObj = cm.getLineHandle(curLine);
}
if ((curLineObj.widgets && curLineObj.widgets.length) ||
(wrapping && curLineObj.height > singleLineH))
return cm.charCoords(pos, "local")[top ? "top" : "bottom"];
var topY = cm.heightAtLine(curLineObj, "local");
return topY + (top ? 0 : curLineObj.height);
}
var lastLine = cm.lastLine()
if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) {
var ann = anns[i];
if (ann.to.line > lastLine) continue;
var top = nextTop || getY(ann.from, true) * hScale;
var bottom = getY(ann.to, false) * hScale;
while (i < anns.length - 1) {
if (anns[i + 1].to.line > lastLine) break;
nextTop = getY(anns[i + 1].from, true) * hScale;
if (nextTop > bottom + .9) break;
ann = anns[++i];
bottom = getY(ann.to, false) * hScale;
}
if (bottom == top) continue;
var height = Math.max(bottom - top, 3);
var elt = frag.appendChild(document.createElement("div"));
elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: "
+ (top + this.buttonHeight) + "px; height: " + height + "px";
elt.className = this.options.className;
if (ann.id) {
elt.setAttribute("annotation-id", ann.id);
}
}
this.div.textContent = "";
this.div.appendChild(frag);
};
Annotation.prototype.clear = function() {
this.cm.off("refresh", this.resizeHandler);
this.cm.off("markerAdded", this.resizeHandler);
this.cm.off("markerCleared", this.resizeHandler);
if (this.changeHandler) this.cm.off("change", this.changeHandler);
this.div.parentNode.removeChild(this.div);
};
});

8
src/lib/codemirror/addon/search/matchesonscrollbar.css

@ -0,0 +1,8 @@
.CodeMirror-search-match {
background: gold;
border-top: 1px solid orange;
border-bottom: 1px solid orange;
-moz-box-sizing: border-box;
box-sizing: border-box;
opacity: .5;
}

97
src/lib/codemirror/addon/search/matchesonscrollbar.js

@ -0,0 +1,97 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"), require("./searchcursor"), require("../scroll/annotatescrollbar"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror", "./searchcursor", "../scroll/annotatescrollbar"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineExtension("showMatchesOnScrollbar", function(query, caseFold, options) {
if (typeof options == "string") options = {className: options};
if (!options) options = {};
return new SearchAnnotation(this, query, caseFold, options);
});
function SearchAnnotation(cm, query, caseFold, options) {
this.cm = cm;
this.options = options;
var annotateOptions = {listenForChanges: false};
for (var prop in options) annotateOptions[prop] = options[prop];
if (!annotateOptions.className) annotateOptions.className = "CodeMirror-search-match";
this.annotation = cm.annotateScrollbar(annotateOptions);
this.query = query;
this.caseFold = caseFold;
this.gap = {from: cm.firstLine(), to: cm.lastLine() + 1};
this.matches = [];
this.update = null;
this.findMatches();
this.annotation.update(this.matches);
var self = this;
cm.on("change", this.changeHandler = function(_cm, change) { self.onChange(change); });
}
var MAX_MATCHES = 1000;
SearchAnnotation.prototype.findMatches = function() {
if (!this.gap) return;
for (var i = 0; i < this.matches.length; i++) {
var match = this.matches[i];
if (match.from.line >= this.gap.to) break;
if (match.to.line >= this.gap.from) this.matches.splice(i--, 1);
}
var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), {caseFold: this.caseFold, multiline: this.options.multiline});
var maxMatches = this.options && this.options.maxMatches || MAX_MATCHES;
while (cursor.findNext()) {
var match = {from: cursor.from(), to: cursor.to()};
if (match.from.line >= this.gap.to) break;
this.matches.splice(i++, 0, match);
if (this.matches.length > maxMatches) break;
}
this.gap = null;
};
function offsetLine(line, changeStart, sizeChange) {
if (line <= changeStart) return line;
return Math.max(changeStart, line + sizeChange);
}
SearchAnnotation.prototype.onChange = function(change) {
var startLine = change.from.line;
var endLine = CodeMirror.changeEnd(change).line;
var sizeChange = endLine - change.to.line;
if (this.gap) {
this.gap.from = Math.min(offsetLine(this.gap.from, startLine, sizeChange), change.from.line);
this.gap.to = Math.max(offsetLine(this.gap.to, startLine, sizeChange), change.from.line);
} else {
this.gap = {from: change.from.line, to: endLine + 1};
}
if (sizeChange) for (var i = 0; i < this.matches.length; i++) {
var match = this.matches[i];
var newFrom = offsetLine(match.from.line, startLine, sizeChange);
if (newFrom != match.from.line) match.from = CodeMirror.Pos(newFrom, match.from.ch);
var newTo = offsetLine(match.to.line, startLine, sizeChange);
if (newTo != match.to.line) match.to = CodeMirror.Pos(newTo, match.to.ch);
}
clearTimeout(this.update);
var self = this;
this.update = setTimeout(function() { self.updateAfterChange(); }, 250);
};
SearchAnnotation.prototype.updateAfterChange = function() {
this.findMatches();
this.annotation.update(this.matches);
};
SearchAnnotation.prototype.clear = function() {
this.cm.off("change", this.changeHandler);
this.annotation.clear();
};
});

293
src/lib/codemirror/addon/search/searchcursor.js

@ -0,0 +1,293 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"))
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod)
else // Plain browser env
mod(CodeMirror)
})(function(CodeMirror) {
"use strict"
var Pos = CodeMirror.Pos
function regexpFlags(regexp) {
var flags = regexp.flags
return flags != null ? flags : (regexp.ignoreCase ? "i" : "")
+ (regexp.global ? "g" : "")
+ (regexp.multiline ? "m" : "")
}
function ensureFlags(regexp, flags) {
var current = regexpFlags(regexp), target = current
for (var i = 0; i < flags.length; i++) if (target.indexOf(flags.charAt(i)) == -1)
target += flags.charAt(i)
return current == target ? regexp : new RegExp(regexp.source, target)
}
function maybeMultiline(regexp) {
return /\\s|\\n|\n|\\W|\\D|\[\^/.test(regexp.source)
}
function searchRegexpForward(doc, regexp, start) {
regexp = ensureFlags(regexp, "g")
for (var line = start.line, ch = start.ch, last = doc.lastLine(); line <= last; line++, ch = 0) {
regexp.lastIndex = ch
var string = doc.getLine(line), match = regexp.exec(string)
if (match)
return {from: Pos(line, match.index),
to: Pos(line, match.index + match[0].length),
match: match}
}
}
function searchRegexpForwardMultiline(doc, regexp, start) {
if (!maybeMultiline(regexp)) return searchRegexpForward(doc, regexp, start)
regexp = ensureFlags(regexp, "gm")
var string, chunk = 1
for (var line = start.line, last = doc.lastLine(); line <= last;) {
// This grows the search buffer in exponentially-sized chunks
// between matches, so that nearby matches are fast and don't
// require concatenating the whole document (in case we're
// searching for something that has tons of matches), but at the
// same time, the amount of retries is limited.
for (var i = 0; i < chunk; i++) {
if (line > last) break
var curLine = doc.getLine(line++)
string = string == null ? curLine : string + "\n" + curLine
}
chunk = chunk * 2
regexp.lastIndex = start.ch
var match = regexp.exec(string)
if (match) {
var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n")
var startLine = start.line + before.length - 1, startCh = before[before.length - 1].length
return {from: Pos(startLine, startCh),
to: Pos(startLine + inside.length - 1,
inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length),
match: match}
}
}
}
function lastMatchIn(string, regexp) {
var cutOff = 0, match
for (;;) {
regexp.lastIndex = cutOff
var newMatch = regexp.exec(string)
if (!newMatch) return match
match = newMatch
cutOff = match.index + (match[0].length || 1)
if (cutOff == string.length) return match
}
}
function searchRegexpBackward(doc, regexp, start) {
regexp = ensureFlags(regexp, "g")
for (var line = start.line, ch = start.ch, first = doc.firstLine(); line >= first; line--, ch = -1) {
var string = doc.getLine(line)
if (ch > -1) string = string.slice(0, ch)
var match = lastMatchIn(string, regexp)
if (match)
return {from: Pos(line, match.index),
to: Pos(line, match.index + match[0].length),
match: match}
}
}
function searchRegexpBackwardMultiline(doc, regexp, start) {
regexp = ensureFlags(regexp, "gm")
var string, chunk = 1
for (var line = start.line, first = doc.firstLine(); line >= first;) {
for (var i = 0; i < chunk; i++) {
var curLine = doc.getLine(line--)
string = string == null ? curLine.slice(0, start.ch) : curLine + "\n" + string
}
chunk *= 2
var match = lastMatchIn(string, regexp)
if (match) {
var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n")
var startLine = line + before.length, startCh = before[before.length - 1].length
return {from: Pos(startLine, startCh),
to: Pos(startLine + inside.length - 1,
inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length),
match: match}
}
}
}
var doFold, noFold
if (String.prototype.normalize) {
doFold = function(str) { return str.normalize("NFD").toLowerCase() }
noFold = function(str) { return str.normalize("NFD") }
} else {
doFold = function(str) { return str.toLowerCase() }
noFold = function(str) { return str }
}
// Maps a position in a case-folded line back to a position in the original line
// (compensating for codepoints increasing in number during folding)
function adjustPos(orig, folded, pos, foldFunc) {
if (orig.length == folded.length) return pos
for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) {
if (min == max) return min
var mid = (min + max) >> 1
var len = foldFunc(orig.slice(0, mid)).length
if (len == pos) return mid
else if (len > pos) max = mid
else min = mid + 1
}
}
function searchStringForward(doc, query, start, caseFold) {
// Empty string would match anything and never progress, so we
// define it to match nothing instead.
if (!query.length) return null
var fold = caseFold ? doFold : noFold
var lines = fold(query).split(/\r|\n\r?/)
search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) {
var orig = doc.getLine(line).slice(ch), string = fold(orig)
if (lines.length == 1) {
var found = string.indexOf(lines[0])
if (found == -1) continue search
var start = adjustPos(orig, string, found, fold) + ch
return {from: Pos(line, adjustPos(orig, string, found, fold) + ch),
to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)}
} else {
var cutFrom = string.length - lines[0].length
if (string.slice(cutFrom) != lines[0]) continue search
for (var i = 1; i < lines.length - 1; i++)
if (fold(doc.getLine(line + i)) != lines[i]) continue search
var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1]
if (endString.slice(0, lastLine.length) != lastLine) continue search
return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch),
to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))}
}
}
}
function searchStringBackward(doc, query, start, caseFold) {
if (!query.length) return null
var fold = caseFold ? doFold : noFold
var lines = fold(query).split(/\r|\n\r?/)
search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) {
var orig = doc.getLine(line)
if (ch > -1) orig = orig.slice(0, ch)
var string = fold(orig)
if (lines.length == 1) {
var found = string.lastIndexOf(lines[0])
if (found == -1) continue search
return {from: Pos(line, adjustPos(orig, string, found, fold)),
to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))}
} else {
var lastLine = lines[lines.length - 1]
if (string.slice(0, lastLine.length) != lastLine) continue search
for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++)
if (fold(doc.getLine(start + i)) != lines[i]) continue search
var top = doc.getLine(line + 1 - lines.length), topString = fold(top)
if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search
return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)),
to: Pos(line, adjustPos(orig, string, lastLine.length, fold))}
}
}
}
function SearchCursor(doc, query, pos, options) {
this.atOccurrence = false
this.doc = doc
pos = pos ? doc.clipPos(pos) : Pos(0, 0)
this.pos = {from: pos, to: pos}
var caseFold
if (typeof options == "object") {
caseFold = options.caseFold
} else { // Backwards compat for when caseFold was the 4th argument
caseFold = options
options = null
}
if (typeof query == "string") {
if (caseFold == null) caseFold = false
this.matches = function(reverse, pos) {
return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold)
}
} else {
query = ensureFlags(query, "gm")
if (!options || options.multiline !== false)
this.matches = function(reverse, pos) {
return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos)
}
else
this.matches = function(reverse, pos) {
return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos)
}
}
}
SearchCursor.prototype = {
findNext: function() {return this.find(false)},
findPrevious: function() {return this.find(true)},
find: function(reverse) {
var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to))
// Implements weird auto-growing behavior on null-matches for
// backwards-compatiblity with the vim code (unfortunately)
while (result && CodeMirror.cmpPos(result.from, result.to) == 0) {
if (reverse) {
if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1)
else if (result.from.line == this.doc.firstLine()) result = null
else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1)))
} else {
if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1)
else if (result.to.line == this.doc.lastLine()) result = null
else result = this.matches(reverse, Pos(result.to.line + 1, 0))
}
}
if (result) {
this.pos = result
this.atOccurrence = true
return this.pos.match || true
} else {
var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0)
this.pos = {from: end, to: end}
return this.atOccurrence = false
}
},
from: function() {if (this.atOccurrence) return this.pos.from},
to: function() {if (this.atOccurrence) return this.pos.to},
replace: function(newText, origin) {
if (!this.atOccurrence) return
var lines = CodeMirror.splitLines(newText)
this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin)
this.pos.to = Pos(this.pos.from.line + lines.length - 1,
lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0))
}
}
CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
return new SearchCursor(this.doc, query, pos, caseFold)
})
CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
return new SearchCursor(this, query, pos, caseFold)
})
CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
var ranges = []
var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold)
while (cur.findNext()) {
if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break
ranges.push({anchor: cur.from(), head: cur.to()})
}
if (ranges.length)
this.setSelections(ranges, 0)
})
});

18882
src/lib/codemirror/lib/codemirror.js
File diff suppressed because it is too large
View File

52
src/lib/lz4/README.md

@ -0,0 +1,52 @@
## Purpose
The purpose of this library is to implement LZ4 compression/decompression,
as documented at the official LZ4 repository:
https://github.com/lz4/lz4/blob/dev/doc/lz4_Block_format.md
The files in this directory are developed as a separate project at:
https://github.com/gorhill/lz4-wasm
## Files
### `lz4-block-codec-any.js`
The purpose is to instanciate a WebAssembly- or pure javascript-based
LZ4 block codec.
If the choosen implementation is not specified, there will be an attempt to
create a WebAssembly-based instance. If for whatever reason this fails, a
pure javascript-based instance will be created.
The script for either instance are dynamically loaded and only when needed,
such that no resources are wasted by keeping in memory code which won't be
used.
### `lz4-block-codec-wasm.js`
This contains the code to instanciate WebAssembly-based LZ4 block codec. Note
that the WebAssembly module is loaded using a `same-origin` fetch, hence
ensuring that no code outside the package is loaded.
### `lz4-block-codec-js.js`
This contains the code to instanciate pure javascript-based LZ4 block codec.
This is used as a fallback implementation should WebAssembly not be available
for whatever reason.
### `lz4-block-codec.wasm`
This is the WebAssembly module, loaded by `lz4-block-codec-wasm.js` using a
`same-origin` fetch.
### `lz4-block-codec.wat`
The WebAssembly source code used to generate the WebAssembly module `lz4-block-codec.wasm`.
wat2wasm ./lz4-block-codec.wat -o ./lz4-block-codec.wasm
wasm-opt ./lz4-block-codec.wasm -O4 -o ./lz4-block-codec.wasm
You can get `wat2wasm` at <https://github.com/WebAssembly/wabt>, and `wasm-opt` at <https://github.com/WebAssembly/binaryen>.

151
src/lib/lz4/lz4-block-codec-any.js

@ -0,0 +1,151 @@
/*******************************************************************************
lz4-block-codec-any.js
A wrapper to instanciate a wasm- and/or js-based LZ4 block
encoder/decoder.
Copyright (C) 2018 Raymond Hill
BSD-2-Clause License (http://www.opensource.org/licenses/bsd-license.php)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Home: https://github.com/gorhill/lz4-wasm
I used the same license as the one picked by creator of LZ4 out of respect
for his creation, see https://lz4.github.io/lz4/
*/
'use strict';
/******************************************************************************/
(function(context) { // >>>> Start of private namespace
/******************************************************************************/
const wd = (function() {
let url = document.currentScript.src;
let match = /[^\/]+$/.exec(url);
return match !== null ?
url.slice(0, match.index) :
'';
})();
const removeScript = function(script) {
if ( !script ) { return; }
if ( script.parentNode === null ) { return; }
script.parentNode.removeChild(script);
};
const createInstanceWASM = function() {
if ( context.LZ4BlockWASM instanceof Function ) {
const instance = new context.LZ4BlockWASM();
return instance.init().then(ok => ok ? instance : null);
}
if ( context.LZ4BlockWASM === null ) {
return Promise.resolve(null);
}
return new Promise(resolve => {
const script = document.createElement('script');
script.src = wd + 'lz4-block-codec-wasm.js';
script.addEventListener('load', ( ) => {
if ( context.LZ4BlockWASM instanceof Function === false ) {
context.LZ4BlockWASM = null;
resolve(null);
return;
}
const instance = new context.LZ4BlockWASM();
instance.init().then(ok => { resolve(ok ? instance : null); });
});
script.addEventListener('error', ( ) => {
context.LZ4BlockWASM = null;
resolve(null);
});
document.head.appendChild(script);
removeScript(script);
});
};
const createInstanceJS = function() {
if ( context.LZ4BlockJS instanceof Function ) {
const instance = new context.LZ4BlockJS();
return instance.init().then(ok => ok ? instance : null);
}
if ( context.LZ4BlockJS === null ) {
return Promise.resolve(null);
}
return new Promise(resolve => {
const script = document.createElement('script');
script.src = wd + 'lz4-block-codec-js.js';
script.addEventListener('load', ( ) => {
if ( context.LZ4BlockJS instanceof Function === false ) {
context.LZ4BlockJS = null;
resolve(null);
return;
}
const instance = new context.LZ4BlockJS();
instance.init().then(ok => { resolve(ok ? instance : null); });
});
script.addEventListener('error', ( ) => {
context.LZ4BlockJS = null;
resolve(null);
});
document.head.appendChild(script);
removeScript(script);
});
};
/******************************************************************************/
context.lz4BlockCodec = {
createInstance: function(flavor) {
let instantiator;
if ( flavor === 'wasm' ) {
instantiator = createInstanceWASM;
} else if ( flavor === 'js' ) {
instantiator = createInstanceJS;
} else {
instantiator = createInstanceWASM || createInstanceJS;
}
return (instantiator)().then(instance => {
if ( instance ) { return instance; }
if ( flavor === undefined ) {
return createInstanceJS();
}
return null;
});
},
reset: function() {
context.LZ4BlockWASM = undefined;
context.LZ4BlockJS = undefined;
}
};
/******************************************************************************/
})(this || self); // <<<< End of private namespace
/******************************************************************************/

297
src/lib/lz4/lz4-block-codec-js.js

@ -0,0 +1,297 @@
/*******************************************************************************
lz4-block-codec-js.js
A javascript wrapper around a pure javascript implementation of
LZ4 block format codec.
Copyright (C) 2018 Raymond Hill
BSD-2-Clause License (http://www.opensource.org/licenses/bsd-license.php)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Home: https://github.com/gorhill/lz4-wasm
I used the same license as the one picked by creator of LZ4 out of respect
for his creation, see https://lz4.github.io/lz4/
*/
'use strict';
/******************************************************************************/
(function(context) { // >>>> Start of private namespace
/******************************************************************************/
const growOutputBuffer = function(instance, size) {
if (
instance.outputBuffer === undefined ||
instance.outputBuffer.byteLength < size
) {
instance.outputBuffer = new ArrayBuffer(size + 0xFFFF & 0x7FFF0000);
}
return instance.outputBuffer;
};
const encodeBound = function(size) {
return size > 0x7E000000 ?
0 :
size + (size / 255 | 0) + 16;
};
const encodeBlock = function(instance, iBuf, oOffset) {
let iLen = iBuf.byteLength;
if ( iLen >= 0x7E000000 ) { throw new RangeError(); }
// "The last match must start at least 12 bytes before end of block"
let lastMatchPos = iLen - 12;
// "The last 5 bytes are always literals"
let lastLiteralPos = iLen - 5;
if ( instance.hashTable === undefined ) {
instance.hashTable = new Int32Array(65536);
}
instance.hashTable.fill(-65536);
if ( iBuf instanceof ArrayBuffer ) {
iBuf = new Uint8Array(iBuf);
}
let oLen = oOffset + encodeBound(iLen);
let oBuf = new Uint8Array(growOutputBuffer(instance, oLen), 0, oLen);
let iPos = 0;
let oPos = oOffset;
let anchorPos = 0;
// sequence-finding loop
for (;;) {
let refPos;
let mOffset;
let sequence = iBuf[iPos] << 8 | iBuf[iPos+1] << 16 | iBuf[iPos+2] << 24;
// match-finding loop
while ( iPos <= lastMatchPos ) {
sequence = sequence >>> 8 | iBuf[iPos+3] << 24;
let hash = (sequence * 0x9E37 & 0xFFFF) + (sequence * 0x79B1 >>> 16) & 0xFFFF;
refPos = instance.hashTable[hash];
instance.hashTable[hash] = iPos;
mOffset = iPos - refPos;
if (
mOffset < 65536 &&
iBuf[refPos+0] === ((sequence ) & 0xFF) &&
iBuf[refPos+1] === ((sequence >>> 8) & 0xFF) &&
iBuf[refPos+2] === ((sequence >>> 16) & 0xFF) &&
iBuf[refPos+3] === ((sequence >>> 24) & 0xFF)
) {
break;
}
iPos += 1;
}
// no match found
if ( iPos > lastMatchPos ) { break; }
// match found
let lLen = iPos - anchorPos;
let mLen = iPos;
iPos += 4; refPos += 4;
while ( iPos < lastLiteralPos && iBuf[iPos] === iBuf[refPos] ) {
iPos += 1; refPos += 1;
}
mLen = iPos - mLen;
let token = mLen < 19 ? mLen - 4 : 15;
// write token, length of literals if needed
if ( lLen >= 15 ) {
oBuf[oPos++] = 0xF0 | token;
let l = lLen - 15;
while ( l >= 255 ) {
oBuf[oPos++] = 255;
l -= 255;
}
oBuf[oPos++] = l;
} else {
oBuf[oPos++] = (lLen << 4) | token;
}
// write literals
while ( lLen-- ) {
oBuf[oPos++] = iBuf[anchorPos++];
}
if ( mLen === 0 ) { break; }
// write offset of match
oBuf[oPos+0] = mOffset;
oBuf[oPos+1] = mOffset >>> 8;
oPos += 2;
// write length of match if needed
if ( mLen >= 19 ) {
let l = mLen - 19;
while ( l >= 255 ) {
oBuf[oPos++] = 255;
l -= 255;
}
oBuf[oPos++] = l;
}
anchorPos = iPos;
}
// last sequence is literals only
let lLen = iLen - anchorPos;
if ( lLen >= 15 ) {
oBuf[oPos++] = 0xF0;
let l = lLen - 15;
while ( l >= 255 ) {
oBuf[oPos++] = 255;
l -= 255;
}
oBuf[oPos++] = l;
} else {
oBuf[oPos++] = lLen << 4;
}
while ( lLen-- ) {
oBuf[oPos++] = iBuf[anchorPos++];
}
return new Uint8Array(oBuf.buffer, 0, oPos);
};
const decodeBlock = function(instance, iBuf, iOffset, oLen) {
let iLen = iBuf.byteLength;
let oBuf = new Uint8Array(growOutputBuffer(instance, oLen), 0, oLen);
let iPos = iOffset, oPos = 0;
while ( iPos < iLen ) {
let token = iBuf[iPos++];
// literals
let clen = token >>> 4;
// length of literals
if ( clen !== 0 ) {
if ( clen === 15 ) {
let l;
for (;;) {
l = iBuf[iPos++];
if ( l !== 255 ) { break; }
clen += 255;
}
clen += l;
}
// copy literals
let end = iPos + clen;
while ( iPos < end ) {
oBuf[oPos++] = iBuf[iPos++];
}
if ( iPos === iLen ) { break; }
}
// match
let mOffset = iBuf[iPos+0] | (iBuf[iPos+1] << 8);
if ( mOffset === 0 || mOffset > oPos ) { return; }
iPos += 2;
// length of match
clen = (token & 0x0F) + 4;
if ( clen === 19 ) {
let l;
for (;;) {
l = iBuf[iPos++];
if ( l !== 255 ) { break; }
clen += 255;
}
clen += l;
}
// copy match
let mPos = oPos - mOffset;
let end = oPos + clen;
while ( oPos < end ) {
oBuf[oPos++] = oBuf[mPos++];
}
}
return oBuf;
};
/******************************************************************************/
context.LZ4BlockJS = function() {
this.hashTable = undefined;
this.outputBuffer = undefined;
};
context.LZ4BlockJS.prototype = {
flavor: 'js',
init: function() {
return Promise.resolve(true);
},
reset: function() {
this.hashTable = undefined;
this.outputBuffer = undefined;
},
bytesInUse: function() {
let bytesInUse = 0;
if ( this.hashTable !== undefined ) {
bytesInUse += this.hashTable.byteLength;
}
if ( this.outputBuffer !== undefined ) {
bytesInUse += this.outputBuffer.byteLength;
}
return bytesInUse;
},
encodeBlock: function(input, outputOffset) {
if ( input instanceof ArrayBuffer ) {
input = new Uint8Array(input);
} else if ( input instanceof Uint8Array === false ) {
throw new TypeError();
}
return encodeBlock(this, input, outputOffset);
},
decodeBlock: function(input, inputOffset, outputSize) {
if ( input instanceof ArrayBuffer ) {
input = new Uint8Array(input);
} else if ( input instanceof Uint8Array === false ) {
throw new TypeError();
}
return decodeBlock(this, input, inputOffset, outputSize);
}
};
/******************************************************************************/
})(this || self); // <<<< End of private namespace
/******************************************************************************/

195
src/lib/lz4/lz4-block-codec-wasm.js

@ -0,0 +1,195 @@
/*******************************************************************************
lz4-block-codec-wasm.js
A javascript wrapper around a WebAssembly implementation of
LZ4 block format codec.
Copyright (C) 2018 Raymond Hill
BSD-2-Clause License (http://www.opensource.org/licenses/bsd-license.php)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Home: https://github.com/gorhill/lz4-wasm
I used the same license as the one picked by creator of LZ4 out of respect
for his creation, see https://lz4.github.io/lz4/
*/
/* global WebAssembly */
'use strict';
/******************************************************************************/
(function(context) { // >>>> Start of private namespace
/******************************************************************************/
const wd = (function() {
let url = document.currentScript.src;
let match = /[^\/]+$/.exec(url);
return match !== null ?
url.slice(0, match.index) :
'';
})();
const growMemoryTo = function(wasmInstance, byteLength) {
let lz4api = wasmInstance.exports;
let neededByteLength = lz4api.getLinearMemoryOffset() + byteLength;
let pageCountBefore = lz4api.memory.buffer.byteLength >>> 16;
let pageCountAfter = (neededByteLength + 65535) >>> 16;
if ( pageCountAfter > pageCountBefore ) {
lz4api.memory.grow(pageCountAfter - pageCountBefore);
}
return lz4api.memory.buffer;
};
const encodeBlock = function(wasmInstance, inputArray, outputOffset) {
let lz4api = wasmInstance.exports;
let mem0 = lz4api.getLinearMemoryOffset();
let hashTableSize = 65536 * 4;
let inputSize = inputArray.byteLength;
if ( inputSize >= 0x7E000000 ) { throw new RangeError(); }
let memSize =
hashTableSize +
inputSize +
outputOffset + lz4api.lz4BlockEncodeBound(inputSize);
let memBuffer = growMemoryTo(wasmInstance, memSize);
let hashTable = new Int32Array(memBuffer, mem0, 65536);
hashTable.fill(-65536, 0, 65536);
let inputMem = new Uint8Array(memBuffer, mem0 + hashTableSize, inputSize);
inputMem.set(inputArray);
let outputSize = lz4api.lz4BlockEncode(
mem0 + hashTableSize,
inputSize,
mem0 + hashTableSize + inputSize + outputOffset
);
if ( outputSize === 0 ) { return; }
let outputArray = new Uint8Array(
memBuffer,
mem0 + hashTableSize + inputSize,
outputOffset + outputSize
);
return outputArray;
};
const decodeBlock = function(wasmInstance, inputArray, inputOffset, outputSize) {
let inputSize = inputArray.byteLength;
let lz4api = wasmInstance.exports;
let mem0 = lz4api.getLinearMemoryOffset();
let memSize = inputSize + outputSize;
let memBuffer = growMemoryTo(wasmInstance, memSize);
let inputArea = new Uint8Array(memBuffer, mem0, inputSize);
inputArea.set(inputArray);
outputSize = lz4api.lz4BlockDecode(
mem0 + inputOffset,
inputSize - inputOffset,
mem0 + inputSize
);
if ( outputSize === 0 ) { return; }
return new Uint8Array(memBuffer, mem0 + inputSize, outputSize);
};
/******************************************************************************/
context.LZ4BlockWASM = function() {
this.lz4wasmInstance = undefined;
};
context.LZ4BlockWASM.prototype = {
flavor: 'wasm',
init: function() {
if (
typeof WebAssembly !== 'object' ||
typeof WebAssembly.instantiateStreaming !== 'function'
) {
this.lz4wasmInstance = null;
}
if ( this.lz4wasmInstance === null ) {
return Promise.resolve(false);
}
if ( this.lz4wasmInstance instanceof WebAssembly.Instance ) {
return Promise.resolve(true);
}
if ( this.lz4wasmInstance === undefined ) {
this.lz4wasmInstance = fetch(
wd + 'lz4-block-codec.wasm',
{ mode: 'same-origin' }
).then(
WebAssembly.instantiateStreaming
).then(result => {
this.lz4wasmInstance = result && result.instance || null;
}).catch(reason => {
this.lz4wasmInstance = null;
console.info(reason);
}).then(( ) =>
this.lz4wasmInstance !== null
);
}
return this.lz4wasmInstance;
},
reset: function() {
this.lz4wasmInstance = undefined;
},
bytesInUse: function() {
return this.lz4wasmInstance instanceof WebAssembly.Instance ?
this.lz4wasmInstance.exports.memory.buffer.byteLength :
0;
},
encodeBlock: function(input, outputOffset) {
if ( this.lz4wasmInstance instanceof WebAssembly.Instance === false ) {
throw new Error('LZ4BlockWASM: not initialized');
}
if ( input instanceof ArrayBuffer ) {
input = new Uint8Array(input);
} else if ( input instanceof Uint8Array === false ) {
throw new TypeError();
}
return encodeBlock(this.lz4wasmInstance, input, outputOffset);
},
decodeBlock: function(input, inputOffset, outputSize) {
if ( this.lz4wasmInstance instanceof WebAssembly.Instance === false ) {
throw new Error('LZ4BlockWASM: not initialized');
}
if ( input instanceof ArrayBuffer ) {
input = new Uint8Array(input);
} else if ( input instanceof Uint8Array === false ) {
throw new TypeError();
}
return decodeBlock(this.lz4wasmInstance, input, inputOffset, outputSize);
}
};
/******************************************************************************/
})(this || self); // <<<< End of private namespace
/******************************************************************************/

BIN
src/lib/lz4/lz4-block-codec.wasm

745
src/lib/lz4/lz4-block-codec.wat

@ -0,0 +1,745 @@
;;
;; lz4-block-codec.wat: a WebAssembly implementation of LZ4 block format codec
;; Copyright (C) 2018 Raymond Hill
;;
;; BSD-2-Clause License (http://www.opensource.org/licenses/bsd-license.php)
;;
;; Redistribution and use in source and binary forms, with or without
;; modification, are permitted provided that the following conditions are
;; met:
;;
;; 1. Redistributions of source code must retain the above copyright
;; notice, this list of conditions and the following disclaimer.
;;
;; 2. Redistributions in binary form must reproduce the above
;; copyright notice, this list of conditions and the following disclaimer
;; in the documentation and/or other materials provided with the
;; distribution.
;;
;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
;; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
;; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
;; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
;; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
;; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
;; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
;; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
;;
;; Home: https://github.com/gorhill/lz4-wasm
;;
;; I used the same license as the one picked by creator of LZ4 out of respect
;; for his creation, see https://lz4.github.io/lz4/
;;
(module
;;
;; module start
;;
;; (func $log (import "imports" "log") (param i32 i32 i32))
(memory (export "memory") 1)
;;
;; Public functions
;;
;;
;; Return an offset to the first byte of usable linear memory.
;; Might be useful in the future to reserve memory space for whatever purpose,
;; like config variables, etc.
;;
(func $getLinearMemoryOffset (export "getLinearMemoryOffset")
(result i32)
i32.const 0
)
;;
;; unsigned int lz4BlockEncodeBound()
;;
;; Return the maximum size of the output buffer holding the compressed data.
;;
;; Reference implementation:
;; https://github.com/lz4/lz4/blob/dev/lib/lz4.h#L156
;;
(func (export "lz4BlockEncodeBound")
(param $ilen i32)
(result i32)
get_local $ilen
i32.const 0x7E000000
i32.gt_u
if
i32.const 0
return
end
get_local $ilen
get_local $ilen
i32.const 255
i32.div_u
i32.add
i32.const 16
i32.add
)
;;
;; unsigned int lz4BlockEncode(
;; unsigned int inPtr,
;; unsigned int ilen,
;; unsigned int outPtr
;; )
;;
;; https://github.com/lz4/lz4/blob/dev/lib/lz4.c#L651
;;
;; The implementation below is modified from the reference one.
;;
;; - There is no skip adjustement for repeated failure to find a match.
;;
;; - All configurable values are hard-coded to match the generic version
;; of the compressor.
;;
;; Note the size of the input block is NOT encoded in the output buffer, it
;; is for the caller to figure how they will save that information on
;; their side. At this point it is probably a trivial amount of work to
;; implement the LZ4 frame format, which encode the content size, but this
;; is for another day.
;;
(func $lz4BlockEncode (export "lz4BlockEncode")
(param $inPtr i32) ;; pointer to start of input buffer
(param $ilen i32) ;; size of input buffer
(param $outPtr i32) ;; pointer to start of output buffer
(result i32)
(local $hashPtrBeg i32) ;; start of hash buffer
(local $hashPtr i32) ;; current hash entry
(local $anchorPtr i32) ;; anchor position in input
(local $inPtrEnd1 i32) ;; point in input at which match-finding must cease
(local $inPtrEnd2 i32) ;; point in input at which match-length finding must cease
(local $inPtrEnd i32) ;; point to end of input
(local $outPtrBeg i32) ;; start of output buffer
(local $refPtr i32) ;; start of match in input
(local $seq32 i32) ;; 4-byte value from current input position
(local $llen i32) ;; length of found literals
(local $moffset i32) ;; offset to found match from current input position
(local $mlen i32) ;; length of found match
get_local $ilen ;; empty input = empty output
i32.const 0x7E000000 ;; max input size: 0x7E000000
i32.gt_u
if
i32.const 0
return
end
get_local $ilen ;; "blocks < 13 bytes cannot be compressed"
i32.const 13
i32.lt_u
if
i32.const 0
return
end
call $getLinearMemoryOffset ;; hash table is at start of usable memory
set_local $hashPtrBeg
get_local $inPtr
tee_local $anchorPtr
get_local $ilen
i32.add
tee_local $inPtrEnd
i32.const -5 ;; "The last 5 bytes are always literals."
i32.add
tee_local $inPtrEnd2
i32.const -7 ;; "The last match must start at least 12 bytes before end of block"
i32.add
set_local $inPtrEnd1
get_local $outPtr
set_local $outPtrBeg
;;
;; sequence processing loop
;;
block $noMoreSequence loop $nextSequence
get_local $inPtr
get_local $inPtrEnd1
i32.ge_u ;; 5 or less bytes left?
br_if $noMoreSequence
get_local $inPtr ;; first sequence of 3 bytes before match-finding loop
i32.load8_u
i32.const 8
i32.shl
get_local $inPtr
i32.load8_u offset=1
i32.const 16
i32.shl
i32.or
get_local $inPtr
i32.load8_u offset=2
i32.const 24
i32.shl
i32.or
set_local $seq32
;;
;; match-finding loop
;;
loop $findMatch block $noMatchFound
get_local $inPtr
get_local $inPtrEnd2
i32.gt_u ;; less than 12 bytes left?
br_if $noMoreSequence
get_local $seq32 ;; update last byte of current sequence
i32.const 8
i32.shr_u
get_local $inPtr
i32.load8_u offset=3
i32.const 24
i32.shl
i32.or
tee_local $seq32
i32.const 0x9E3779B1 ;; compute 16-bit hash
i32.mul
i32.const 16
i32.shr_u ;; hash value is at top of stack
i32.const 2 ;; lookup refPtr at hash entry
i32.shl
get_local $hashPtrBeg
i32.add
tee_local $hashPtr
i32.load
set_local $refPtr
get_local $hashPtr ;; update hash entry with inPtr
get_local $inPtr
i32.store
get_local $inPtr
get_local $refPtr
i32.sub
tee_local $moffset ;; remember match offset, we will need it in case of match
i32.const 0xFFFF
i32.gt_s ;; match offset > 65535 = unusable match
br_if $noMatchFound
;;
;; confirm match: different sequences can yield same hash
;; compare-branch each byte to potentially save memory read ops
;;
get_local $seq32 ;; byte 0
i32.const 0xFF
i32.and
get_local $refPtr
i32.load8_u
i32.ne ;; refPtr[0] !== inPtr[0]
br_if $noMatchFound
get_local $seq32 ;; byte 1
i32.const 8
i32.shr_u
i32.const 0xFF
i32.and
get_local $refPtr
i32.load8_u offset=1
i32.ne
br_if $noMatchFound ;; refPtr[1] !== inPtr[1]
get_local $seq32 ;; byte 2
i32.const 16
i32.shr_u
i32.const 0xFF
i32.and
get_local $refPtr
i32.load8_u offset=2
i32.ne ;; refPtr[2] !== inPtr[2]
br_if $noMatchFound
get_local $seq32 ;; byte 3
i32.const 24
i32.shr_u
i32.const 0xFF
i32.and
get_local $refPtr
i32.load8_u offset=3
i32.ne ;; refPtr[3] !== inPtr[3]
br_if $noMatchFound
;;
;; a valid match has been found at this point
;;
get_local $inPtr ;; compute length of literals
get_local $anchorPtr
i32.sub
set_local $llen
get_local $inPtr ;; find match length
i32.const 4 ;; skip over confirmed 4-byte match
i32.add
set_local $inPtr
get_local $refPtr
i32.const 4
i32.add
tee_local $mlen ;; remember refPtr to later compute match length
set_local $refPtr
block $endOfMatch loop ;; scan input buffer until match ends
get_local $inPtr
get_local $inPtrEnd2
i32.ge_u
br_if $endOfMatch
get_local $inPtr
i32.load8_u
get_local $refPtr
i32.load8_u
i32.ne
br_if $endOfMatch
get_local $inPtr
i32.const 1
i32.add
set_local $inPtr
get_local $refPtr
i32.const 1
i32.add
set_local $refPtr
br 0
end end $endOfMatch
;; encode token
get_local $outPtr ;; output token
get_local $llen
get_local $refPtr
get_local $mlen
i32.sub
tee_local $mlen
call $writeToken
get_local $outPtr
i32.const 1
i32.add
set_local $outPtr
get_local $llen ;; encode/write length of literals if needed
i32.const 15
i32.ge_s
if
get_local $outPtr
get_local $llen
call $writeLength
set_local $outPtr
end
;; copy literals
get_local $outPtr
get_local $anchorPtr
get_local $llen
call $copy
get_local $outPtr
get_local $llen
i32.add
set_local $outPtr
;; encode match offset
get_local $outPtr
get_local $moffset
i32.store8
get_local $outPtr
get_local $moffset
i32.const 8
i32.shr_u
i32.store8 offset=1
get_local $outPtr
i32.const 2
i32.add
set_local $outPtr
get_local $mlen ;; encode/write length of match if needed
i32.const 15
i32.ge_s
if
get_local $outPtr
get_local $mlen
call $writeLength
set_local $outPtr
end
get_local $inPtr ;; advance anchor to current position
set_local $anchorPtr
br $nextSequence
end $noMatchFound
get_local $inPtr ;; no match found: advance to next byte
i32.const 1
i32.add
set_local $inPtr
br $findMatch end ;; match offset > 65535 = unusable match
end end $noMoreSequence
;;
;; generate last (match-less) sequence if compression succeeded
;;
get_local $outPtr
get_local $outPtrBeg
i32.eq
if
i32.const 0
return
end
get_local $outPtr
get_local $inPtrEnd
get_local $anchorPtr
i32.sub
tee_local $llen
i32.const 0
call $writeToken
get_local $outPtr
i32.const 1
i32.add
set_local $outPtr
get_local $llen
i32.const 15
i32.ge_u
if
get_local $outPtr
get_local $llen
call $writeLength
set_local $outPtr
end
get_local $outPtr
get_local $anchorPtr
get_local $llen
call $copy
get_local $outPtr ;; return number of written bytes
get_local $llen
i32.add
get_local $outPtrBeg
i32.sub
)
;;
;; unsigned int lz4BlockDecode(
;; unsigned int inPtr,
;; unsigned int ilen
;; unsigned int outPtr
;; )
;;
;; Reference:
;; https://github.com/lz4/lz4/blob/dev/doc/lz4_Block_format.md
;;
(func (export "lz4BlockDecode")
(param $inPtr0 i32) ;; start of input buffer
(param $ilen i32) ;; length of input buffer
(param $outPtr0 i32) ;; start of output buffer
(result i32)
(local $inPtr i32) ;; current position in input buffer
(local $inPtrEnd i32) ;; end of input buffer
(local $outPtr i32) ;; current position in output buffer
(local $matchPtr i32) ;; position of current match
(local $token i32) ;; sequence token
(local $clen i32) ;; number of bytes to copy
(local $_ i32) ;; general purpose variable
get_local $ilen ;; if ( ilen == 0 ) { return 0; }
i32.eqz
if
i32.const 0
return
end
get_local $inPtr0
tee_local $inPtr ;; current position in input buffer
get_local $ilen
i32.add
set_local $inPtrEnd
get_local $outPtr0 ;; start of output buffer
set_local $outPtr ;; current position in output buffer
block $noMoreSequence loop ;; iterate through all sequences
get_local $inPtr
get_local $inPtrEnd
i32.ge_u
br_if $noMoreSequence ;; break when nothing left to read in input buffer
get_local $inPtr ;; read token -- consume one byte
i32.load8_u
get_local $inPtr
i32.const 1
i32.add
set_local $inPtr
tee_local $token ;; extract length of literals from token
i32.const 4
i32.shr_u
tee_local $clen ;; consume extra length bytes if present
i32.eqz
if else
get_local $clen
i32.const 15
i32.eq
if loop
get_local $inPtr
i32.load8_u
get_local $inPtr
i32.const 1
i32.add
set_local $inPtr
tee_local $_
get_local $clen
i32.add
set_local $clen
get_local $_
i32.const 255
i32.eq
br_if 0
end end
get_local $outPtr ;; copy literals to ouput buffer
get_local $inPtr
get_local $clen
call $copy
get_local $outPtr ;; advance output buffer pointer past copy
get_local $clen
i32.add
set_local $outPtr
get_local $clen ;; advance input buffer pointer past literals
get_local $inPtr
i32.add
tee_local $inPtr
get_local $inPtrEnd ;; exit if this is the last sequence
i32.eq
br_if $noMoreSequence
end
get_local $outPtr ;; read match offset
get_local $inPtr
i32.load8_u
get_local $inPtr
i32.load8_u offset=1
i32.const 8
i32.shl
i32.or
i32.sub
tee_local $matchPtr
get_local $outPtr ;; match position can't be outside input buffer bounds
i32.eq
br_if $noMoreSequence
get_local $matchPtr
get_local $inPtrEnd
i32.lt_u
br_if $noMoreSequence
get_local $inPtr ;; advance input pointer past match offset bytes
i32.const 2
i32.add
set_local $inPtr
get_local $token ;; extract length of match from token
i32.const 15
i32.and
i32.const 4
i32.add
tee_local $clen
i32.const 19 ;; consume extra length bytes if present
i32.eq
if loop
get_local $inPtr
i32.load8_u
get_local $inPtr
i32.const 1
i32.add
set_local $inPtr
tee_local $_
get_local $clen
i32.add
set_local $clen
get_local $_
i32.const 255
i32.eq
br_if 0
end end
get_local $outPtr ;; copy match to ouput buffer
get_local $matchPtr
get_local $clen
call $copy
get_local $clen ;; advance output buffer pointer past copy
get_local $outPtr
i32.add
set_local $outPtr
br 0
end end $noMoreSequence
get_local $outPtr ;; return number of written bytes
get_local $outPtr0
i32.sub
)
;;
;; Private functions
;;
;;
;; Encode a sequence token
;;
;; Reference documentation:
;; https://github.com/lz4/lz4/blob/dev/doc/lz4_Block_format.md
;;
(func $writeToken
(param $outPtr i32)
(param $llen i32)
(param $mlen i32)
get_local $outPtr
get_local $llen
i32.const 15
get_local $llen
i32.const 15
i32.lt_u
select
i32.const 4
i32.shl
get_local $mlen
i32.const 15
get_local $mlen
i32.const 15
i32.lt_u
select
i32.or
i32.store8
)
;;
;; Encode and output length bytes. The return value is the pointer following
;; the last byte written.
;;
;; Reference documentation:
;; https://github.com/lz4/lz4/blob/dev/doc/lz4_Block_format.md
;;
(func $writeLength
(param $outPtr i32)
(param $len i32)
(result i32)
get_local $len
i32.const 15
i32.sub
set_local $len
loop
get_local $outPtr
get_local $len
i32.const 255
get_local $len
i32.const 255
i32.lt_u
select
i32.store8
get_local $outPtr
i32.const 1
i32.add
set_local $outPtr
get_local $len
i32.const 255
i32.sub
tee_local $len
i32.const 0
i32.ge_s
br_if 0
end
get_local $outPtr
)
;;
;; Copy n bytes from source to destination.
;;
;; It is overlap-safe only from left-to-right -- which is only what is
;; required in the current module.
;;
(func $copy
(param $dst i32)
(param $src i32)
(param $len i32)
block $lessThan8 loop
get_local $len
i32.const 8
i32.lt_u
br_if $lessThan8
get_local $dst
get_local $src
i32.load8_u
i32.store8
get_local $dst
get_local $src
i32.load8_u offset=1
i32.store8 offset=1
get_local $dst
get_local $src
i32.load8_u offset=2
i32.store8 offset=2
get_local $dst
get_local $src
i32.load8_u offset=3
i32.store8 offset=3
get_local $dst
get_local $src
i32.load8_u offset=4
i32.store8 offset=4
get_local $dst
get_local $src
i32.load8_u offset=5
i32.store8 offset=5
get_local $dst
get_local $src
i32.load8_u offset=6
i32.store8 offset=6
get_local $dst
get_local $src
i32.load8_u offset=7
i32.store8 offset=7
get_local $dst
i32.const 8
i32.add
set_local $dst
get_local $src
i32.const 8
i32.add
set_local $src
get_local $len
i32.const -8
i32.add
set_local $len
br 0
end end $lessThan8
get_local $len
i32.const 4
i32.ge_u
if
get_local $dst
get_local $src
i32.load8_u
i32.store8
get_local $dst
get_local $src
i32.load8_u offset=1
i32.store8 offset=1
get_local $dst
get_local $src
i32.load8_u offset=2
i32.store8 offset=2
get_local $dst
get_local $src
i32.load8_u offset=3
i32.store8 offset=3
get_local $dst
i32.const 4
i32.add
set_local $dst
get_local $src
i32.const 4
i32.add
set_local $src
get_local $len
i32.const -4
i32.add
set_local $len
end
get_local $len
i32.const 2
i32.ge_u
if
get_local $dst
get_local $src
i32.load8_u
i32.store8
get_local $dst
get_local $src
i32.load8_u offset=1
i32.store8 offset=1
get_local $dst
i32.const 2
i32.add
set_local $dst
get_local $src
i32.const 2
i32.add
set_local $src
get_local $len
i32.const -2
i32.add
set_local $len
end
get_local $len
i32.eqz
if else
get_local $dst
get_local $src
i32.load8_u
i32.store8
end
)
;;
;; module end
;;
)

343
src/lib/publicsuffixlist.js

@ -1,343 +0,0 @@
/*******************************************************************************
publicsuffixlist.js - an efficient javascript implementation to deal with
Mozilla Foundation's Public Suffix List <http://publicsuffix.org/list/>
Copyright (C) 2013 Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
/*! Home: https://github.com/gorhill/publicsuffixlist.js */
'use strict';
/*
This code is mostly dumb: I consider this to be lower-level code, thus
in order to ensure efficiency, the caller is responsible for sanitizing
the inputs.
*/
/******************************************************************************/
// A single instance of PublicSuffixList is enough.
;(function(root) {
/******************************************************************************/
var exceptions = new Map();
var rules = new Map();
// This value dictate how the search will be performed:
// < this.cutoffLength = indexOf()
// >= this.cutoffLength = binary search
var cutoffLength = 256;
var mustPunycode = /[^\w.*-]/;
/******************************************************************************/
// In the context of this code, a domain is defined as:
// "{label}.{public suffix}".
// A single standalone label is a public suffix as per
// http://publicsuffix.org/list/:
// "If no rules match, the prevailing rule is '*' "
// This means 'localhost' is not deemed a domain by this
// code, since according to the definition above, it would be
// evaluated as a public suffix. The caller is therefore responsible to
// decide how to further interpret such public suffix.
//
// `hostname` must be a valid ascii-based hostname.
function getDomain(hostname) {
// A hostname starting with a dot is not a valid hostname.
if ( !hostname || hostname.charAt(0) === '.' ) {
return '';
}
hostname = hostname.toLowerCase();
var suffix = getPublicSuffix(hostname);
if ( suffix === hostname ) {
return '';
}
var pos = hostname.lastIndexOf('.', hostname.lastIndexOf('.', hostname.length - suffix.length) - 1);
if ( pos <= 0 ) {
return hostname;
}
return hostname.slice(pos + 1);
}
/******************************************************************************/
// Return longest public suffix.
//
// `hostname` must be a valid ascii-based string which respect hostname naming.
function getPublicSuffix(hostname) {
if ( !hostname ) {
return '';
}
// Since we slice down the hostname with each pass, the first match
// is the longest, so no need to find all the matching rules.
while ( true ) {
let pos = hostname.indexOf('.');
if ( pos < 0 ) {
return hostname;
}
if ( search(exceptions, hostname) ) {
return hostname.slice(pos + 1);
}
if ( search(rules, hostname) ) {
return hostname;
}
if ( search(rules, '*' + hostname.slice(pos)) ) {
return hostname;
}
hostname = hostname.slice(pos + 1);
}
// unreachable
}
/******************************************************************************/
// Look up a specific hostname.
function search(store, hostname) {
// Extract TLD
let tld, remainder;
let pos = hostname.lastIndexOf('.');
if ( pos === -1 ) {
tld = hostname;
remainder = hostname;
} else {
tld = hostname.slice(pos + 1);
remainder = hostname.slice(0, pos);
}
let substore = store.get(tld);
if ( substore === undefined ) {
return false;
}
// If substore is a string, use indexOf()
if ( typeof substore === 'string' ) {
return substore.indexOf(' ' + remainder + ' ') >= 0;
}
// It is an array: use binary search.
let l = remainder.length;
if ( l >= substore.length ) {
return false;
}
let haystack = substore[l];
if ( haystack === null ) {
return false;
}
let left = 0;
let right = Math.floor(haystack.length / l + 0.5);
while ( left < right ) {
let i = left + right >> 1;
let needle = haystack.substr(l*i, l);
if ( remainder < needle ) {
right = i;
} else if ( remainder > needle ) {
left = i + 1;
} else {
return true;
}
}
return false;
}
/******************************************************************************/
// Parse and set a UTF-8 text-based suffix list. Format is same as found at:
// http://publicsuffix.org/list/
//
// `toAscii` is a converter from unicode to punycode. Required since the
// Public Suffix List contains unicode characters.
// Suggestion: use <https://github.com/bestiejs/punycode.js> it's quite good.
function parse(text, toAscii) {
exceptions = new Map();
rules = new Map();
let lineBeg = 0;
let textEnd = text.length;
while ( lineBeg < textEnd ) {
let lineEnd = text.indexOf('\n', lineBeg);
if ( lineEnd < 0 ) {
lineEnd = text.indexOf('\r', lineBeg);
if ( lineEnd < 0 ) {
lineEnd = textEnd;
}
}
let line = text.slice(lineBeg, lineEnd).trim();
lineBeg = lineEnd + 1;
if ( line.length === 0 ) {
continue;
}
// Ignore comments
let pos = line.indexOf('//');
if ( pos !== -1 ) {
line = line.slice(0, pos);
}
// Ignore surrounding whitespaces
line = line.trim();
if ( line.length === 0 ) {
continue;
}
// Is this an exception rule?
let store;
if ( line.charAt(0) === '!' ) {
store = exceptions;
line = line.slice(1);
} else {
store = rules;
}
if ( mustPunycode.test(line) ) {
line = toAscii(line);
}
// http://publicsuffix.org/list/:
// "... all rules must be canonicalized in the normal way
// for hostnames - lower-case, Punycode ..."
line = line.toLowerCase();
// Extract TLD
let tld;
pos = line.lastIndexOf('.');
if ( pos === -1 ) {
tld = line;
} else {
tld = line.slice(pos + 1);
line = line.slice(0, pos);
}
// Store suffix using tld as key
let substore = store.get(tld);
if ( substore === undefined ) {
store.set(tld, substore = []);
}
if ( line ) {
substore.push(line);
}
}
crystallize(exceptions);
crystallize(rules);
window.dispatchEvent(new CustomEvent('publicSuffixList'));
}
/******************************************************************************/
// Cristallize the storage of suffixes using optimal internal representation
// for future look up.
function crystallize(store) {
for ( let entry of store ) {
let tld = entry[0];
let suffixes = entry[1];
// No suffix
if ( suffixes.length === 0 ) {
store.set(tld, '');
continue;
}
// Concatenated list of suffixes less than cutoff length:
// Store as string, lookup using indexOf()
let s = suffixes.join(' ');
if ( s.length < cutoffLength ) {
store.set(tld, ' ' + s + ' ');
continue;
}
// Concatenated list of suffixes greater or equal to cutoff length
// Store as array keyed on suffix length, lookup using binary search.
// I borrowed the idea to key on string length here:
// http://ejohn.org/blog/dictionary-lookups-in-javascript/#comment-392072
let buckets = [];
for ( let suffix of suffixes ) {
let l = suffix.length;
if ( buckets.length <= l ) {
extendArray(buckets, l);
}
if ( buckets[l] === null ) {
buckets[l] = [];
}
buckets[l].push(suffix);
}
for ( let i = 0; i < buckets.length; i++ ) {
let bucket = buckets[i];
if ( bucket !== null ) {
buckets[i] = bucket.sort().join('');
}
}
store.set(tld, buckets);
}
return store;
}
let extendArray = function(aa, rb) {
for ( let i = aa.length; i <= rb; i++ ) {
aa.push(null);
}
};
/******************************************************************************/
let selfieMagic = 3;
let toSelfie = function() {
return {
magic: selfieMagic,
rules: Array.from(rules),
exceptions: Array.from(exceptions)
};
};
let fromSelfie = function(selfie) {
if ( selfie instanceof Object === false || selfie.magic !== selfieMagic ) {
return false;
}
rules = new Map(selfie.rules);
exceptions = new Map(selfie.exceptions);
window.dispatchEvent(new CustomEvent('publicSuffixList'));
return true;
};
/******************************************************************************/
// Public API
root = root || window;
root.publicSuffixList = {
'version': '1.0',
'parse': parse,
'getDomain': getDomain,
'getPublicSuffix': getPublicSuffix,
'toSelfie': toSelfie,
'fromSelfie': fromSelfie
};
/******************************************************************************/
})(this);

647
src/lib/publicsuffixlist/publicsuffixlist.js

@ -0,0 +1,647 @@
/*******************************************************************************
publicsuffixlist.js - an efficient javascript implementation to deal with
Mozilla Foundation's Public Suffix List <http://publicsuffix.org/list/>
Copyright (C) 2013-present Raymond Hill
License: pick the one which suits you:
GPL v3 see <https://www.gnu.org/licenses/gpl.html>
APL v2 see <http://www.apache.org/licenses/LICENSE-2.0>
*/
/*! Home: https://github.com/gorhill/publicsuffixlist.js -- GPLv3 APLv2 */
/* jshint browser:true, esversion:6, laxbreak:true, undef:true, unused:true */
/* globals WebAssembly, console, exports:true, module */
/*******************************************************************************
Reference:
https://publicsuffix.org/list/
Excerpt:
> Algorithm
>
> 1. Match domain against all rules and take note of the matching ones.
> 2. If no rules match, the prevailing rule is "*".
> 3. If more than one rule matches, the prevailing rule is the one which
is an exception rule.
> 4. If there is no matching exception rule, the prevailing rule is the
one with the most labels.
> 5. If the prevailing rule is a exception rule, modify it by removing
the leftmost label.
> 6. The public suffix is the set of labels from the domain which match
the labels of the prevailing rule, using the matching algorithm above.
> 7. The registered or registrable domain is the public suffix plus one
additional label.
*/
/******************************************************************************/
(function(context) {
// >>>>>>>> start of anonymous namespace
'use strict';
/*******************************************************************************
Tree encoding in array buffer:
Node:
+ u8: length of char data
+ u8: flags => bit 0: is_publicsuffix, bit 1: is_exception
+ u16: length of array of children
+ u32: char data or offset to char data
+ u32: offset to array of children
= 12 bytes
More bits in flags could be used; for example:
- to distinguish private suffixes
*/
// i32 / i8
const HOSTNAME_SLOT = 0; // jshint ignore:line
const LABEL_INDICES_SLOT = 256; // -- / 256
const RULES_PTR_SLOT = 100; // 100 / 400
const CHARDATA_PTR_SLOT = 101; // 101 / 404
const EMPTY_STRING = '';
const SELFIE_MAGIC = 2;
let wasmMemory;
let pslBuffer32;
let pslBuffer8;
let pslByteLength = 0;
let hostnameArg = EMPTY_STRING;
/******************************************************************************/
const fireChangedEvent = function() {
if (
window instanceof Object &&
window.dispatchEvent instanceof Function &&
window.CustomEvent instanceof Function
) {
window.dispatchEvent(new CustomEvent('publicSuffixListChanged'));
}
};
/******************************************************************************/
const allocateBuffers = function(byteLength) {
pslByteLength = byteLength + 3 & ~3;
if (
pslBuffer32 !== undefined &&
pslBuffer32.byteLength >= pslByteLength
) {
return;
}
if ( wasmMemory !== undefined ) {
const newPageCount = pslByteLength + 0xFFFF >>> 16;
const curPageCount = wasmMemory.buffer.byteLength >>> 16;
const delta = newPageCount - curPageCount;
if ( delta > 0 ) {
wasmMemory.grow(delta);
pslBuffer32 = new Uint32Array(wasmMemory.buffer);
pslBuffer8 = new Uint8Array(wasmMemory.buffer);
}
} else {
pslBuffer8 = new Uint8Array(pslByteLength);
pslBuffer32 = new Uint32Array(pslBuffer8.buffer);
}
hostnameArg = '';
pslBuffer8[LABEL_INDICES_SLOT] = 0;
};
/******************************************************************************/
// Parse and set a UTF-8 text-based suffix list. Format is same as found at:
// http://publicsuffix.org/list/
//
// `toAscii` is a converter from unicode to punycode. Required since the
// Public Suffix List contains unicode characters.
// Suggestion: use <https://github.com/bestiejs/punycode.js>
const parse = function(text, toAscii) {
// Use short property names for better minifying results
const rootRule = {
l: EMPTY_STRING, // l => label
f: 0, // f => flags
c: undefined // c => children
};
// Tree building
{
const compareLabels = function(a, b) {
let n = a.length;
let d = n - b.length;
if ( d !== 0 ) { return d; }
for ( let i = 0; i < n; i++ ) {
d = a.charCodeAt(i) - b.charCodeAt(i);
if ( d !== 0 ) { return d; }
}
return 0;
};
const addToTree = function(rule, exception) {
let node = rootRule;
let end = rule.length;
while ( end > 0 ) {
const beg = rule.lastIndexOf('.', end - 1);
const label = rule.slice(beg + 1, end);
end = beg;
if ( Array.isArray(node.c) === false ) {
const child = { l: label, f: 0, c: undefined };
node.c = [ child ];
node = child;
continue;
}
let left = 0;
let right = node.c.length;
while ( left < right ) {
const i = left + right >>> 1;
const d = compareLabels(label, node.c[i].l);
if ( d < 0 ) {
right = i;
if ( right === left ) {
const child = {
l: label,
f: 0,
c: undefined
};
node.c.splice(left, 0, child);
node = child;
break;
}
continue;
}
if ( d > 0 ) {
left = i + 1;
if ( left === right ) {
const child = {
l: label,
f: 0,
c: undefined
};
node.c.splice(right, 0, child);
node = child;
break;
}
continue;
}
/* d === 0 */
node = node.c[i];
break;
}
}
node.f |= 0b01;
if ( exception ) {
node.f |= 0b10;
}
};
// 2. If no rules match, the prevailing rule is "*".
addToTree('*', false);
const mustPunycode = /[^a-z0-9.-]/;
const textEnd = text.length;
let lineBeg = 0;
while ( lineBeg < textEnd ) {
let lineEnd = text.indexOf('\n', lineBeg);
if ( lineEnd === -1 ) {
lineEnd = text.indexOf('\r', lineBeg);
if ( lineEnd === -1 ) {
lineEnd = textEnd;
}
}
let line = text.slice(lineBeg, lineEnd).trim();
lineBeg = lineEnd + 1;
// Ignore comments
const pos = line.indexOf('//');
if ( pos !== -1 ) {
line = line.slice(0, pos);
}
// Ignore surrounding whitespaces
line = line.trim();
if ( line.length === 0 ) { continue; }
const exception = line.charCodeAt(0) === 0x21 /* '!' */;
if ( exception ) {
line = line.slice(1);
}
if ( mustPunycode.test(line) ) {
line = toAscii(line.toLowerCase());
}
addToTree(line, exception);
}
}
{
const labelToOffsetMap = new Map();
const treeData = [];
const charData = [];
const allocate = function(n) {
const ibuf = treeData.length;
for ( let i = 0; i < n; i++ ) {
treeData.push(0);
}
return ibuf;
};
const storeNode = function(ibuf, node) {
const nChars = node.l.length;
const nChildren = node.c !== undefined
? node.c.length
: 0;
treeData[ibuf+0] = nChildren << 16 | node.f << 8 | nChars;
// char data
if ( nChars <= 4 ) {
let v = 0;
if ( nChars > 0 ) {
v |= node.l.charCodeAt(0);
if ( nChars > 1 ) {
v |= node.l.charCodeAt(1) << 8;
if ( nChars > 2 ) {
v |= node.l.charCodeAt(2) << 16;
if ( nChars > 3 ) {
v |= node.l.charCodeAt(3) << 24;
}
}
}
}
treeData[ibuf+1] = v;
} else {
let offset = labelToOffsetMap.get(node.l);
if ( offset === undefined ) {
offset = charData.length;
for ( let i = 0; i < nChars; i++ ) {
charData.push(node.l.charCodeAt(i));
}
labelToOffsetMap.set(node.l, offset);
}
treeData[ibuf+1] = offset;
}
// child nodes
if ( Array.isArray(node.c) === false ) {
treeData[ibuf+2] = 0;
return;
}
const iarray = allocate(nChildren * 3);
treeData[ibuf+2] = iarray;
for ( let i = 0; i < nChildren; i++ ) {
storeNode(iarray + i * 3, node.c[i]);
}
};
// First 512 bytes are reserved for internal use
allocate(512 >> 2);
const iRootRule = allocate(3);
storeNode(iRootRule, rootRule);
treeData[RULES_PTR_SLOT] = iRootRule;
const iCharData = treeData.length << 2;
treeData[CHARDATA_PTR_SLOT] = iCharData;
const byteLength = (treeData.length << 2) + (charData.length + 3 & ~3);
allocateBuffers(byteLength);
pslBuffer32.set(treeData);
pslBuffer8.set(charData, treeData.length << 2);
}
fireChangedEvent();
};
/******************************************************************************/
const setHostnameArg = function(hostname) {
const buf = pslBuffer8;
if ( hostname === hostnameArg ) { return buf[LABEL_INDICES_SLOT]; }
if ( hostname === null || hostname.length === 0 ) {
hostnameArg = '';
return (buf[LABEL_INDICES_SLOT] = 0);
}
hostname = hostname.toLowerCase();
hostnameArg = hostname;
let n = hostname.length;
if ( n > 255 ) { n = 255; }
buf[LABEL_INDICES_SLOT] = n;
let i = n;
let j = LABEL_INDICES_SLOT + 1;
while ( i-- ) {
const c = hostname.charCodeAt(i);
if ( c === 0x2E /* '.' */ ) {
buf[j+0] = i + 1;
buf[j+1] = i;
j += 2;
}
buf[i] = c;
}
buf[j] = 0;
return n;
};
/******************************************************************************/
// Returns an offset to the start of the public suffix.
//
// WASM-able, because no information outside the buffer content is required.
const getPublicSuffixPosJS = function() {
const buf8 = pslBuffer8;
const buf32 = pslBuffer32;
const iCharData = buf32[CHARDATA_PTR_SLOT];
let iNode = pslBuffer32[RULES_PTR_SLOT];
let cursorPos = -1;
let iLabel = LABEL_INDICES_SLOT;
// Label-lookup loop
for (;;) {
// Extract label indices
const labelBeg = buf8[iLabel+1];
const labelLen = buf8[iLabel+0] - labelBeg;
// Match-lookup loop: binary search
let r = buf32[iNode+0] >>> 16;
if ( r === 0 ) { break; }
const iCandidates = buf32[iNode+2];
let l = 0;
let iFound = 0;
while ( l < r ) {
const iCandidate = l + r >>> 1;
const iCandidateNode = iCandidates + iCandidate + (iCandidate << 1);
const candidateLen = buf32[iCandidateNode+0] & 0x000000FF;
let d = labelLen - candidateLen;
if ( d === 0 ) {
const iCandidateChar = candidateLen <= 4
? iCandidateNode + 1 << 2
: iCharData + buf32[iCandidateNode+1];
for ( let i = 0; i < labelLen; i++ ) {
d = buf8[labelBeg+i] - buf8[iCandidateChar+i];
if ( d !== 0 ) { break; }
}
}
if ( d < 0 ) {
r = iCandidate;
} else if ( d > 0 ) {
l = iCandidate + 1;
} else /* if ( d === 0 ) */ {
iFound = iCandidateNode;
break;
}
}
// 2. If no rules match, the prevailing rule is "*".
if ( iFound === 0 ) {
if ( buf8[iCandidates + 1 << 2] !== 0x2A /* '*' */ ) { break; }
iFound = iCandidates;
}
iNode = iFound;
// 5. If the prevailing rule is a exception rule, modify it by
// removing the leftmost label.
if ( (buf32[iNode+0] & 0x00000200) !== 0 ) {
if ( iLabel > LABEL_INDICES_SLOT ) {
return iLabel - 2;
}
break;
}
if ( (buf32[iNode+0] & 0x00000100) !== 0 ) {
cursorPos = iLabel;
}
if ( labelBeg === 0 ) { break; }
iLabel += 2;
}
return cursorPos;
};
let getPublicSuffixPosWASM;
let getPublicSuffixPos = getPublicSuffixPosJS;
/******************************************************************************/
const getPublicSuffix = function(hostname) {
if ( pslBuffer32 === undefined ) { return EMPTY_STRING; }
const hostnameLen = setHostnameArg(hostname);
const buf8 = pslBuffer8;
if ( hostnameLen === 0 || buf8[0] === 0x2E /* '.' */ ) {
return EMPTY_STRING;
}
const cursorPos = getPublicSuffixPos();
if ( cursorPos === -1 ) {
return EMPTY_STRING;
}
const beg = buf8[cursorPos + 1];
return beg === 0 ? hostnameArg : hostnameArg.slice(beg);
};
/******************************************************************************/
const getDomain = function(hostname) {
if ( pslBuffer32 === undefined ) { return EMPTY_STRING; }
const hostnameLen = setHostnameArg(hostname);
const buf8 = pslBuffer8;
if ( hostnameLen === 0 || buf8[0] === 0x2E /* '.' */ ) {
return EMPTY_STRING;
}
const cursorPos = getPublicSuffixPos();
if ( cursorPos === -1 || buf8[cursorPos + 1] === 0 ) {
return EMPTY_STRING;
}
// 7. The registered or registrable domain is the public suffix plus one
// additional label.
const beg = buf8[cursorPos + 3];
return beg === 0 ? hostnameArg : hostnameArg.slice(beg);
};
/******************************************************************************/
const toSelfie = function(encoder) {
if ( pslBuffer8 === undefined ) { return ''; }
if ( encoder instanceof Object ) {
const bufferStr = encoder.encode(pslBuffer8.buffer, pslByteLength);
return `${SELFIE_MAGIC}\t${bufferStr}`;
}
return {
magic: SELFIE_MAGIC,
buf32: Array.from(
new Uint32Array(pslBuffer8.buffer, 0, pslByteLength >>> 2)
),
};
};
const fromSelfie = function(selfie, decoder) {
let byteLength = 0;
if (
typeof selfie === 'string' &&
selfie.length !== 0 &&
decoder instanceof Object
) {
const pos = selfie.indexOf('\t');
if ( pos === -1 || selfie.slice(0, pos) !== `${SELFIE_MAGIC}` ) {
return false;
}
const bufferStr = selfie.slice(pos + 1);
byteLength = decoder.decodeSize(bufferStr);
if ( byteLength === 0 ) { return false; }
allocateBuffers(byteLength);
decoder.decode(bufferStr, pslBuffer8.buffer);
} else if (
selfie instanceof Object &&
selfie.magic === SELFIE_MAGIC &&
Array.isArray(selfie.buf32)
) {
byteLength = selfie.buf32.length << 2;
allocateBuffers(byteLength);
pslBuffer32.set(selfie.buf32);
} else {
return false;
}
// Important!
hostnameArg = '';
pslBuffer8[LABEL_INDICES_SLOT] = 0;
fireChangedEvent();
return true;
};
/******************************************************************************/
// The WASM module is entirely optional, the JS implementation will be
// used should the WASM module be unavailable for whatever reason.
const enableWASM = (function() {
// The directory from which the current script was fetched should also
// contain the related WASM file. The script is fetched from a trusted
// location, and consequently so will be the related WASM file.
let workingDir;
{
const url = new URL(document.currentScript.src);
const match = /[^\/]+$/.exec(url.pathname);
if ( match !== null ) {
url.pathname = url.pathname.slice(0, match.index);
}
workingDir = url.href;
}
let memory;
return function() {
if ( getPublicSuffixPosWASM instanceof Function ) {
return Promise.resolve(true);
}
if (
typeof WebAssembly !== 'object' ||
typeof WebAssembly.instantiateStreaming !== 'function'
) {
return Promise.resolve(false);
}
// The wasm code will work only if CPU is natively little-endian,
// as we use native uint32 array in our js code.
const uint32s = new Uint32Array(1);
const uint8s = new Uint8Array(uint32s.buffer);
uint32s[0] = 1;
if ( uint8s[0] !== 1 ) {
return Promise.resolve(false);
}
return fetch(
workingDir + 'wasm/publicsuffixlist.wasm',
{ mode: 'same-origin' }
).then(response => {
const pageCount = pslBuffer8 !== undefined
? pslBuffer8.byteLength + 0xFFFF >>> 16
: 1;
memory = new WebAssembly.Memory({ initial: pageCount });
return WebAssembly.instantiateStreaming(
response,
{ imports: { memory: memory } }
);
}).then(({ instance }) => {
const curPageCount = memory.buffer.byteLength >>> 16;
const newPageCount = pslBuffer8 !== undefined
? pslBuffer8.byteLength + 0xFFFF >>> 16
: 0;
if ( newPageCount > curPageCount ) {
memory.grow(newPageCount - curPageCount);
}
if ( pslBuffer32 !== undefined ) {
const buf8 = new Uint8Array(memory.buffer);
const buf32 = new Uint32Array(memory.buffer);
buf32.set(pslBuffer32);
pslBuffer8 = buf8;
pslBuffer32 = buf32;
}
wasmMemory = memory;
getPublicSuffixPosWASM = instance.exports.getPublicSuffixPos;
getPublicSuffixPos = getPublicSuffixPosWASM;
memory = undefined;
return true;
}).catch(reason => {
console.info(reason);
return false;
});
};
})();
const disableWASM = function() {
if ( getPublicSuffixPosWASM instanceof Function ) {
getPublicSuffixPos = getPublicSuffixPosJS;
getPublicSuffixPosWASM = undefined;
}
if ( wasmMemory === undefined ) { return; }
if ( pslBuffer32 !== undefined ) {
const buf8 = new Uint8Array(pslByteLength);
const buf32 = new Uint32Array(buf8.buffer);
buf32.set(pslBuffer32);
pslBuffer8 = buf8;
pslBuffer32 = buf32;
}
wasmMemory = undefined;
};
/******************************************************************************/
context = context || window;
context.publicSuffixList = {
version: '2.0',
parse,
getDomain,
getPublicSuffix,
toSelfie, fromSelfie,
disableWASM, enableWASM,
};
if ( typeof module !== 'undefined' ) {
module.exports = context.publicSuffixList;
} else if ( typeof exports !== 'undefined' ) {
exports = context.publicSuffixList;
}
/******************************************************************************/
// <<<<<<<< end of anonymous namespace
})(this);

29
src/lib/publicsuffixlist/wasm/README.md

@ -0,0 +1,29 @@
### For code reviewers
All `wasm` files in that directory where created by compiling the
corresponding `wat` file using the command (using
`publicsuffixlist.wat`/`publicsuffixlist.wasm` as example):
wat2wasm publicsuffixlist.wat -o publicsuffixlist.wasm
Assuming:
- The command is executed from within the present directory.
### `wat2wasm` tool
The `wat2wasm` tool can be downloaded from an official WebAssembly project:
<https://github.com/WebAssembly/wabt/releases>.
### `wat2wasm` tool online
You can also use the following online `wat2wasm` tool:
<https://webassembly.github.io/wabt/demo/wat2wasm/>.
Just paste the whole content of the `wat` file to compile into the WAT pane.
Click "Download" button to retrieve the resulting `wasm` file.
### See also
For the curious, the following online tool allows you to find out the machine
code as a result from the WASM code: https://mbebenita.github.io/WasmExplorer/

BIN
src/lib/publicsuffixlist/wasm/publicsuffixlist.wasm

317
src/lib/publicsuffixlist/wasm/publicsuffixlist.wat

@ -0,0 +1,317 @@
;;
;; uBlock Origin - a browser extension to block requests.
;; Copyright (C) 2019-present Raymond Hill
;;
;; License: pick the one which suits you:
;; GPL v3 see <https://www.gnu.org/licenses/gpl.html>
;; APL v2 see <http://www.apache.org/licenses/LICENSE-2.0>
;;
;; Home: https://github.com/gorhill/publicsuffixlist.js
;; File: publicsuffixlist.wat
;;
;; Description: WebAssembly implementation for core lookup method in
;; publicsuffixlist.js
;;
;; How to compile:
;;
;; wat2wasm publicsuffixlist.wat -o publicsuffixlist.wasm
;;
;; The `wat2wasm` tool can be downloaded from an official WebAssembly
;; project:
;; https://github.com/WebAssembly/wabt/releases
(module
;;
;; module start
;;
(memory (import "imports" "memory") 1)
;;
;; Tree encoding in array buffer:
;;
;; Node:
;; + u8: length of char data
;; + u8: flags => bit 0: is_publicsuffix, bit 1: is_exception
;; + u16: length of array of children
;; + u32: char data or offset to char data
;; + u32: offset to array of children
;; = 12 bytes
;;
;; // i32 / i8
;; const HOSTNAME_SLOT = 0; // jshint ignore:line
;; const LABEL_INDICES_SLOT = 256; // -- / 256
;; const RULES_PTR_SLOT = 100; // 100 / 400
;; const CHARDATA_PTR_SLOT = 101; // 101 / 404
;; const EMPTY_STRING = '';
;; const SELFIE_MAGIC = 2;
;;
;;
;; Public functions
;;
;;
;; unsigned int getPublicSuffixPos()
;;
;; Returns an offset to the start of the public suffix.
;;
(func (export "getPublicSuffixPos")
(result i32) ;; result = match index, -1 = miss
(local $iCharData i32) ;; offset to start of character data
(local $iNode i32) ;; offset to current node
(local $iLabel i32) ;; offset to label indices
(local $cursorPos i32) ;; position of cursor within hostname argument
(local $labelBeg i32)
(local $labelLen i32)
(local $nCandidates i32)
(local $iCandidates i32)
(local $iFound i32)
(local $l i32)
(local $r i32)
(local $d i32)
(local $iCandidate i32)
(local $iCandidateNode i32)
(local $candidateLen i32)
(local $iCandidateChar i32)
(local $_1 i32)
(local $_2 i32)
(local $_3 i32)
;;
;; const iCharData = buf32[CHARDATA_PTR_SLOT];
i32.const 404
i32.load
set_local $iCharData
;; let iNode = pslBuffer32[RULES_PTR_SLOT];
i32.const 400
i32.load
i32.const 2
i32.shl
set_local $iNode
;; let iLabel = LABEL_INDICES_SLOT;
i32.const 256
set_local $iLabel
;; let cursorPos = -1;
i32.const -1
set_local $cursorPos
;; label-lookup loop
;; for (;;) {
block $labelLookupDone loop $labelLookup
;; // Extract label indices
;; const labelBeg = buf8[iLabel+1];
;; const labelLen = buf8[iLabel+0] - labelBeg;
get_local $iLabel
i32.load8_u
get_local $iLabel
i32.load8_u offset=1
tee_local $labelBeg
i32.sub
set_local $labelLen
;; // Match-lookup loop: binary search
;; let r = buf32[iNode+0] >>> 16;
;; if ( r === 0 ) { break; }
get_local $iNode
i32.load16_u offset=2
tee_local $r
i32.eqz
br_if $labelLookupDone
;; const iCandidates = buf32[iNode+2];
get_local $iNode
i32.load offset=8
i32.const 2
i32.shl
set_local $iCandidates
;; let l = 0;
;; let iFound = 0;
i32.const 0
tee_local $l
set_local $iFound
;; while ( l < r ) {
block $binarySearchDone loop $binarySearch
get_local $l
get_local $r
i32.ge_u
br_if $binarySearchDone
;; const iCandidate = l + r >>> 1;
get_local $l
get_local $r
i32.add
i32.const 1
i32.shr_u
tee_local $iCandidate
;; const iCandidateNode = iCandidates + iCandidate + (iCandidate << 1);
i32.const 2
i32.shl
tee_local $_1
get_local $_1
i32.const 1
i32.shl
i32.add
get_local $iCandidates
i32.add
tee_local $iCandidateNode
;; const candidateLen = buf32[iCandidateNode+0] & 0x000000FF;
i32.load8_u
set_local $candidateLen
;; let d = labelLen - candidateLen;
get_local $labelLen
get_local $candidateLen
i32.sub
tee_local $d
;; if ( d === 0 ) {
i32.eqz
if
;; const iCandidateChar = candidateLen <= 4
get_local $candidateLen
i32.const 4
i32.le_u
if
;; ? iCandidateNode + 1 << 2
get_local $iCandidateNode
i32.const 4
i32.add
set_local $iCandidateChar
else
;; : buf32[CHARDATA_PTR_SLOT] + buf32[iCandidateNode+1];
get_local $iCharData
get_local $iCandidateNode
i32.load offset=4
i32.add
set_local $iCandidateChar
end
;; for ( let i = 0; i < labelLen; i++ ) {
get_local $labelBeg
tee_local $_1
get_local $labelLen
i32.add
set_local $_3
get_local $iCandidateChar
set_local $_2
block $findDiffDone loop $findDiff
;; d = buf8[labelBeg+i] - buf8[iCandidateChar+i];
;; if ( d !== 0 ) { break; }
get_local $_1
i32.load8_u
get_local $_2
i32.load8_u
i32.sub
tee_local $d
br_if $findDiffDone
get_local $_1
i32.const 1
i32.add
tee_local $_1
get_local $_3
i32.eq
br_if $findDiffDone
get_local $_2
i32.const 1
i32.add
set_local $_2
br $findDiff
;; }
end end
;; }
end
;; if ( d < 0 ) {
;; r = iCandidate;
get_local $d
i32.const 0
i32.lt_s
if
get_local $iCandidate
set_local $r
br $binarySearch
end
;; } else if ( d > 0 ) {
;; l = iCandidate + 1;
get_local $d
i32.const 0
i32.gt_s
if
get_local $iCandidate
i32.const 1
i32.add
set_local $l
br $binarySearch
end
;; } else /* if ( d === 0 ) */ {
;; iFound = iCandidateNode;
;; break;
;; }
get_local $iCandidateNode
set_local $iFound
end end
;; }
;; // 2. If no rules match, the prevailing rule is "*".
;; if ( iFound === 0 ) {
;; if ( buf8[iCandidates + 1 << 2] !== 0x2A /* '*' */ ) { break; }
;; iFound = iCandidates;
;; }
get_local $iFound
i32.eqz
if
get_local $iCandidates
i32.load8_u offset=4
i32.const 0x2A
i32.ne
br_if $labelLookupDone
get_local $iCandidates
set_local $iFound
end
;; iNode = iFound;
get_local $iFound
tee_local $iNode
;; // 5. If the prevailing rule is a exception rule, modify it by
;; // removing the leftmost label.
;; if ( (buf32[iNode+0] & 0x00000200) !== 0 ) {
;; if ( iLabel > LABEL_INDICES_SLOT ) {
;; return iLabel - 2;
;; }
;; break;
;; }
i32.load8_u offset=1
tee_local $_1
i32.const 0x02
i32.and
if
get_local $iLabel
i32.const 256
i32.gt_u
if
get_local $iLabel
i32.const -2
i32.add
return
end
br $labelLookupDone
end
;; if ( (buf32[iNode+0] & 0x00000100) !== 0 ) {
;; cursorPos = labelBeg;
;; }
get_local $_1
i32.const 0x01
i32.and
if
get_local $iLabel
set_local $cursorPos
end
;; if ( labelBeg === 0 ) { break; }
get_local $labelBeg
i32.eqz
br_if $labelLookupDone
;; iLabel += 2;
get_local $iLabel
i32.const 2
i32.add
set_local $iLabel
br $labelLookup
end end
get_local $cursorPos
)
;;
;; module end
;;
)

171
src/logger-ui.html

@ -2,83 +2,148 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/common.css" type="text/css">
<meta name="viewport" content="width=560, initial-scale=1">
<link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" href="css/fa-icons.css" type="text/css">
<link rel="stylesheet" href="css/scope-selector.css" type="text/css">
<link rel="stylesheet" href="css/logger-ui.css" type="text/css">
<link rel="shortcut icon" href="img/icon_16.png" type="image/png">
<link rel="stylesheet" type="text/css" href="css/logger-ui.css">
<link rel="shortcut icon" type="image/png" href="img/icon_64.png">
<title data-i18n="loggerPageName"></title>
<style id="vwRendererRuntimeStyles"></style>
</head>
<body class="compactView f">
<body>
<div id="toolbar">
<div class="permatoolbar">
<div>
<select id="pageSelector">
<option value="" data-i18n="statsPageDetailedAllPages">
<option value="-1" data-i18n="statsPageDetailedBehindTheScenePage">
</select>
<span id="reloadTab" class="fa-icon disabled">refresh</span>
<span id="popupPanelButton" class="fa-icon disabled">th</span>
<option value="0" data-i18n="logAll">
<option value="-1" data-i18n="logBehindTheScene">
<option value="_" data-i18n="loggerCurrentTab">
</select>
<span id="refresh" class="button fa-icon disabled needtab" data-i18n-title="loggerReloadTip">refresh</span>
<span id="showpopup" class="button px-icon disabled needtab" data-i18n-title="loggerPopupPanelTip"><img src="/img/icon_64.png"></span>
</div>
<div>
<span id="compactViewToggler" class="fa-icon">double-angle-up</span>
<span id="clean" class="fa-icon disabled">times</span>
<span id="clear" class="fa-icon disabled">eraser</span>
<span id="filterButton" class="fa-icon">filter</span>
<input id="filterInput" type="text" placeholder="loggerFilterInputPlaceholder">
<input id="maxEntries" type="text" size="5" title="loggerMaxEntriesTip">
<a id="info" class="button fa-icon" href="https://github.com/gorhill/uBlock/wiki/The-logger" target="_blank" data-i18n-title="loggerInfoTip">info-circle</a>
</div>
</div>
<div id="popupPanelContainer"></div>
<div id="inspectors">
<div id="netInspector" class="inspector f">
<div class="permatoolbar">
<div>
<span class="button fa-icon vCompactToggler">double-angle-up</span>
<span id="clean" class="button fa-icon disabled">times</span>
<span id="clear" class="button fa-icon disabled" data-i18n-title="loggerClearTip">eraser</span>
<span id="pause"><span class="button fa-icon" data-i18n-title="loggerPauseTip">pause-circle-o</span><span class="button fa-icon" data-i18n-title="loggerUnpauseTip">play-circle-o</span></span>
<span id="filterExprGroup">
<span id="filterButton" class="button fa-icon" data-i18n-title="loggerRowFiltererButtonTip">filter</span>
<span id="filterInput">
<input type="search" placeholder="logFilterPrompt">
<span id="filterExprButton" class="button fa-icon expanded" data-i18n-title="loggerRowFiltererBuiltinTip">angle-up</span>
<div id="filterExprPicker">
<div><span data-filtex="!" data-i18n="loggerRowFiltererBuiltinNot"></span><span data-filtex="\t(?:--|\+\+)\t" data-i18n="loggerRowFiltererBuiltinBlocked"></span><span data-filtex="\tinfo\t" data-i18n="loggerRowFiltererBuiltinInfo"></span></div>
<div><span data-filtex="!" data-i18n="loggerRowFiltererBuiltinNot"></span>
<span style="flex-direction: column;">
<div style="margin-bottom: 1px;"><span data-filtex="\tcookie\t">cookie</span><span data-filtex="\t(?:css|(?:inline-)?font)\t">css/font</span><span data-filtex="\timage\t">image</span><span data-filtex="\tmedia\t">media</span><span data-filtex="\t(?:inline-)?script(?:ing)?\t">script</span></div>
<div><span data-filtex="\t(?:fetch|websocket|xhr)\t">fetch</span><span data-filtex="\tframe\t">frame</span><span data-filtex="\t(?:beacon|csp_report|ping|other)\t">other</span></div>
</span>
</div>
<div><span data-filtex="!" data-i18n="loggerRowFiltererBuiltinNot"></span><span data-filtex="\t(?:0,)?1\t" data-i18n="loggerRowFiltererBuiltin1p"></span><span data-filtex="\t(?:3(?:,\d)?|0,3)\t" data-i18n="loggerRowFiltererBuiltin3p"></span></div>
<div id="filterExprCnameOf" style="display:none"><span data-filtex="!" data-i18n="loggerRowFiltererBuiltinNot"></span><span data-filtex="\taliasURL=.">CNAME</span></div>
</div>
</span>
</span>
</div>
<div>
<span id="loggerStats" class="button fa-icon" style="display: none;">bar-chart</span>
<span id="loggerExport" class="button fa-icon">clipboard</span>
<span id="loggerSettings" class="button fa-icon">cog</span>
</div>
</div>
<div class="vscrollable">
<div id="vwRenderer">
<div id="vwScroller">
<div id="vwVirtualContent">
<div id="vwContent"></div>
</div>
</div>
<div id="vwLineSizer">
<div class="logEntry oneLine"><div><span>00:00:00</span><span>%35%</span><span>+++</span><span>%65%</span><span>1234567890</span><span>+++</span></div></div>
</div>
</div>
</div>
</div>
<iframe id="popupContainer"></iframe>
</div>
<div id="content">
<style id="tabFilterer"></style>
<table>
<colgroup><col><col><col><col><col><col></colgroup>
<tbody></tbody>
</table>
<div id="modalOverlay">
<div>
<div id="modalOverlayContainer"></div>
<div id="modalOverlayClose"><svg viewBox="0 0 64 64"><path d="M 16 16 L 48 48 M 16 48 L 48 16" /></svg></div>
</div>
</div>
<div style="display: none;">
<div id="emphasizeTemplate"><span><span></span><b></b><span></span></span></div>
<div id="hiddenTemplate"><span style="display:none;"></span></div>
<div id="ruleEditor" class="modalDialog">
<div class="dialog">
<section class="scopeWidget">
<span class="scope" id="specificScope"><span>&nbsp;</span></span><!--
--><span class="scope tip-anchor-right" id="globalScope" data-scope="*" data-i18n-tip="matrixGlobalScopeTip"><span><span>&#x2217;</span></span></span>
<div class="ruleEditorToolbar">
<span id="matrixReloadButton" class="fa-icon tip-anchor-right" data-i18n-tip="matrixReloadButton">refresh</span>
</div>
</section>
<section>
<div class="ruleWidgets"></div>
<div class="ruleEditorToolbar">
<span id="matrixRevertButton" class="fa-icon scopeRel tip-anchor-right" data-i18n-tip="matrixRevertButtonTip">reply</span>
<span id="matrixPersistButton" class="fa-icon fa-icon-badged scopeRel tip-anchor-right" data-i18n-tip="matrixPersistButtonTip">lock</span>
</section>
</section>
<div id="templates" style="display: none;">
<div id="logEntryTemplate"><div><span></span>&#8203;<span>--</span>&#8203;<span></span>&#8203;<span></span>&#8203;<span></span>&#8203;<span></span></div></div>
<div id="netFilteringDialog" data-pane="rule">
<div class="headers">
&ensp;
<span class="header" data-pane="rule" data-i18n="loggerEntryRuleHeader"></span>
<span class="header" data-pane="details" data-i18n="loggerEntryDetailsHeader"></span>
</div>
<div class="panes">
<div class="pane" data-pane="rule">
<iframe></iframe>
</div>
<div class="pane" data-pane="details">
<div><span data-i18n="loggerEntryDetailsContext"></span><span></span></div>
<div><span data-i18n="loggerEntryDetailsPartyness"></span><span class="prose"></span></div>
<div><span data-i18n="loggerEntryDetailsType"></span><span></span></div>
<div><span data-i18n="loggerEntryDetailsURL"></span><span></span></div>
<div><span>CNAME</span><span></span></div>
<div><span>Original URL</span><span></span></div>
</div>
</div>
</div>
<div id="loggerExportDialog">
<div class="options">
<div data-radio="format">
<span data-i18n="loggerExportFormatList" data-radio-item="list"></span>
<span data-i18n="loggerExportFormatTable" data-radio-item="table"></span>
</div>
<div data-radio="encoding">
<span data-i18n="loggerExportEncodePlain" data-radio-item="plain"></span>
<span data-i18n="loggerExportEncodeMarkdown" data-radio-item="markdown"></span>
</div>
<div>
<span data-i18n="genericCopyToClipboard" class="pushbutton"></span>
</div>
</div>
<textarea class="output" readonly spellcheck="false"></textarea>
</div>
<div id="ruleRowTemplate" style="display: none;">
<div class="ruleRow"><!--
--><span class="ruleCell" data-type="*">&nbsp;</span><!--
--><span class="ruleCell" data-type>&nbsp;</span><!--
--></div>
<div id="loggerSettingsDialog">
<div><span data-i18n="loggerSettingDiscardPrompt"></span>
<ul>
<li><label data-i18n="loggerSettingPerEntryMaxAge"><input type="number" min="0" max="50000" /></label>
<li><label data-i18n="loggerSettingPerTabMaxLoads"><input type="number" min="0" max="1000000" /></label>
<li><label data-i18n="loggerSettingPerTabMaxEntries"><input type="number" min="0" max="1000000" /></label>
</ul>
</div>
<div><label data-i18n="loggerSettingPerEntryLineCount"><input type="number" min="2" max="6"></label></div>
</div>
<div id="ruleActionPicker"><div class="allowRule"></div><div class="blockRule"></div></div>
</div>
<script src="js/fa-icons.js"></script>
<script src="lib/punycode.js"></script>
<script src="lib/publicsuffixlist.js"></script>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/scope-selector.js"></script>
<script src="js/logger-ui.js"></script>
</body>

1
src/main-blocked.html

@ -34,6 +34,7 @@
</div>
<script src="js/fa-icons.js"></script>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>

20
src/popup.html

@ -14,7 +14,7 @@
<body>
<div class="paneHead">
<a id="gotoDashboard" class="extensionURL" href="#" data-extension-url="dashboard.html" data-i18n-tip="matrixDashboardMenuEntry">uMatrix <span id="version"> </span></a>
<div id="gotoDashboard" data-extension-url="dashboard.html" data-i18n-tip="matrixDashboardMenuEntry">uMatrix <span id="version"> </span></div>
<div id="toolbarContainer">
<div class="toolbar">
<span class="scope" id="specificScope"><span>&nbsp;</span></span><!--
@ -25,16 +25,16 @@
<span id="buttonPersist" class="fa-icon fa-icon-badged scopeRel tip-anchor-center" data-i18n-tip="matrixPersistButtonTip">lock</span>
<span id="buttonRevertScope" class="fa-icon fa-icon-badged scopeRel tip-anchor-center" data-i18n-tip="matrixRevertButtonTip">reply</span>
</div>
<div class="toolbar">
<div class="toolbar needtab">
<span id="buttonReload" class="fa-icon tip-anchor-right" data-i18n-tip="matrixReloadButton">refresh</span>
</div>
<div class="toolbar">
<span id="buttonRevertAll" class="fa-icon tip-anchor-right" data-i18n-tip="matrixRevertAllEntry">reply-all</span>
<span class="fa-icon extensionURL tip-anchor-right" data-extension-url="logger-ui.html" data-i18n-tip="matrixLoggerMenuEntry">list-alt</span>
<span id="buttonRevertAll" class="fa-icon tip-anchor-right needtab" data-i18n-tip="matrixRevertAllEntry">reply-all</span>
<span class="fa-icon tip-anchor-right" data-extension-url="logger-ui.html#_" data-i18n-tip="matrixLoggerMenuEntry">list-alt</span>
</div>
</div>
<div id="matHead" class="matrix collapsible">
<div class="matRow rw" style="display:none"><div class="matCell" data-req-type="all">all</div><div class="matCell" data-req-type="cookie">cookie</div><div class="matCell" data-req-type="css">css</div><div class="matCell" data-req-type="image">img</div><div class="matCell" data-req-type="media">media</div><div class="matCell" data-req-type="script">script</div><div class="matCell" data-req-type="xhr">XHR</div><div class="matCell" data-req-type="frame">frame</div><div class="matCell" data-req-type="other">other</div></div>
<div class="matRow rw" style="display:none"><div class="matCell" data-req-type="all">all</div><div class="matCell" data-req-type="cookie">cookie</div><div class="matCell" data-req-type="css">css</div><div class="matCell" data-req-type="image">img</div><div class="matCell" data-req-type="media">media</div><div class="matCell" data-req-type="script">script</div><div class="matCell" data-req-type="fetch">fetch</div><div class="matCell" data-req-type="frame">frame</div><div class="matCell" data-req-type="other">other</div></div>
</div>
</div>
@ -68,10 +68,11 @@
<div id="dropDownMenuSwitches" class="dropdown-menu-capture">
<div class="dropdown-menu">
<ul id="mtxSwitches">
<li id="mtxSwitch_https-strict" class="dropdown-menu-entry exists"><!-- <svg><use xlink:href="#toggleButton" /></svg> --><svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 152 96"><g style="fill:#bbb;"><ellipse cx="48" cy="48" rx="24" ry="24" /><ellipse cx="104" cy="48" rx="24" ry="24" /><rect width="56" height="48" x="48" y="24" /></g><g class="off" style="fill:#bbb;"><ellipse cx="48" cy="48" rx="48" ry="48" /><ellipse style="fill:#fff;" cx="48" cy="48" rx="40" ry="40" /><ellipse class="dot" cx="48" cy="48" rx="12" ry="12" /></g><g class="on" style="fill:#bbb;"><ellipse style="fill:#444;" cx="104" cy="48" rx="48" ry="48" /><ellipse class="dot" cx="104" cy="48" rx="12" ry="12" /></g></svg><span data-i18n="matrixSwitchNoMixedContent"></span>&emsp;<a class="fa-icon" href="https://developer.mozilla.org/docs/Web/Security/Mixed_content" target="_blank">info-circle</a>
<li id="mtxSwitch_no-workers" class="dropdown-menu-entry exists"><!-- <svg><use xlink:href="#toggleButton" /></svg> --><svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 152 96"><g style="fill:#bbb;"><ellipse cx="48" cy="48" rx="24" ry="24" /><ellipse cx="104" cy="48" rx="24" ry="24" /><rect width="56" height="48" x="48" y="24" /></g><g class="off" style="fill:#bbb;"><ellipse cx="48" cy="48" rx="48" ry="48" /><ellipse style="fill:#fff;" cx="48" cy="48" rx="40" ry="40" /><ellipse class="dot" cx="48" cy="48" rx="12" ry="12" /></g><g class="on" style="fill:#bbb;"><ellipse style="fill:#444;" cx="104" cy="48" rx="48" ry="48" /><ellipse class="dot" cx="104" cy="48" rx="12" ry="12" /></g></svg><span data-i18n="matrixSwitchNoWorker"></span>&emsp;<a class="fa-icon" href="https://developer.mozilla.org/docs/Web/API/Web_Workers_API" target="_blank">info-circle</a>
<li id="mtxSwitch_referrer-spoof" class="dropdown-menu-entry"><!-- <svg><use xlink:href="#toggleButton" /></svg> --><svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 152 96"><g style="fill:#bbb;"><ellipse cx="48" cy="48" rx="24" ry="24" /><ellipse cx="104" cy="48" rx="24" ry="24" /><rect width="56" height="48" x="48" y="24" /></g><g class="off" style="fill:#bbb;"><ellipse cx="48" cy="48" rx="48" ry="48" /><ellipse style="fill:#fff;" cx="48" cy="48" rx="40" ry="40" /><ellipse class="dot" cx="48" cy="48" rx="12" ry="12" /></g><g class="on" style="fill:#bbb;"><ellipse style="fill:#444;" cx="104" cy="48" rx="48" ry="48" /><ellipse class="dot" cx="104" cy="48" rx="12" ry="12" /></g></svg><span data-i18n="matrixSwitchReferrerSpoof"></span>&emsp;<a class="fa-icon" href="https://developer.mozilla.org/docs/Web/HTTP/Headers/Referer" target="_blank">info-circle</a>
<li id="mtxSwitch_noscript-spoof" class="dropdown-menu-entry"><!-- <svg><use xlink:href="#toggleButton" /></svg> --><svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 152 96"><g style="fill:#bbb;"><ellipse cx="48" cy="48" rx="24" ry="24" /><ellipse cx="104" cy="48" rx="24" ry="24" /><rect width="56" height="48" x="48" y="24" /></g><g class="off" style="fill:#bbb;"><ellipse cx="48" cy="48" rx="48" ry="48" /><ellipse style="fill:#fff;" cx="48" cy="48" rx="40" ry="40" /><ellipse class="dot" cx="48" cy="48" rx="12" ry="12" /></g><g class="on" style="fill:#bbb;"><ellipse style="fill:#444;" cx="104" cy="48" rx="48" ry="48" /><ellipse class="dot" cx="104" cy="48" rx="12" ry="12" /></g></svg><span data-i18n="matrixSwitchNoscriptSpoof"></span>&emsp;<a class="fa-icon" href="https://developer.mozilla.org/docs/Web/HTML/Element/noscript" target="_blank">info-circle</a>
<li id="mtxSwitch_https-strict" class="dropdown-menu-entry exists"><!-- <svg><use xlink:href="#toggleButton" /></svg> --><svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 152 96"><g><ellipse cx="48" cy="48" rx="24" ry="24" /><ellipse cx="104" cy="48" rx="24" ry="24" /><rect width="56" height="48" x="48" y="24" /></g><g class="off"><ellipse cx="48" cy="48" rx="48" ry="48" /><ellipse style="fill:#fff;" cx="48" cy="48" rx="40" ry="40" /><ellipse class="dot" cx="48" cy="48" rx="12" ry="12" /></g><g class="on"><ellipse style="fill:#444;" cx="104" cy="48" rx="48" ry="48" /><ellipse class="dot" cx="104" cy="48" rx="12" ry="12" /></g></svg><span data-i18n="matrixSwitchNoMixedContent"></span>&emsp;<a class="fa-icon" href="https://developer.mozilla.org/docs/Web/Security/Mixed_content" target="_blank">info-circle</a>
<li id="mtxSwitch_no-workers" class="dropdown-menu-entry exists"><!-- <svg><use xlink:href="#toggleButton" /></svg> --><svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 152 96"><g><ellipse cx="48" cy="48" rx="24" ry="24" /><ellipse cx="104" cy="48" rx="24" ry="24" /><rect width="56" height="48" x="48" y="24" /></g><g class="off"><ellipse cx="48" cy="48" rx="48" ry="48" /><ellipse style="fill:#fff;" cx="48" cy="48" rx="40" ry="40" /><ellipse class="dot" cx="48" cy="48" rx="12" ry="12" /></g><g class="on"><ellipse style="fill:#444;" cx="104" cy="48" rx="48" ry="48" /><ellipse class="dot" cx="104" cy="48" rx="12" ry="12" /></g></svg><span data-i18n="matrixSwitchNoWorker"></span>&emsp;<a class="fa-icon" href="https://developer.mozilla.org/docs/Web/API/Web_Workers_API" target="_blank">info-circle</a>
<li id="mtxSwitch_referrer-spoof" class="dropdown-menu-entry"><!-- <svg><use xlink:href="#toggleButton" /></svg> --><svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 152 96"><g><ellipse cx="48" cy="48" rx="24" ry="24" /><ellipse cx="104" cy="48" rx="24" ry="24" /><rect width="56" height="48" x="48" y="24" /></g><g class="off"><ellipse cx="48" cy="48" rx="48" ry="48" /><ellipse style="fill:#fff;" cx="48" cy="48" rx="40" ry="40" /><ellipse class="dot" cx="48" cy="48" rx="12" ry="12" /></g><g class="on"><ellipse style="fill:#444;" cx="104" cy="48" rx="48" ry="48" /><ellipse class="dot" cx="104" cy="48" rx="12" ry="12" /></g></svg><span data-i18n="matrixSwitchReferrerSpoof"></span>&emsp;<a class="fa-icon" href="https://developer.mozilla.org/docs/Web/HTTP/Headers/Referer" target="_blank">info-circle</a>
<li id="mtxSwitch_noscript-spoof" class="dropdown-menu-entry"><!-- <svg><use xlink:href="#toggleButton" /></svg> --><svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 152 96"><g><ellipse cx="48" cy="48" rx="24" ry="24" /><ellipse cx="104" cy="48" rx="24" ry="24" /><rect width="56" height="48" x="48" y="24" /></g><g class="off"><ellipse cx="48" cy="48" rx="48" ry="48" /><ellipse style="fill:#fff;" cx="48" cy="48" rx="40" ry="40" /><ellipse class="dot" cx="48" cy="48" rx="12" ry="12" /></g><g class="on"><ellipse style="fill:#444;" cx="104" cy="48" rx="48" ry="48" /><ellipse class="dot" cx="104" cy="48" rx="12" ry="12" /></g></svg><span data-i18n="matrixSwitchNoscriptSpoof"></span>&emsp;<a class="fa-icon" href="https://developer.mozilla.org/docs/Web/HTML/Element/noscript" target="_blank">info-circle</a>
<li id="mtxSwitch_cname-reveal" class="dropdown-menu-entry"><!-- <svg><use xlink:href="#toggleButton" /></svg> --><svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 152 96"><g><ellipse cx="48" cy="48" rx="24" ry="24" /><ellipse cx="104" cy="48" rx="24" ry="24" /><rect width="56" height="48" x="48" y="24" /></g><g class="off"><ellipse cx="48" cy="48" rx="48" ry="48" /><ellipse style="fill:#fff;" cx="48" cy="48" rx="40" ry="40" /><ellipse class="dot" cx="48" cy="48" rx="12" ry="12" /></g><g class="on"><ellipse style="fill:#444;" cx="104" cy="48" rx="48" ry="48" /><ellipse class="dot" cx="104" cy="48" rx="12" ry="12" /></g></svg><span data-i18n="matrixSwitchRevealCname"></span>&emsp;<a class="fa-icon" href="https://en.wikipedia.org/wiki/CNAME_record" target="_blank">info-circle</a>
</ul>
</div>
</div>
@ -118,6 +119,7 @@
<script src="js/fa-icons.js"></script>
<script src="lib/punycode.js"></script>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>

15
src/raw-settings.html

@ -3,10 +3,15 @@
<head>
<meta charset="utf-8">
<title data-i18n="rawSettingsPageName"></title>
<link rel="stylesheet" href="lib/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="css/fa-icons.css">
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/dashboard-common.css">
<link rel="stylesheet" href="css/raw-settings.css">
<link rel="stylesheet" href="css/codemirror.css">
<link rel="shortcut icon" type="image/png" href="img/icon_16.png"/>
</head>
@ -18,11 +23,17 @@
<p><button id="rawSettingsApply" class="custom important" type="button" disabled="true" data-i18n="genericApplyChanges"></button>&ensp;
</p>
<textarea id="rawSettings" dir="auto" spellcheck="false"></textarea>
</div><!-- end of div.body -->
<div id="rawSettings" class="codeMirrorContainer codeMirrorFillVertical"></div>
<script src="lib/codemirror/lib/codemirror.js"></script>
<script src="lib/codemirror/addon/selection/active-line.js"></script>
<script src="js/codemirror/mode/raw-settings.js"></script>
<script src="js/fa-icons.js"></script>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>

1
src/settings.html

@ -115,6 +115,7 @@ ul > li.separator {
</div><!-- end of div.body -->
<script src="js/fa-icons.js"></script>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>

3
src/user-rules.html

@ -40,7 +40,7 @@
<button type="button" id="editSaveButton" data-i18n="userRulesEditSave"></button>
</div>
</div>
<div id="ruleFilter"><span class="fa-icon">filter</span>&ensp;<input type="text" size="32"></div>
<div id="ruleFilter"><span class="fa-icon">filter</span>&ensp;<input type="search" size="32"></div>
</div>
<div class="codeMirrorContainer codeMirrorMergeContainer vfill-available"></div>
</div>
@ -56,6 +56,7 @@
<script src="lib/codemirror/addon/selection/active-line.js"></script>
<script src="js/fa-icons.js"></script>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>

Loading…
Cancel
Save