1
0
Fork 0
mirror of https://github.com/gorhill/uMatrix.git synced 2024-06-02 02:14:52 +12:00
uMatrix/src/js/pagestats.js
2015-05-02 18:46:25 -04:00

557 lines
15 KiB
JavaScript

/*******************************************************************************
µMatrix - a Chromium browser extension to black/white list requests.
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/uMatrix
*/
/* global chrome, µMatrix */
/* jshint bitwise: false, boss: true */
/*******************************************************************************
A PageRequestStore object is used to store net requests in two ways:
To record distinct net requests
To create a log of net requests
**/
µMatrix.PageRequestStats = (function() {
/******************************************************************************/
// Caching useful global vars
var µm = µMatrix;
var µmuri = null;
/******************************************************************************/
// Hidden vars
var typeToCode = {
'doc' : 'a',
'frame' : 'b',
'css' : 'c',
'script': 'd',
'image' : 'e',
'plugin': 'f',
'xhr' : 'g',
'other' : 'h',
'cookie': 'i'
};
var codeToType = {
'a': 'doc',
'b': 'frame',
'c': 'css',
'd': 'script',
'e': 'image',
'f': 'plugin',
'g': 'xhr',
'h': 'other',
'i': 'cookie'
};
/******************************************************************************/
// It's just a dict-based "packer"
var stringPacker = {
codeGenerator: 1,
codeJunkyard: [],
mapStringToEntry: {},
mapCodeToString: {},
Entry: function(code) {
this.count = 0;
this.code = code;
},
remember: function(code) {
if ( code === '' ) {
return;
}
var s = this.mapCodeToString[code];
if ( s ) {
var entry = this.mapStringToEntry[s];
entry.count++;
}
},
forget: function(code) {
if ( code === '' ) {
return;
}
var s = this.mapCodeToString[code];
if ( s ) {
var entry = this.mapStringToEntry[s];
entry.count--;
if ( !entry.count ) {
// console.debug('stringPacker > releasing code "%s" (aka "%s")', code, s);
this.codeJunkyard.push(entry);
delete this.mapCodeToString[code];
delete this.mapStringToEntry[s];
}
}
},
pack: function(s) {
var entry = this.entryFromString(s);
if ( !entry ) {
return '';
}
return entry.code;
},
unpack: function(packed) {
return this.mapCodeToString[packed] || '';
},
stringify: function(code) {
if ( code <= 0xFFFF ) {
return String.fromCharCode(code);
}
return String.fromCharCode(code >>> 16) + String.fromCharCode(code & 0xFFFF);
},
entryFromString: function(s) {
if ( s === '' ) {
return null;
}
var entry = this.mapStringToEntry[s];
if ( !entry ) {
entry = this.codeJunkyard.pop();
if ( !entry ) {
entry = new this.Entry(this.stringify(this.codeGenerator++));
} else {
// console.debug('stringPacker > recycling code "%s" (aka "%s")', entry.code, s);
entry.count = 0;
}
this.mapStringToEntry[s] = entry;
this.mapCodeToString[entry.code] = s;
}
return entry;
}
};
/******************************************************************************/
var LogEntry = function() {
this.url = '';
this.type = '';
this.when = 0;
this.block = false;
};
var logEntryJunkyard = [];
LogEntry.prototype.dispose = function() {
this.url = this.type = '';
// Let's not grab and hold onto too much memory..
if ( logEntryJunkyard.length < 200 ) {
logEntryJunkyard.push(this);
}
};
var logEntryFactory = function() {
var entry = logEntryJunkyard.pop();
if ( entry ) {
return entry;
}
return new LogEntry();
};
/******************************************************************************/
var PageRequestStats = function() {
this.requests = {};
this.ringBuffer = null;
this.ringBufferPointer = 0;
if ( !µmuri ) {
µmuri = µm.URI;
}
};
/******************************************************************************/
PageRequestStats.prototype.init = function() {
return this;
};
/******************************************************************************/
var pageRequestStoreJunkyard = [];
var pageRequestStoreFactory = function() {
var pageRequestStore = pageRequestStoreJunkyard.pop();
if ( pageRequestStore ) {
pageRequestStore.init();
} else {
pageRequestStore = new PageRequestStats();
}
pageRequestStore.resizeLogBuffer(µm.userSettings.maxLoggedRequests);
return pageRequestStore;
};
/******************************************************************************/
PageRequestStats.prototype.disposeOne = function(reqKey) {
if ( this.requests[reqKey] ) {
delete this.requests[reqKey];
forgetRequestKey(reqKey);
}
};
/******************************************************************************/
PageRequestStats.prototype.dispose = function() {
var requests = this.requests;
for ( var reqKey in requests ) {
if ( requests.hasOwnProperty(reqKey) === false ) {
continue;
}
stringPacker.forget(reqKey.slice(3));
}
this.requests = {};
var i = this.ringBuffer.length;
var logEntry;
while ( i-- ) {
logEntry = this.ringBuffer[i];
if ( logEntry ) {
logEntry.dispose();
}
}
this.ringBuffer = [];
this.ringBufferPointer = 0;
if ( pageRequestStoreJunkyard.length < 8 ) {
pageRequestStoreJunkyard.push(this);
}
};
/******************************************************************************/
// Request key:
// index: 0123
// THHN
// ^^ ^
// || |
// || +--- short string code for hostname (dict-based)
// |+--- FNV32a hash of whole URI (irreversible)
// +--- single char code for type of request
var makeRequestKey = function(uri, reqType) {
// Ref: Given a URL, returns a unique 4-character long hash string
// Based on: FNV32a
// http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-reference-source
// The rest is custom, suited for µMatrix.
var hint = 0x811c9dc5;
var i = uri.length;
while ( i-- ) {
hint ^= uri.charCodeAt(i);
hint += (hint<<1) + (hint<<4) + (hint<<7) + (hint<<8) + (hint<<24);
hint >>>= 0;
}
var key = typeToCode[reqType] || 'z';
return key +
String.fromCharCode(hint >>> 22, hint >>> 12 & 0x3FF, hint & 0xFFF) +
stringPacker.pack(µmuri.hostnameFromURI(uri));
};
/******************************************************************************/
var rememberRequestKey = function(reqKey) {
stringPacker.remember(reqKey.slice(4));
};
var forgetRequestKey = function(reqKey) {
stringPacker.forget(reqKey.slice(4));
};
/******************************************************************************/
// Exported
var hostnameFromRequestKey = function(reqKey) {
return stringPacker.unpack(reqKey.slice(4));
};
PageRequestStats.prototype.hostnameFromRequestKey = hostnameFromRequestKey;
var typeFromRequestKey = function(reqKey) {
return codeToType[reqKey.charAt(0)];
};
PageRequestStats.prototype.typeFromRequestKey = typeFromRequestKey;
/******************************************************************************/
PageRequestStats.prototype.createEntryIfNotExists = function(url, type) {
var reqKey = makeRequestKey(url, type);
if ( this.requests[reqKey] ) {
return false;
}
rememberRequestKey(reqKey);
this.requests[reqKey] = Date.now();
return true;
};
/******************************************************************************/
PageRequestStats.prototype.resizeLogBuffer = function(size) {
if ( !this.ringBuffer ) {
this.ringBuffer = new Array(0);
this.ringBufferPointer = 0;
}
if ( size === this.ringBuffer.length ) {
return;
}
if ( !size ) {
this.ringBuffer = new Array(0);
this.ringBufferPointer = 0;
return;
}
var newBuffer = new Array(size);
var copySize = Math.min(size, this.ringBuffer.length);
var newBufferPointer = (copySize % size) | 0;
var isrc = this.ringBufferPointer;
var ides = newBufferPointer;
while ( copySize-- ) {
isrc--;
if ( isrc < 0 ) {
isrc = this.ringBuffer.length - 1;
}
ides--;
if ( ides < 0 ) {
ides = size - 1;
}
newBuffer[ides] = this.ringBuffer[isrc];
}
this.ringBuffer = newBuffer;
this.ringBufferPointer = newBufferPointer;
};
/******************************************************************************/
PageRequestStats.prototype.clearLogBuffer = function() {
var buffer = this.ringBuffer;
if ( buffer === null ) {
return;
}
var logEntry;
var i = buffer.length;
while ( i-- ) {
if ( logEntry = buffer[i] ) {
logEntry.dispose();
buffer[i] = null;
}
}
this.ringBufferPointer = 0;
};
/******************************************************************************/
PageRequestStats.prototype.logRequest = function(url, type, block) {
var buffer = this.ringBuffer;
var len = buffer.length;
if ( !len ) {
return;
}
var pointer = this.ringBufferPointer;
if ( !buffer[pointer] ) {
buffer[pointer] = logEntryFactory();
}
var logEntry = buffer[pointer];
logEntry.url = url;
logEntry.type = type;
logEntry.when = Date.now();
logEntry.block = block;
this.ringBufferPointer = ((pointer + 1) % len) | 0;
};
/******************************************************************************/
PageRequestStats.prototype.getLoggedRequests = function() {
var buffer = this.ringBuffer;
if ( !buffer.length ) {
return [];
}
// [0 - pointer] = most recent
// [pointer - length] = least recent
// thus, ascending order:
// [pointer - length] + [0 - pointer]
var pointer = this.ringBufferPointer;
return buffer.slice(pointer).concat(buffer.slice(0, pointer)).reverse();
};
/******************************************************************************/
PageRequestStats.prototype.getLoggedRequestEntry = function(reqURL, reqType) {
return this.requests[makeRequestKey(reqURL, reqType)];
};
/******************************************************************************/
PageRequestStats.prototype.getRequestKeys = function() {
return Object.keys(this.requests);
};
/******************************************************************************/
PageRequestStats.prototype.getRequestDict = function() {
return this.requests;
};
/******************************************************************************/
// Export
return {
factory: pageRequestStoreFactory,
hostnameFromRequestKey: hostnameFromRequestKey,
typeFromRequestKey: typeFromRequestKey
};
/******************************************************************************/
})();
/******************************************************************************/
/******************************************************************************/
µMatrix.PageStore = (function() {
'use strict';
/******************************************************************************/
var µm = µMatrix;
var pageStoreJunkyard = [];
/******************************************************************************/
var pageStoreFactory = function(tabContext) {
var entry = pageStoreJunkyard.pop();
if ( entry ) {
return entry.init(tabContext);
}
return new PageStore(tabContext);
};
/******************************************************************************/
function PageStore(tabContext) {
this.requestStats = new WebRequestStats();
this.off = false;
this.init(tabContext);
}
/******************************************************************************/
PageStore.prototype.init = function(tabContext) {
this.tabId = tabContext.tabId;
this.rawUrl = tabContext.rawURL;
this.pageUrl = tabContext.normalURL;
this.pageHostname = tabContext.rootHostname;
this.pageDomain = tabContext.rootDomain;
this.pageScriptBlocked = false;
this.thirdpartyScript = false;
this.requests = µm.PageRequestStats.factory();
this.domains = {};
this.allHostnamesString = ' ';
this.requestStats.reset();
this.distinctRequestCount = 0;
this.perLoadAllowedRequestCount = 0;
this.perLoadBlockedRequestCount = 0;
this.incinerationTimer = null;
return this;
};
/******************************************************************************/
PageStore.prototype.dispose = function() {
this.requests.dispose();
this.rawUrl = '';
this.pageUrl = '';
this.pageHostname = '';
this.pageDomain = '';
this.domains = {};
this.allHostnamesString = ' ';
if ( this.incinerationTimer !== null ) {
clearTimeout(this.incinerationTimer);
this.incinerationTimer = null;
}
if ( pageStoreJunkyard.length < 8 ) {
pageStoreJunkyard.push(this);
}
};
/******************************************************************************/
PageStore.prototype.recordRequest = function(type, url, block) {
// Count blocked/allowed requests
this.requestStats.record(type, block);
// https://github.com/gorhill/httpswitchboard/issues/306
// If it is recorded locally, record globally
µm.requestStats.record(type, block);
µm.updateBadgeAsync(this.tabId);
if ( block !== false ) {
this.perLoadBlockedRequestCount++;
} else {
this.perLoadAllowedRequestCount++;
}
this.requests.logRequest(url, type, block);
if ( !this.requests.createEntryIfNotExists(url, type, block) ) {
return;
}
var hostname = µm.URI.hostnameFromURI(url);
// https://github.com/gorhill/httpswitchboard/issues/181
if ( type === 'script' && hostname !== this.pageHostname ) {
this.thirdpartyScript = true;
}
this.distinctRequestCount++;
if ( this.domains.hasOwnProperty(hostname) === false ) {
this.domains[hostname] = true;
this.allHostnamesString += hostname + ' ';
}
µm.urlStatsChanged(this.pageUrl);
// console.debug("pagestats.js > PageStore.recordRequest(): %o: %s @ %s", this, type, url);
};
/******************************************************************************/
return {
factory: pageStoreFactory
};
/******************************************************************************/
})();
/******************************************************************************/