uBlock/src/js/pagestore.js

702 lines
22 KiB
JavaScript
Raw Normal View History

2014-06-24 00:42:43 +02:00
/*******************************************************************************
µBlock - a Chromium browser extension to block requests.
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
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
*/
2014-10-02 22:45:26 +02:00
/* jshint bitwise: false */
2014-12-17 16:09:08 +01:00
/* global vAPI, µBlock */
2014-06-24 00:42:43 +02:00
/*******************************************************************************
A PageRequestStore object is used to store net requests in two ways:
To record distinct net requests
To create a log of net requests
**/
/******************************************************************************/
/******************************************************************************/
µBlock.PageStore = (function() {
2014-12-17 16:09:08 +01:00
'use strict';
2014-06-24 00:42:43 +02:00
/******************************************************************************/
var µb = µBlock;
2014-09-14 22:20:40 +02:00
/******************************************************************************/
2014-06-24 00:42:43 +02:00
/******************************************************************************/
var LogEntry = function(details, result) {
this.init(details, result);
};
/******************************************************************************/
var logEntryFactory = function(details, result) {
var entry = logEntryJunkyard.pop();
if ( entry ) {
return entry.init(details, result);
}
return new LogEntry(details, result);
};
var logEntryJunkyard = [];
var logEntryJunkyardMax = 100;
/******************************************************************************/
LogEntry.prototype.init = function(details, result) {
this.tstamp = Date.now();
this.url = details.requestURL;
this.domain = details.requestDomain;
this.hostname = details.requestHostname;
this.type = details.requestType;
this.result = result;
return this;
};
/******************************************************************************/
LogEntry.prototype.dispose = function() {
this.url = this.domain = this.hostname = this.type = this.result = '';
if ( logEntryJunkyard.length < logEntryJunkyardMax ) {
logEntryJunkyard.push(this);
}
};
/******************************************************************************/
var LogBuffer = function() {
this.lastReadTime = 0;
this.size = 25;
this.buffer = null;
this.readPtr = 0;
this.writePtr = 0;
};
/******************************************************************************/
var logBufferFactory = function() {
return new LogBuffer();
};
var liveLogBuffers = [];
/******************************************************************************/
LogBuffer.prototype.dispose = function() {
if ( this.buffer === null ) {
return null;
}
var entry;
var i = this.buffer.length;
while ( i-- ) {
entry = this.buffer[i];
if ( entry instanceof LogEntry ) {
entry.dispose();
}
}
this.buffer = null;
return null;
};
/******************************************************************************/
LogBuffer.prototype.start = function() {
if ( this.buffer === null ) {
this.buffer = new Array(this.size);
this.readPtr = 0;
this.writePtr = 0;
liveLogBuffers.push(this);
}
};
/******************************************************************************/
LogBuffer.prototype.stop = function() {
this.dispose();
this.buffer = null;
// The janitor will remove us from the live pool eventually.
};
/******************************************************************************/
LogBuffer.prototype.writeOne = function(details, result) {
if ( this.buffer === null ) {
return;
}
// Reusing log entry = less memory churning
var entry = this.buffer[this.writePtr];
if ( entry instanceof LogEntry === false ) {
this.buffer[this.writePtr] = logEntryFactory(details, result);
} else {
entry.init(details, result);
}
this.writePtr += 1;
if ( this.writePtr === this.size ) {
this.writePtr = 0;
}
// Grow the buffer between 1.5x-2x the current size
if ( this.writePtr === this.readPtr ) {
var toMove = this.buffer.slice(0, this.writePtr);
var minSize = Math.ceil(this.size * 1.5);
2015-01-07 17:36:23 +01:00
this.size += toMove.length;
if ( this.size < minSize ) {
this.buffer = this.buffer.concat(toMove, new Array(minSize - this.size));
this.writePtr = this.size;
} else {
this.buffer = this.buffer.concat(toMove);
this.writePtr = 0;
}
this.size = this.buffer.length;
}
};
/******************************************************************************/
LogBuffer.prototype.readAll = function() {
var out;
if ( this.buffer === null ) {
this.start();
out = [];
} else if ( this.readPtr < this.writePtr ) {
out = this.buffer.slice(this.readPtr, this.writePtr);
} else if ( this.writePtr < this.readPtr ) {
out = this.buffer.slice(this.readPtr).concat(this.buffer.slice(0, this.writePtr));
} else {
out = [];
}
this.readPtr = this.writePtr;
this.lastReadTime = Date.now();
return out;
};
/******************************************************************************/
var logBufferJanitor = function() {
var logBuffer;
var obsolete = Date.now() - logBufferObsoleteAfter;
var i = liveLogBuffers.length;
while ( i-- ) {
logBuffer = liveLogBuffers[i];
if ( logBuffer.lastReadTime < obsolete ) {
logBuffer.stop();
liveLogBuffers.splice(i, 1);
}
}
setTimeout(logBufferJanitor, logBufferJanitorPeriod);
};
// The janitor will look for stale log buffer every 2 minutes.
var logBufferJanitorPeriod = 2 * 60 * 1000;
// After 30 seconds without being read, a buffer will be considered unused, and
// thus removed from memory.
var logBufferObsoleteAfter = 30 * 1000;
setTimeout(logBufferJanitor, logBufferJanitorPeriod);
/******************************************************************************/
/******************************************************************************/
2014-09-14 22:20:40 +02:00
// To mitigate memory churning
var netFilteringResultCacheEntryJunkyard = [];
var netFilteringResultCacheEntryJunkyardMax = 200;
/******************************************************************************/
var NetFilteringResultCacheEntry = function(result, type) {
this.init(result, type);
2014-09-14 22:20:40 +02:00
};
/******************************************************************************/
NetFilteringResultCacheEntry.prototype.init = function(result, type) {
2014-10-02 22:45:26 +02:00
this.result = result;
this.type = type;
2014-09-14 22:20:40 +02:00
this.time = Date.now();
};
/******************************************************************************/
NetFilteringResultCacheEntry.prototype.dispose = function() {
2014-10-02 22:45:26 +02:00
this.result = '';
this.type = '';
2014-09-14 22:20:40 +02:00
if ( netFilteringResultCacheEntryJunkyard.length < netFilteringResultCacheEntryJunkyardMax ) {
netFilteringResultCacheEntryJunkyard.push(this);
}
};
2014-09-14 22:20:40 +02:00
/******************************************************************************/
NetFilteringResultCacheEntry.factory = function(result, type) {
2014-09-14 22:20:40 +02:00
var entry = netFilteringResultCacheEntryJunkyard.pop();
if ( entry === undefined ) {
entry = new NetFilteringResultCacheEntry(result, type);
2014-09-14 22:20:40 +02:00
} else {
entry.init(result, type);
2014-09-14 22:20:40 +02:00
}
return entry;
};
/******************************************************************************/
/******************************************************************************/
// To mitigate memory churning
var uidGenerator = 1;
var netFilteringCacheJunkyard = [];
var netFilteringCacheJunkyardMax = 10;
/******************************************************************************/
var NetFilteringResultCache = function() {
this.init();
};
/******************************************************************************/
NetFilteringResultCache.factory = function() {
var entry = netFilteringCacheJunkyard.pop();
if ( entry === undefined ) {
entry = new NetFilteringResultCache();
} else {
entry.init();
}
return entry;
};
/******************************************************************************/
NetFilteringResultCache.prototype.init = function() {
this.urls = {};
this.count = 0;
this.shelfLife = 60 * 1000;
this.timer = null;
this.boundPruneAsyncCallback = this.pruneAsyncCallback.bind(this);
2014-09-14 22:20:40 +02:00
};
/******************************************************************************/
NetFilteringResultCache.prototype.dispose = function() {
for ( var key in this.urls ) {
if ( this.urls.hasOwnProperty(key) === false ) {
continue;
}
2014-09-14 22:20:40 +02:00
this.urls[key].dispose();
}
this.urls = {};
this.count = 0;
if ( this.timer !== null ) {
clearTimeout(this.timer);
this.timer = null;
}
2015-01-06 14:44:19 +01:00
this.boundPruneAsyncCallback = null;
2014-09-14 22:20:40 +02:00
if ( netFilteringCacheJunkyard.length < netFilteringCacheJunkyardMax ) {
netFilteringCacheJunkyard.push(this);
}
return null;
};
/******************************************************************************/
NetFilteringResultCache.prototype.add = function(context, result) {
var url = context.requestURL;
var type = context.requestType;
2014-09-14 22:20:40 +02:00
var entry = this.urls[url];
if ( entry !== undefined ) {
2014-10-02 22:45:26 +02:00
entry.result = result;
entry.type = type;
2014-09-14 22:20:40 +02:00
entry.time = Date.now();
return;
}
this.urls[url] = NetFilteringResultCacheEntry.factory(result, type);
2014-09-14 22:20:40 +02:00
if ( this.count === 0 ) {
this.pruneAsync();
}
this.count += 1;
};
/******************************************************************************/
NetFilteringResultCache.prototype.fetchAll = function() {
return this.urls;
};
/******************************************************************************/
NetFilteringResultCache.prototype.compareEntries = function(a, b) {
return this.urls[b].time - this.urls[a].time;
};
/******************************************************************************/
NetFilteringResultCache.prototype.prune = function() {
var keys = Object.keys(this.urls).sort(this.compareEntries.bind(this));
var obsolete = Date.now() - this.shelfLife;
var key, entry;
var i = keys.length;
while ( i-- ) {
key = keys[i];
entry = this.urls[key];
if ( entry.time > obsolete ) {
break;
}
2014-09-14 22:20:40 +02:00
entry.dispose();
delete this.urls[key];
}
this.count -= keys.length - i - 1;
if ( this.count > 0 ) {
this.pruneAsync();
}
};
2014-10-02 22:45:26 +02:00
// https://www.youtube.com/watch?v=hcVpbsDyOhM
2014-09-14 22:20:40 +02:00
/******************************************************************************/
NetFilteringResultCache.prototype.pruneAsync = function() {
if ( this.timer === null ) {
this.timer = setTimeout(this.boundPruneAsyncCallback, this.shelfLife * 2);
}
};
NetFilteringResultCache.prototype.pruneAsyncCallback = function() {
this.timer = null;
this.prune();
2014-09-14 22:20:40 +02:00
};
/******************************************************************************/
NetFilteringResultCache.prototype.lookup = function(url) {
2014-10-02 22:45:26 +02:00
return this.urls[url];
2014-09-14 22:20:40 +02:00
};
/******************************************************************************/
/******************************************************************************/
// To mitigate memory churning
var frameStoreJunkyard = [];
var frameStoreJunkyardMax = 50;
/******************************************************************************/
var FrameStore = function(rootHostname, frameURL) {
this.init(rootHostname, frameURL);
};
/******************************************************************************/
FrameStore.factory = function(rootHostname, frameURL) {
2014-09-14 22:20:40 +02:00
var entry = frameStoreJunkyard.pop();
if ( entry === undefined ) {
entry = new FrameStore(rootHostname, frameURL);
2014-09-14 22:20:40 +02:00
} else {
entry.init(rootHostname, frameURL);
2014-09-14 22:20:40 +02:00
}
return entry;
};
/******************************************************************************/
FrameStore.prototype.init = function(rootHostname, frameURL) {
var µburi = µb.URI;
this.pageHostname = µburi.hostnameFromURI(frameURL);
this.pageDomain = µburi.domainFromHostname(this.pageHostname) || this.pageHostname;
this.rootHostname = rootHostname;
this.rootDomain = µburi.domainFromHostname(rootHostname) || rootHostname;
// This is part of the filtering evaluation context
this.requestURL = this.requestHostname = this.requestType = '';
return this;
};
/******************************************************************************/
FrameStore.prototype.dispose = function() {
this.pageHostname = this.pageDomain =
this.rootHostname = this.rootDomain =
this.requestURL = this.requestHostname = this.requestType = '';
2014-09-14 22:20:40 +02:00
if ( frameStoreJunkyard.length < frameStoreJunkyardMax ) {
frameStoreJunkyard.push(this);
}
return null;
};
2014-09-14 22:20:40 +02:00
/******************************************************************************/
/******************************************************************************/
2014-09-14 22:20:40 +02:00
// To mitigate memory churning
var pageStoreJunkyard = [];
var pageStoreJunkyardMax = 10;
/******************************************************************************/
var PageStore = function(tabId, pageURL) {
2014-06-24 00:42:43 +02:00
this.init(tabId, pageURL);
};
2014-06-24 00:42:43 +02:00
/******************************************************************************/
2014-09-14 22:20:40 +02:00
PageStore.factory = function(tabId, pageURL) {
var entry = pageStoreJunkyard.pop();
if ( entry === undefined ) {
entry = new PageStore(tabId, pageURL);
} else {
entry.init(tabId, pageURL);
}
return entry;
};
/******************************************************************************/
2014-06-24 00:42:43 +02:00
PageStore.prototype.init = function(tabId, pageURL) {
this.tabId = tabId;
2014-07-14 17:24:59 +02:00
this.previousPageURL = '';
this.pageURL = pageURL;
this.pageHostname = µb.URI.hostnameFromURI(pageURL);
2014-08-27 08:01:10 +02:00
// https://github.com/gorhill/uBlock/issues/185
// Use hostname if no domain can be extracted
this.pageDomain = µb.URI.domainFromHostname(this.pageHostname) || this.pageHostname;
this.rootHostname = this.pageHostname;
this.rootDomain = this.pageDomain;
2014-08-27 08:01:10 +02:00
// This is part of the filtering evaluation context
this.requestURL = this.requestHostname = this.requestType = '';
2014-12-30 22:36:29 +01:00
this.hostnameToCountMap = {};
2014-09-14 22:20:40 +02:00
this.frames = {};
2014-08-02 17:40:27 +02:00
this.netFiltering = true;
this.netFilteringReadTime = 0;
2014-07-14 17:24:59 +02:00
this.perLoadBlockedRequestCount = 0;
this.perLoadAllowedRequestCount = 0;
this.skipLocalMirroring = false;
2014-09-14 22:20:40 +02:00
this.netFilteringCache = NetFilteringResultCache.factory();
// Preserve old buffer if there is one already, it may be in use, and
// overwritting it would required another read to restart it.
if ( this.logBuffer instanceof LogBuffer === false ) {
this.logBuffer = logBufferFactory();
2014-09-14 22:20:40 +02:00
}
2014-07-14 17:24:59 +02:00
return this;
};
/******************************************************************************/
2014-10-02 22:45:26 +02:00
PageStore.prototype.reuse = function(pageURL, context) {
// If URL changes without a page reload (more and more common), then we
2014-10-02 22:45:26 +02:00
// need to keep all that we collected for reuse. In particular, not
// doing so was causing a problem in `videos.foxnews.com`: clicking a
// video thumbnail would not work, because the frame hierarchy structure
// was flushed from memory, while not really being flushed on the page.
if ( context === 'tabUpdated' ) {
this.previousPageURL = this.pageURL;
this.pageURL = pageURL;
this.pageHostname = µb.URI.hostnameFromURI(pageURL);
this.pageDomain = µb.URI.domainFromHostname(this.pageHostname) || this.pageHostname;
this.rootHostname = this.pageHostname;
this.rootDomain = this.pageDomain;
2014-12-08 18:37:35 +01:00
// As part of https://github.com/gorhill/uBlock/issues/405
// URL changed, force a re-evaluation of filtering switch
this.netFilteringReadTime = 0;
2014-10-02 22:45:26 +02:00
return this;
}
// A new page is completely reloaded from scratch, reset all.
2014-09-14 22:20:40 +02:00
this.disposeFrameStores();
this.netFilteringCache = this.netFilteringCache.dispose();
2014-08-02 17:40:27 +02:00
var previousPageURL = this.pageURL;
this.init(this.tabId, pageURL);
this.previousPageURL = previousPageURL;
2014-06-24 00:42:43 +02:00
return this;
};
2014-09-14 22:20:40 +02:00
// https://www.youtube.com/watch?v=dltNSbOupgE
2014-06-24 00:42:43 +02:00
/******************************************************************************/
PageStore.prototype.dispose = function() {
// rhill 2013-11-07: Even though at init time these are reset, I still
// need to release the memory taken by these, which can amount to
// sizeable enough chunks (especially requests, through the request URL
// used as a key).
this.pageURL = this.previousPageURL =
this.pageHostname = this.pageDomain =
this.rootHostname = this.rootDomain =
this.requestURL = this.requestHostname = this.requestType = '';
2014-12-30 22:36:29 +01:00
this.hostnameToCountMap = null;
2014-09-14 22:20:40 +02:00
this.disposeFrameStores();
this.netFilteringCache = this.netFilteringCache.dispose();
this.logBuffer = this.logBuffer.dispose();
2014-09-14 22:20:40 +02:00
if ( pageStoreJunkyard.length < pageStoreJunkyardMax ) {
2014-06-24 00:42:43 +02:00
pageStoreJunkyard.push(this);
}
2014-09-14 22:20:40 +02:00
return null;
};
/******************************************************************************/
PageStore.prototype.disposeFrameStores = function() {
var frames = this.frames;
for ( var k in frames ) {
if ( frames.hasOwnProperty(k) ) {
2014-09-14 22:20:40 +02:00
frames[k].dispose();
}
}
this.frames = {};
2014-06-24 00:42:43 +02:00
};
/******************************************************************************/
PageStore.prototype.addFrame = function(frameId, frameURL) {
var frameStore = this.frames[frameId];
if ( frameStore === undefined ) {
this.frames[frameId] = frameStore = FrameStore.factory(this.rootHostname, frameURL);
//console.debug('µBlock> PageStore.addFrame(%d, "%s")', frameId, frameURL);
}
return frameStore;
};
/******************************************************************************/
PageStore.prototype.getFrame = function(frameId) {
return this.frames[frameId];
};
/******************************************************************************/
2014-08-02 17:40:27 +02:00
PageStore.prototype.getNetFilteringSwitch = function() {
if ( this.netFilteringReadTime < µb.netWhitelistModifyTime ) {
2014-12-14 23:21:59 +01:00
this.netFiltering = µb.getNetFilteringSwitch(this.pageURL);
2014-08-02 17:40:27 +02:00
this.netFilteringReadTime = Date.now();
}
return this.netFiltering;
};
/******************************************************************************/
PageStore.prototype.filterRequest = function(context) {
if ( this.getNetFilteringSwitch() === false ) {
this.cacheResult(context, '');
return '';
2014-06-24 00:42:43 +02:00
}
var entry = this.netFilteringCache.lookup(context.requestURL);
if ( entry !== undefined ) {
//console.debug('cache HIT: PageStore.filterRequest("%s")', context.requestURL);
return entry.result;
2014-09-14 22:20:40 +02:00
}
var result = '';
// Given that:
// - Dynamic filtering override static filtering
// - Evaluating dynamic filtering is much faster than static filtering
// We evaluate dynamic filtering first, and hopefully we can skip
// evaluation of static filtering.
if ( µb.userSettings.advancedUserEnabled ) {
var df = µb.dynamicNetFilteringEngine.clearRegisters();
df.evaluateCellZY(context.rootHostname, context.requestHostname, context.requestType);
if ( df.mustBlockOrAllow() ) {
result = df.toFilterString();
}
}
// Static filtering never override dynamic filtering
if ( result === '' ) {
result = µb.staticNetFilteringEngine.matchString(context);
}
//console.debug('cache MISS: PageStore.filterRequest("%s")', context.requestURL);
this.cacheResult(context, result);
// console.debug('[%s, %s] = "%s"', context.requestHostname, context.requestType, result);
return result;
};
/******************************************************************************/
PageStore.prototype.cacheResult = function(context, result) {
var requestHostname = context.requestHostname;
2014-12-30 22:36:29 +01:00
if ( this.hostnameToCountMap.hasOwnProperty(requestHostname) === false ) {
this.hostnameToCountMap[requestHostname] = 0;
}
var c = result.charAt(1);
if ( c === '' || c === 'a' ) {
this.hostnameToCountMap[requestHostname] += 0x00010000;
} else /* if ( c === 'b' ) */ {
this.hostnameToCountMap[requestHostname] += 0x00000001;
}
if ( collapsibleRequestTypes.indexOf(context.requestType) !== -1 ) {
this.netFilteringCache.add(context, result);
2014-10-02 22:45:26 +02:00
}
};
// Cache only what is worth it if logging is disabled
// http://jsperf.com/string-indexof-vs-object
var collapsibleRequestTypes = 'image sub_frame object';
2014-10-06 20:02:44 +02:00
/******************************************************************************/
2014-09-14 22:20:40 +02:00
// false: not blocked
// true: blocked
2014-07-03 14:28:15 +02:00
2014-09-14 22:20:40 +02:00
PageStore.prototype.boolFromResult = function(result) {
return typeof result === 'string' && result.charAt(1) === 'b';
2014-06-24 00:42:43 +02:00
};
/******************************************************************************/
PageStore.prototype.updateBadge = function() {
2014-08-02 17:40:27 +02:00
var netFiltering = this.getNetFilteringSwitch();
var iconPaths = netFiltering ?
2014-09-04 03:18:36 +02:00
{ '19': 'img/browsericons/icon19.png', '38': 'img/browsericons/icon38.png' } :
{ '19': 'img/browsericons/icon19-off.png', '38': 'img/browsericons/icon38-off.png' };
2014-06-24 00:42:43 +02:00
var iconStr = '';
2014-08-02 17:40:27 +02:00
if ( µb.userSettings.showIconBadge && netFiltering && this.perLoadBlockedRequestCount ) {
2014-10-19 16:44:09 +02:00
// Safari can't show formatted strings, only integers.
if ( vAPI.safari ) {
iconStr = this.perLoadBlockedRequestCount;
}
else {
iconStr = µb.utils.formatCount(this.perLoadBlockedRequestCount);
}
2014-06-24 00:42:43 +02:00
}
vAPI.setIcon(this.tabId, iconPaths, iconStr);
2014-06-24 00:42:43 +02:00
};
// https://www.youtube.com/watch?v=drW8p_dTLD4
2014-06-24 00:42:43 +02:00
/******************************************************************************/
return {
2014-09-14 22:20:40 +02:00
factory: PageStore.factory
2014-06-24 00:42:43 +02:00
};
})();
/******************************************************************************/