mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-16 23:42:39 +01:00
3919a16bb8
Related issues: - https://github.com/uBlockOrigin/uBlock-issues/issues/1201 - https://github.com/uBlockOrigin/uBlock-issues/issues/3184 Top-level media resources are not meant to be blocked, see: https://github.com/gorhill/uBlock/issues/2813#issuecomment-317177212
1139 lines
38 KiB
JavaScript
1139 lines
38 KiB
JavaScript
/*******************************************************************************
|
|
|
|
uBlock Origin - a comprehensive, efficient content blocker
|
|
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
|
|
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
|
|
*/
|
|
|
|
/******************************************************************************/
|
|
|
|
import {
|
|
domainFromHostname,
|
|
hostnameFromURI,
|
|
isNetworkURI,
|
|
} from './uri-utils.js';
|
|
|
|
import {
|
|
sessionFirewall,
|
|
sessionSwitches,
|
|
sessionURLFiltering,
|
|
} from './filtering-engines.js';
|
|
|
|
import contextMenu from './contextmenu.js';
|
|
import logger from './logger.js';
|
|
import { orphanizeString } from './text-utils.js';
|
|
import { redirectEngine } from './redirect-engine.js';
|
|
import staticNetFilteringEngine from './static-net-filtering.js';
|
|
import webext from './webext.js';
|
|
import µb from './background.js';
|
|
|
|
/*******************************************************************************
|
|
|
|
A PageRequestStore object is used to store net requests in two ways:
|
|
|
|
To record distinct net requests
|
|
To create a log of net requests
|
|
|
|
**/
|
|
|
|
/******************************************************************************/
|
|
|
|
const NetFilteringResultCache = class {
|
|
constructor() {
|
|
this.pruneTimer = vAPI.defer.create(( ) => {
|
|
this.prune();
|
|
});
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.blocked = new Map();
|
|
this.results = new Map();
|
|
this.hash = 0;
|
|
return this;
|
|
}
|
|
|
|
// https://github.com/gorhill/uBlock/issues/3619
|
|
// Don't collapse redirected resources
|
|
rememberResult(fctxt, result) {
|
|
if ( fctxt.tabId <= 0 ) { return; }
|
|
if ( this.results.size === 0 ) {
|
|
this.pruneAsync();
|
|
}
|
|
const key = `${fctxt.getDocHostname()} ${fctxt.type} ${fctxt.url}`;
|
|
this.results.set(key, {
|
|
result,
|
|
redirectURL: fctxt.redirectURL,
|
|
logData: fctxt.filter,
|
|
tstamp: Date.now()
|
|
});
|
|
if ( result !== 1 || fctxt.redirectURL !== undefined ) { return; }
|
|
const now = Date.now();
|
|
this.blocked.set(key, now);
|
|
this.hash = now;
|
|
}
|
|
|
|
rememberBlock(fctxt) {
|
|
if ( fctxt.tabId <= 0 ) { return; }
|
|
if ( this.blocked.size === 0 ) {
|
|
this.pruneAsync();
|
|
}
|
|
if ( fctxt.redirectURL !== undefined ) { return; }
|
|
const now = Date.now();
|
|
this.blocked.set(
|
|
`${fctxt.getDocHostname()} ${fctxt.type} ${fctxt.url}`,
|
|
now
|
|
);
|
|
this.hash = now;
|
|
}
|
|
|
|
forgetResult(docHostname, type, url) {
|
|
const key = `${docHostname} ${type} ${url}`;
|
|
this.results.delete(key);
|
|
this.blocked.delete(key);
|
|
}
|
|
|
|
empty() {
|
|
this.blocked.clear();
|
|
this.results.clear();
|
|
this.hash = 0;
|
|
this.pruneTimer.off();
|
|
}
|
|
|
|
prune() {
|
|
const obsolete = Date.now() - this.shelfLife;
|
|
for ( const entry of this.blocked ) {
|
|
if ( entry[1] <= obsolete ) {
|
|
this.results.delete(entry[0]);
|
|
this.blocked.delete(entry[0]);
|
|
}
|
|
}
|
|
for ( const entry of this.results ) {
|
|
if ( entry[1].tstamp <= obsolete ) {
|
|
this.results.delete(entry[0]);
|
|
}
|
|
}
|
|
if ( this.blocked.size !== 0 || this.results.size !== 0 ) {
|
|
this.pruneAsync();
|
|
}
|
|
}
|
|
|
|
pruneAsync() {
|
|
this.pruneTimer.on(this.shelfLife);
|
|
}
|
|
|
|
lookupResult(fctxt) {
|
|
const entry = this.results.get(
|
|
fctxt.getDocHostname() + ' ' +
|
|
fctxt.type + ' ' +
|
|
fctxt.url
|
|
);
|
|
if ( entry === undefined ) { return; }
|
|
// We need to use a new WAR secret if one is present since WAR secrets
|
|
// can only be used once.
|
|
if (
|
|
entry.redirectURL !== undefined &&
|
|
entry.redirectURL.startsWith(this.extensionOriginURL)
|
|
) {
|
|
const redirectURL = new URL(entry.redirectURL);
|
|
redirectURL.searchParams.set('secret', vAPI.warSecret.short());
|
|
entry.redirectURL = redirectURL.href;
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
lookupAllBlocked(hostname) {
|
|
const result = [];
|
|
for ( const entry of this.blocked ) {
|
|
const pos = entry[0].indexOf(' ');
|
|
if ( entry[0].slice(0, pos) === hostname ) {
|
|
result[result.length] = entry[0].slice(pos + 1);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static factory() {
|
|
return new NetFilteringResultCache();
|
|
}
|
|
};
|
|
|
|
NetFilteringResultCache.prototype.shelfLife = 15000;
|
|
NetFilteringResultCache.prototype.extensionOriginURL = vAPI.getURL('/');
|
|
|
|
/******************************************************************************/
|
|
|
|
// Frame stores are used solely to associate a URL with a frame id.
|
|
|
|
const FrameStore = class {
|
|
constructor(frameURL, parentId) {
|
|
this.init(frameURL, parentId);
|
|
}
|
|
|
|
init(frameURL, parentId) {
|
|
this.t0 = Date.now();
|
|
this.parentId = parentId;
|
|
this.exceptCname = undefined;
|
|
this.clickToLoad = false;
|
|
this.rawURL = frameURL;
|
|
if ( frameURL !== undefined ) {
|
|
this.hostname = hostnameFromURI(frameURL);
|
|
this.domain = domainFromHostname(this.hostname) || this.hostname;
|
|
}
|
|
// Evaluated on-demand
|
|
// - 0b01: specific cosmetic filtering
|
|
// - 0b10: generic cosmetic filtering
|
|
this._cosmeticFilteringBits = undefined;
|
|
return this;
|
|
}
|
|
|
|
dispose() {
|
|
this.rawURL = this.hostname = this.domain = '';
|
|
if ( FrameStore.junkyard.length < FrameStore.junkyardMax ) {
|
|
FrameStore.junkyard.push(this);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
updateURL(url) {
|
|
if ( typeof url !== 'string' ) { return; }
|
|
this.rawURL = url;
|
|
this.hostname = hostnameFromURI(url);
|
|
this.domain = domainFromHostname(this.hostname) || this.hostname;
|
|
this._cosmeticFilteringBits = undefined;
|
|
}
|
|
|
|
getCosmeticFilteringBits(tabId) {
|
|
if ( this._cosmeticFilteringBits !== undefined ) {
|
|
return this._cosmeticFilteringBits;
|
|
}
|
|
this._cosmeticFilteringBits = 0b11;
|
|
{
|
|
const result = staticNetFilteringEngine.matchRequestReverse(
|
|
'specifichide',
|
|
this.rawURL
|
|
);
|
|
if ( result !== 0 && logger.enabled ) {
|
|
µb.filteringContext
|
|
.duplicate()
|
|
.fromTabId(tabId)
|
|
.setURL(this.rawURL)
|
|
.setDocOriginFromURL(this.rawURL)
|
|
.setRealm('network')
|
|
.setType('specifichide')
|
|
.setFilter(staticNetFilteringEngine.toLogData())
|
|
.toLogger();
|
|
}
|
|
if ( result === 2 ) {
|
|
this._cosmeticFilteringBits &= ~0b01;
|
|
}
|
|
}
|
|
{
|
|
const result = staticNetFilteringEngine.matchRequestReverse(
|
|
'generichide',
|
|
this.rawURL
|
|
);
|
|
if ( result !== 0 && logger.enabled ) {
|
|
µb.filteringContext
|
|
.duplicate()
|
|
.fromTabId(tabId)
|
|
.setURL(this.rawURL)
|
|
.setDocOriginFromURL(this.rawURL)
|
|
.setRealm('network')
|
|
.setType('generichide')
|
|
.setFilter(staticNetFilteringEngine.toLogData())
|
|
.toLogger();
|
|
}
|
|
if ( result === 2 ) {
|
|
this._cosmeticFilteringBits &= ~0b10;
|
|
}
|
|
}
|
|
return this._cosmeticFilteringBits;
|
|
}
|
|
|
|
shouldApplySpecificCosmeticFilters(tabId) {
|
|
return (this.getCosmeticFilteringBits(tabId) & 0b01) !== 0;
|
|
}
|
|
|
|
shouldApplyGenericCosmeticFilters(tabId) {
|
|
return (this.getCosmeticFilteringBits(tabId) & 0b10) !== 0;
|
|
}
|
|
|
|
static factory(frameURL, parentId = -1) {
|
|
const entry = FrameStore.junkyard.pop();
|
|
if ( entry === undefined ) {
|
|
return new FrameStore(frameURL, parentId);
|
|
}
|
|
return entry.init(frameURL, parentId);
|
|
}
|
|
};
|
|
|
|
// To mitigate memory churning
|
|
FrameStore.junkyard = [];
|
|
FrameStore.junkyardMax = 50;
|
|
|
|
/******************************************************************************/
|
|
|
|
const CountDetails = class {
|
|
constructor() {
|
|
this.allowed = { any: 0, frame: 0, script: 0 };
|
|
this.blocked = { any: 0, frame: 0, script: 0 };
|
|
}
|
|
reset() {
|
|
const { allowed, blocked } = this;
|
|
blocked.any = blocked.frame = blocked.script =
|
|
allowed.any = allowed.frame = allowed.script = 0;
|
|
}
|
|
inc(blocked, type = undefined) {
|
|
const stat = blocked ? this.blocked : this.allowed;
|
|
if ( type !== undefined ) { stat[type] += 1; }
|
|
stat.any += 1;
|
|
}
|
|
};
|
|
|
|
const HostnameDetails = class {
|
|
constructor(hostname) {
|
|
this.counts = new CountDetails();
|
|
this.init(hostname);
|
|
}
|
|
init(hostname) {
|
|
this.hostname = hostname;
|
|
this.counts.reset();
|
|
}
|
|
dispose() {
|
|
this.hostname = '';
|
|
if ( HostnameDetails.junkyard.length < HostnameDetails.junkyardMax ) {
|
|
HostnameDetails.junkyard.push(this);
|
|
}
|
|
}
|
|
};
|
|
|
|
HostnameDetails.junkyard = [];
|
|
HostnameDetails.junkyardMax = 100;
|
|
|
|
const HostnameDetailsMap = class extends Map {
|
|
reset() {
|
|
this.clear();
|
|
}
|
|
dispose() {
|
|
for ( const item of this.values() ) {
|
|
item.dispose();
|
|
}
|
|
this.reset();
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const PageStore = class {
|
|
constructor(tabId, details) {
|
|
this.extraData = new Map();
|
|
this.journal = [];
|
|
this.journalLastCommitted = this.journalLastUncommitted = -1;
|
|
this.journalLastUncommittedOrigin = undefined;
|
|
this.netFilteringCache = NetFilteringResultCache.factory();
|
|
this.hostnameDetailsMap = new HostnameDetailsMap();
|
|
this.counts = new CountDetails();
|
|
this.journalTimer = vAPI.defer.create(( ) => {
|
|
this.journalProcess();
|
|
});
|
|
this.largeMediaTimer = vAPI.defer.create(( ) => {
|
|
this.injectLargeMediaElementScriptlet();
|
|
});
|
|
this.init(tabId, details);
|
|
}
|
|
|
|
static factory(tabId, details) {
|
|
let entry = PageStore.junkyard.pop();
|
|
if ( entry === undefined ) {
|
|
entry = new PageStore(tabId, details);
|
|
} else {
|
|
entry.init(tabId, details);
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
// https://github.com/gorhill/uBlock/issues/3201
|
|
// The context is used to determine whether we report behavior change
|
|
// to the logger.
|
|
|
|
init(tabId, details) {
|
|
const tabContext = µb.tabContextManager.mustLookup(tabId);
|
|
this.tabId = tabId;
|
|
|
|
// If we are navigating from-to same site, remember whether large
|
|
// media elements were temporarily allowed.
|
|
const now = Date.now();
|
|
if ( typeof this.allowLargeMediaElementsUntil !== 'number' ) {
|
|
this.allowLargeMediaElementsUntil = now;
|
|
} else if ( tabContext.rootHostname !== this.tabHostname ) {
|
|
if ( this.tabHostname.endsWith('about-scheme') === false ) {
|
|
this.allowLargeMediaElementsUntil = now;
|
|
}
|
|
}
|
|
|
|
this.tabHostname = tabContext.rootHostname;
|
|
this.rawURL = tabContext.rawURL;
|
|
this.hostnameDetailsMap.reset();
|
|
this.contentLastModified = 0;
|
|
this.logData = undefined;
|
|
this.counts.reset();
|
|
this.remoteFontCount = 0;
|
|
this.popupBlockedCount = 0;
|
|
this.largeMediaCount = 0;
|
|
this.allowLargeMediaElementsRegex = undefined;
|
|
this.extraData.clear();
|
|
|
|
this.frameAddCount = 0;
|
|
this.frames = new Map();
|
|
this.setFrameURL({ url: tabContext.rawURL });
|
|
|
|
if ( this.titleFromDetails(details) === false ) {
|
|
this.title = tabContext.rawURL;
|
|
}
|
|
|
|
// Evaluated on-demand
|
|
this._noCosmeticFiltering = undefined;
|
|
|
|
// Remember if the webpage was potentially improperly filtered, for
|
|
// reporting purpose.
|
|
this.hasUnprocessedRequest = vAPI.net.hasUnprocessedRequest(tabId);
|
|
|
|
return this;
|
|
}
|
|
|
|
reuse(context, details) {
|
|
// When force refreshing a page, the page store data needs to be reset.
|
|
|
|
// If the hostname changes, we can't merely just update the context.
|
|
const tabContext = µb.tabContextManager.mustLookup(this.tabId);
|
|
if ( tabContext.rootHostname !== this.tabHostname ) {
|
|
context = '';
|
|
}
|
|
|
|
// If URL changes without a page reload (more and more common), then
|
|
// we 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' ) {
|
|
// As part of https://github.com/chrisaljoudi/uBlock/issues/405
|
|
// URL changed, force a re-evaluation of filtering switch
|
|
this.rawURL = tabContext.rawURL;
|
|
this.setFrameURL({ url: this.rawURL });
|
|
this.titleFromDetails(details);
|
|
return this;
|
|
}
|
|
|
|
// A new page is completely reloaded from scratch, reset all.
|
|
this.largeMediaTimer.off();
|
|
this.disposeFrameStores();
|
|
this.init(this.tabId, details);
|
|
return this;
|
|
}
|
|
|
|
dispose() {
|
|
this.tabHostname = '';
|
|
this.title = '';
|
|
this.rawURL = '';
|
|
this.hostnameDetailsMap.dispose();
|
|
this.netFilteringCache.empty();
|
|
this.allowLargeMediaElementsUntil = Date.now();
|
|
this.allowLargeMediaElementsRegex = undefined;
|
|
this.largeMediaTimer.off();
|
|
this.disposeFrameStores();
|
|
this.journalTimer.off();
|
|
this.journal = [];
|
|
this.journalLastUncommittedOrigin = undefined;
|
|
this.journalLastCommitted = this.journalLastUncommitted = -1;
|
|
if ( PageStore.junkyard.length < PageStore.junkyardMax ) {
|
|
PageStore.junkyard.push(this);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
titleFromDetails(details) {
|
|
if (
|
|
details instanceof Object === false ||
|
|
details.title === undefined
|
|
) {
|
|
return false;
|
|
}
|
|
this.title = orphanizeString(details.title.slice(0, 128));
|
|
return true;
|
|
}
|
|
|
|
disposeFrameStores() {
|
|
for ( const frameStore of this.frames.values() ) {
|
|
frameStore.dispose();
|
|
}
|
|
this.frames.clear();
|
|
}
|
|
|
|
getFrameStore(frameId) {
|
|
return this.frames.get(frameId) || null;
|
|
}
|
|
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/1858
|
|
// Mind that setFrameURL() can be called from navigation event handlers.
|
|
setFrameURL(details) {
|
|
let { frameId, url, parentFrameId } = details;
|
|
if ( frameId === undefined ) { frameId = 0; }
|
|
if ( parentFrameId === undefined ) { parentFrameId = -1; }
|
|
let frameStore = this.frames.get(frameId);
|
|
if ( frameStore !== undefined ) {
|
|
if ( url === frameStore.rawURL ) {
|
|
frameStore.parentId = parentFrameId;
|
|
} else {
|
|
frameStore.init(url, parentFrameId);
|
|
}
|
|
return frameStore;
|
|
}
|
|
frameStore = FrameStore.factory(url, parentFrameId);
|
|
this.frames.set(frameId, frameStore);
|
|
this.frameAddCount += 1;
|
|
if ( url.startsWith('about:') ) {
|
|
frameStore.updateURL(this.getEffectiveFrameURL({ frameId }));
|
|
}
|
|
if ( (this.frameAddCount & 0b111111) === 0 ) {
|
|
this.pruneFrames();
|
|
}
|
|
return frameStore;
|
|
}
|
|
|
|
getEffectiveFrameURL(sender) {
|
|
let { frameId } = sender;
|
|
for (;;) {
|
|
const frameStore = this.getFrameStore(frameId);
|
|
if ( frameStore === null ) { break; }
|
|
if ( frameStore.rawURL.startsWith('about:') === false ) {
|
|
return frameStore.rawURL;
|
|
}
|
|
frameId = frameStore.parentId;
|
|
if ( frameId === -1 ) { break; }
|
|
}
|
|
return sender.frameURL;
|
|
}
|
|
|
|
// There is no event to tell us a specific subframe has been removed from
|
|
// the main document. The code below will remove subframes which are no
|
|
// longer present in the root document. Removing obsolete subframes is
|
|
// not a critical task, so this is executed just once on a while, to avoid
|
|
// bloated dictionary of subframes.
|
|
// A TTL is used to avoid race conditions when new iframes are added
|
|
// through the webRequest API but still not yet visible through the
|
|
// webNavigation API.
|
|
async pruneFrames() {
|
|
let entries;
|
|
try {
|
|
entries = await webext.webNavigation.getAllFrames({
|
|
tabId: this.tabId
|
|
});
|
|
} catch(ex) {
|
|
}
|
|
if ( Array.isArray(entries) === false ) { return; }
|
|
const toKeep = new Set();
|
|
for ( const { frameId } of entries ) {
|
|
toKeep.add(frameId);
|
|
}
|
|
const obsolete = Date.now() - 60000;
|
|
for ( const [ frameId, { t0 } ] of this.frames ) {
|
|
if ( toKeep.has(frameId) || t0 >= obsolete ) { continue; }
|
|
this.frames.delete(frameId);
|
|
}
|
|
}
|
|
|
|
getNetFilteringSwitch() {
|
|
return µb.tabContextManager
|
|
.mustLookup(this.tabId)
|
|
.getNetFilteringSwitch();
|
|
}
|
|
|
|
toggleNetFilteringSwitch(url, scope, state) {
|
|
µb.toggleNetFilteringSwitch(url, scope, state);
|
|
this.netFilteringCache.empty();
|
|
}
|
|
|
|
shouldApplyCosmeticFilters(frameId = 0) {
|
|
if ( this._noCosmeticFiltering === undefined ) {
|
|
this._noCosmeticFiltering = this.getNetFilteringSwitch() === false;
|
|
if ( this._noCosmeticFiltering === false ) {
|
|
this._noCosmeticFiltering = sessionSwitches.evaluateZ(
|
|
'no-cosmetic-filtering',
|
|
this.tabHostname
|
|
) === true;
|
|
if ( this._noCosmeticFiltering && logger.enabled ) {
|
|
µb.filteringContext
|
|
.duplicate()
|
|
.fromTabId(this.tabId)
|
|
.setURL(this.rawURL)
|
|
.setRealm('cosmetic')
|
|
.setType('dom')
|
|
.setFilter(sessionSwitches.toLogData())
|
|
.toLogger();
|
|
}
|
|
}
|
|
}
|
|
if ( this._noCosmeticFiltering ) { return false; }
|
|
if ( frameId === -1 ) { return true; }
|
|
// Cosmetic filtering can be effectively disabled when both specific
|
|
// and generic cosmetic filters are disabled.
|
|
return this.shouldApplySpecificCosmeticFilters(frameId) ||
|
|
this.shouldApplyGenericCosmeticFilters(frameId);
|
|
}
|
|
|
|
shouldApplySpecificCosmeticFilters(frameId) {
|
|
if ( this.shouldApplyCosmeticFilters(-1) === false ) { return false; }
|
|
const frameStore = this.getFrameStore(frameId);
|
|
if ( frameStore === null ) { return false; }
|
|
return frameStore.shouldApplySpecificCosmeticFilters(this.tabId);
|
|
}
|
|
|
|
shouldApplyGenericCosmeticFilters(frameId) {
|
|
if ( this.shouldApplyCosmeticFilters(-1) === false ) { return false; }
|
|
const frameStore = this.getFrameStore(frameId);
|
|
if ( frameStore === null ) { return false; }
|
|
return frameStore.shouldApplyGenericCosmeticFilters(this.tabId);
|
|
}
|
|
|
|
// https://github.com/gorhill/uBlock/issues/2105
|
|
// Be sure to always include the current page's hostname -- it might not
|
|
// be present when the page itself is pulled from the browser's
|
|
// short-term memory cache.
|
|
getAllHostnameDetails() {
|
|
if (
|
|
this.hostnameDetailsMap.has(this.tabHostname) === false &&
|
|
isNetworkURI(this.rawURL)
|
|
) {
|
|
this.hostnameDetailsMap.set(
|
|
this.tabHostname,
|
|
new HostnameDetails(this.tabHostname)
|
|
);
|
|
}
|
|
return this.hostnameDetailsMap;
|
|
}
|
|
|
|
injectLargeMediaElementScriptlet() {
|
|
vAPI.tabs.executeScript(this.tabId, {
|
|
file: '/js/scriptlets/load-large-media-interactive.js',
|
|
allFrames: true,
|
|
runAt: 'document_idle',
|
|
});
|
|
contextMenu.update(this.tabId);
|
|
}
|
|
|
|
temporarilyAllowLargeMediaElements(state) {
|
|
this.largeMediaCount = 0;
|
|
contextMenu.update(this.tabId);
|
|
if ( state ) {
|
|
this.allowLargeMediaElementsUntil = 0;
|
|
this.allowLargeMediaElementsRegex = undefined;
|
|
} else {
|
|
this.allowLargeMediaElementsUntil = Date.now();
|
|
}
|
|
vAPI.tabs.executeScript(this.tabId, {
|
|
file: '/js/scriptlets/load-large-media-all.js',
|
|
allFrames: true,
|
|
});
|
|
}
|
|
|
|
// https://github.com/gorhill/uBlock/issues/2053
|
|
// There is no way around using journaling to ensure we deal properly with
|
|
// potentially out of order navigation events vs. network request events.
|
|
journalAddRequest(fctxt, result) {
|
|
const hostname = fctxt.getHostname();
|
|
if ( hostname === '' ) { return; }
|
|
this.journal.push(hostname, result, fctxt.itype);
|
|
this.journalTimer.on(µb.hiddenSettings.requestJournalProcessPeriod);
|
|
}
|
|
|
|
journalAddRootFrame(type, url) {
|
|
if ( type === 'committed' ) {
|
|
this.journalLastCommitted = this.journal.length;
|
|
if (
|
|
this.journalLastUncommitted !== -1 &&
|
|
this.journalLastUncommitted < this.journalLastCommitted &&
|
|
this.journalLastUncommittedOrigin === hostnameFromURI(url)
|
|
) {
|
|
this.journalLastCommitted = this.journalLastUncommitted;
|
|
}
|
|
} else if ( type === 'uncommitted' ) {
|
|
const newOrigin = hostnameFromURI(url);
|
|
if (
|
|
this.journalLastUncommitted === -1 ||
|
|
this.journalLastUncommittedOrigin !== newOrigin
|
|
) {
|
|
this.journalLastUncommitted = this.journal.length;
|
|
this.journalLastUncommittedOrigin = newOrigin;
|
|
}
|
|
}
|
|
this.journalTimer.offon(µb.hiddenSettings.requestJournalProcessPeriod);
|
|
}
|
|
|
|
journalProcess() {
|
|
this.journalTimer.off();
|
|
|
|
const journal = this.journal;
|
|
const pivot = Math.max(0, this.journalLastCommitted);
|
|
const now = Date.now();
|
|
const { SCRIPT, SUB_FRAME, OBJECT } = µb.FilteringContext;
|
|
let aggregateAllowed = 0;
|
|
let aggregateBlocked = 0;
|
|
|
|
// Everything after pivot originates from current page.
|
|
for ( let i = pivot; i < journal.length; i += 3 ) {
|
|
const hostname = journal[i+0];
|
|
let hnDetails = this.hostnameDetailsMap.get(hostname);
|
|
if ( hnDetails === undefined ) {
|
|
hnDetails = new HostnameDetails(hostname);
|
|
this.hostnameDetailsMap.set(hostname, hnDetails);
|
|
this.contentLastModified = now;
|
|
}
|
|
const blocked = journal[i+1] === 1;
|
|
const itype = journal[i+2];
|
|
if ( itype === SCRIPT ) {
|
|
hnDetails.counts.inc(blocked, 'script');
|
|
this.counts.inc(blocked, 'script');
|
|
} else if ( itype === SUB_FRAME || itype === OBJECT ) {
|
|
hnDetails.counts.inc(blocked, 'frame');
|
|
this.counts.inc(blocked, 'frame');
|
|
} else {
|
|
hnDetails.counts.inc(blocked);
|
|
this.counts.inc(blocked);
|
|
}
|
|
if ( blocked ) {
|
|
aggregateBlocked += 1;
|
|
} else {
|
|
aggregateAllowed += 1;
|
|
}
|
|
}
|
|
this.journalLastUncommitted = this.journalLastCommitted = -1;
|
|
|
|
// https://github.com/chrisaljoudi/uBlock/issues/905#issuecomment-76543649
|
|
// No point updating the badge if it's not being displayed.
|
|
if ( aggregateBlocked !== 0 && µb.userSettings.showIconBadge ) {
|
|
µb.updateToolbarIcon(this.tabId, 0x02);
|
|
}
|
|
|
|
// Everything before pivot does not originate from current page -- we
|
|
// still need to bump global blocked/allowed counts.
|
|
for ( let i = 0; i < pivot; i += 3 ) {
|
|
if ( journal[i+1] === 1 ) {
|
|
aggregateBlocked += 1;
|
|
} else {
|
|
aggregateAllowed += 1;
|
|
}
|
|
}
|
|
if ( aggregateAllowed || aggregateBlocked ) {
|
|
µb.incrementRequestStats(aggregateBlocked, aggregateAllowed);
|
|
}
|
|
journal.length = 0;
|
|
}
|
|
|
|
filterRequest(fctxt) {
|
|
fctxt.filter = undefined;
|
|
fctxt.redirectURL = undefined;
|
|
|
|
if ( this.getNetFilteringSwitch(fctxt) === false ) {
|
|
return 0;
|
|
}
|
|
|
|
if (
|
|
fctxt.itype === fctxt.CSP_REPORT &&
|
|
this.filterCSPReport(fctxt) === 1
|
|
) {
|
|
return 1;
|
|
}
|
|
|
|
if (
|
|
(fctxt.itype & fctxt.FONT_ANY) !== 0 &&
|
|
this.filterFont(fctxt) === 1 )
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
if (
|
|
fctxt.itype === fctxt.SCRIPT &&
|
|
this.filterScripting(fctxt, true) === 1
|
|
) {
|
|
return 1;
|
|
}
|
|
|
|
const cacheableResult =
|
|
this.cacheableResults.has(fctxt.itype) &&
|
|
fctxt.aliasURL === undefined;
|
|
|
|
if ( cacheableResult ) {
|
|
const entry = this.netFilteringCache.lookupResult(fctxt);
|
|
if ( entry !== undefined ) {
|
|
fctxt.redirectURL = entry.redirectURL;
|
|
fctxt.filter = entry.logData;
|
|
return entry.result;
|
|
}
|
|
}
|
|
|
|
const requestType = fctxt.type;
|
|
const loggerEnabled = logger.enabled;
|
|
|
|
// Dynamic URL filtering.
|
|
let result = sessionURLFiltering.evaluateZ(
|
|
fctxt.getTabHostname(),
|
|
fctxt.url,
|
|
requestType
|
|
);
|
|
if ( result !== 0 && loggerEnabled ) {
|
|
fctxt.filter = sessionURLFiltering.toLogData();
|
|
}
|
|
|
|
// Dynamic hostname/type filtering.
|
|
if ( result === 0 && µb.userSettings.advancedUserEnabled ) {
|
|
result = sessionFirewall.evaluateCellZY(
|
|
fctxt.getTabHostname(),
|
|
fctxt.getHostname(),
|
|
requestType
|
|
);
|
|
if ( result !== 0 && result !== 3 && loggerEnabled ) {
|
|
fctxt.filter = sessionFirewall.toLogData();
|
|
}
|
|
}
|
|
|
|
// Static filtering has lowest precedence.
|
|
const snfe = staticNetFilteringEngine;
|
|
if ( result === 0 || result === 3 ) {
|
|
result = snfe.matchRequest(fctxt);
|
|
if ( result !== 0 ) {
|
|
if ( loggerEnabled ) {
|
|
fctxt.setFilter(snfe.toLogData());
|
|
}
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/943
|
|
// Blanket-except blocked aliased canonical hostnames?
|
|
if (
|
|
result === 1 &&
|
|
fctxt.aliasURL !== undefined &&
|
|
snfe.isBlockImportant() === false &&
|
|
this.shouldExceptCname(fctxt)
|
|
) {
|
|
return 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Click-to-load?
|
|
// When frameId is not -1, the resource is always sub_frame.
|
|
if ( result === 1 && fctxt.frameId !== -1 ) {
|
|
const frameStore = this.getFrameStore(fctxt.frameId);
|
|
if ( frameStore !== null && frameStore.clickToLoad ) {
|
|
result = 2;
|
|
if ( loggerEnabled ) {
|
|
fctxt.pushFilter({
|
|
result,
|
|
source: 'network',
|
|
raw: 'click-to-load',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Modifier(s)?
|
|
// A modifier is an action which transform the original network request.
|
|
// https://github.com/gorhill/uBlock/issues/949
|
|
// Redirect blocked request?
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/760
|
|
// Redirect non-blocked request?
|
|
if ( (fctxt.itype & fctxt.INLINE_ANY) === 0 ) {
|
|
if ( result === 1 ) {
|
|
this.redirectBlockedRequest(fctxt);
|
|
} else {
|
|
this.redirectNonBlockedRequest(fctxt);
|
|
}
|
|
}
|
|
|
|
if ( cacheableResult ) {
|
|
this.netFilteringCache.rememberResult(fctxt, result);
|
|
} else if ( result === 1 && this.collapsibleResources.has(fctxt.itype) ) {
|
|
this.netFilteringCache.rememberBlock(fctxt);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
filterOnHeaders(fctxt, headers) {
|
|
fctxt.filter = undefined;
|
|
|
|
if ( this.getNetFilteringSwitch(fctxt) === false ) { return 0; }
|
|
|
|
let result = staticNetFilteringEngine.matchHeaders(fctxt, headers);
|
|
if ( result === 0 ) { return 0; }
|
|
|
|
const loggerEnabled = logger.enabled;
|
|
if ( loggerEnabled ) {
|
|
fctxt.filter = staticNetFilteringEngine.toLogData();
|
|
}
|
|
|
|
// Dynamic filtering allow rules
|
|
// URL filtering
|
|
if (
|
|
result === 1 &&
|
|
sessionURLFiltering.evaluateZ(
|
|
fctxt.getTabHostname(),
|
|
fctxt.url,
|
|
fctxt.type
|
|
) === 2
|
|
) {
|
|
result = 2;
|
|
if ( loggerEnabled ) {
|
|
fctxt.filter = sessionURLFiltering.toLogData();
|
|
}
|
|
}
|
|
// Hostname filtering
|
|
if (
|
|
result === 1 &&
|
|
µb.userSettings.advancedUserEnabled &&
|
|
sessionFirewall.evaluateCellZY(
|
|
fctxt.getTabHostname(),
|
|
fctxt.getHostname(),
|
|
fctxt.type
|
|
) === 2
|
|
) {
|
|
result = 2;
|
|
if ( loggerEnabled ) {
|
|
fctxt.filter = sessionFirewall.toLogData();
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
redirectBlockedRequest(fctxt) {
|
|
const directives = staticNetFilteringEngine.redirectRequest(redirectEngine, fctxt);
|
|
if ( directives === undefined ) { return; }
|
|
if ( logger.enabled !== true ) { return; }
|
|
fctxt.pushFilters(directives.map(a => a.logData()));
|
|
if ( fctxt.redirectURL === undefined ) { return; }
|
|
fctxt.pushFilter({
|
|
source: 'redirect',
|
|
raw: directives[directives.length-1].value
|
|
});
|
|
}
|
|
|
|
redirectNonBlockedRequest(fctxt) {
|
|
const transformDirectives = staticNetFilteringEngine.transformRequest(fctxt);
|
|
const pruneDirectives = fctxt.redirectURL === undefined &&
|
|
staticNetFilteringEngine.hasQuery(fctxt) &&
|
|
staticNetFilteringEngine.filterQuery(fctxt) ||
|
|
undefined;
|
|
if ( transformDirectives === undefined && pruneDirectives === undefined ) { return; }
|
|
if ( logger.enabled !== true ) { return; }
|
|
if ( transformDirectives !== undefined ) {
|
|
fctxt.pushFilters(transformDirectives.map(a => a.logData()));
|
|
}
|
|
if ( pruneDirectives !== undefined ) {
|
|
fctxt.pushFilters(pruneDirectives.map(a => a.logData()));
|
|
}
|
|
if ( fctxt.redirectURL === undefined ) { return; }
|
|
fctxt.pushFilter({
|
|
source: 'redirect',
|
|
raw: fctxt.redirectURL
|
|
});
|
|
}
|
|
|
|
filterCSPReport(fctxt) {
|
|
if (
|
|
sessionSwitches.evaluateZ(
|
|
'no-csp-reports',
|
|
fctxt.getHostname()
|
|
)
|
|
) {
|
|
if ( logger.enabled ) {
|
|
fctxt.filter = sessionSwitches.toLogData();
|
|
}
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
filterFont(fctxt) {
|
|
if ( fctxt.itype === fctxt.FONT ) {
|
|
this.remoteFontCount += 1;
|
|
}
|
|
if (
|
|
sessionSwitches.evaluateZ(
|
|
'no-remote-fonts',
|
|
fctxt.getTabHostname()
|
|
) !== false
|
|
) {
|
|
if ( logger.enabled ) {
|
|
fctxt.filter = sessionSwitches.toLogData();
|
|
}
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
filterScripting(fctxt, netFiltering) {
|
|
fctxt.filter = undefined;
|
|
if ( netFiltering === undefined ) {
|
|
netFiltering = this.getNetFilteringSwitch(fctxt);
|
|
}
|
|
if (
|
|
netFiltering === false ||
|
|
sessionSwitches.evaluateZ(
|
|
'no-scripting',
|
|
fctxt.getTabHostname()
|
|
) === false
|
|
) {
|
|
return 0;
|
|
}
|
|
if ( logger.enabled ) {
|
|
fctxt.filter = sessionSwitches.toLogData();
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// The caller is responsible to check whether filtering is enabled or not.
|
|
filterLargeMediaElement(fctxt, size) {
|
|
fctxt.filter = undefined;
|
|
|
|
if ( this.allowLargeMediaElementsUntil === 0 ) {
|
|
return 0;
|
|
}
|
|
// Disregard large media elements previously allowed: for example, to
|
|
// seek inside a previously allowed audio/video.
|
|
if (
|
|
this.allowLargeMediaElementsRegex instanceof RegExp &&
|
|
this.allowLargeMediaElementsRegex.test(fctxt.url)
|
|
) {
|
|
return 0;
|
|
}
|
|
if ( Date.now() < this.allowLargeMediaElementsUntil ) {
|
|
const sources = this.allowLargeMediaElementsRegex instanceof RegExp
|
|
? [ this.allowLargeMediaElementsRegex.source ]
|
|
: [];
|
|
sources.push('^' + µb.escapeRegex(fctxt.url));
|
|
this.allowLargeMediaElementsRegex = new RegExp(sources.join('|'));
|
|
return 0;
|
|
}
|
|
if (
|
|
sessionSwitches.evaluateZ(
|
|
'no-large-media',
|
|
fctxt.getTabHostname()
|
|
) !== true
|
|
) {
|
|
this.allowLargeMediaElementsUntil = 0;
|
|
return 0;
|
|
}
|
|
if ( (size >>> 10) < µb.userSettings.largeMediaSize ) {
|
|
return 0;
|
|
}
|
|
|
|
this.largeMediaCount += 1;
|
|
this.largeMediaTimer.on(500);
|
|
|
|
if ( logger.enabled ) {
|
|
fctxt.filter = sessionSwitches.toLogData();
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
clickToLoad(frameId, frameURL) {
|
|
let frameStore = this.getFrameStore(frameId);
|
|
if ( frameStore === null ) {
|
|
frameStore = this.setFrameURL({ frameId, url: frameURL });
|
|
}
|
|
this.netFilteringCache.forgetResult(
|
|
this.tabHostname,
|
|
'sub_frame',
|
|
frameURL
|
|
);
|
|
frameStore.clickToLoad = true;
|
|
}
|
|
|
|
shouldExceptCname(fctxt) {
|
|
let exceptCname;
|
|
let frameStore;
|
|
if ( fctxt.docId !== undefined ) {
|
|
frameStore = this.getFrameStore(fctxt.docId);
|
|
if ( frameStore instanceof Object ) {
|
|
exceptCname = frameStore.exceptCname;
|
|
}
|
|
}
|
|
if ( exceptCname === undefined ) {
|
|
const result = staticNetFilteringEngine.matchRequestReverse(
|
|
'cname',
|
|
frameStore instanceof Object
|
|
? frameStore.rawURL
|
|
: fctxt.getDocOrigin()
|
|
);
|
|
exceptCname = result === 2
|
|
? staticNetFilteringEngine.toLogData()
|
|
: false;
|
|
if ( frameStore instanceof Object ) {
|
|
frameStore.exceptCname = exceptCname;
|
|
}
|
|
}
|
|
if ( exceptCname === false ) { return false; }
|
|
if ( exceptCname instanceof Object ) {
|
|
fctxt.setFilter(exceptCname);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
getBlockedResources(request, response) {
|
|
const normalURL = µb.normalizeTabURL(this.tabId, request.frameURL);
|
|
const resources = request.resources;
|
|
const fctxt = µb.filteringContext;
|
|
fctxt.fromTabId(this.tabId)
|
|
.setDocOriginFromURL(normalURL);
|
|
// Force some resources to go through the filtering engine in order to
|
|
// populate the blocked-resources cache. This is required because for
|
|
// some resources it's not possible to detect whether they were blocked
|
|
// content script-side (i.e. `iframes` -- unlike `img`).
|
|
if ( Array.isArray(resources) && resources.length !== 0 ) {
|
|
for ( const resource of resources ) {
|
|
this.filterRequest(
|
|
fctxt.setType(resource.type).setURL(resource.url)
|
|
);
|
|
}
|
|
}
|
|
if ( this.netFilteringCache.hash === response.hash ) { return; }
|
|
response.hash = this.netFilteringCache.hash;
|
|
response.blockedResources =
|
|
this.netFilteringCache.lookupAllBlocked(fctxt.getDocHostname());
|
|
}
|
|
};
|
|
|
|
PageStore.prototype.cacheableResults = new Set([
|
|
µb.FilteringContext.SUB_FRAME,
|
|
]);
|
|
|
|
PageStore.prototype.collapsibleResources = new Set([
|
|
µb.FilteringContext.IMAGE,
|
|
µb.FilteringContext.MEDIA,
|
|
µb.FilteringContext.OBJECT,
|
|
µb.FilteringContext.SUB_FRAME,
|
|
]);
|
|
|
|
// To mitigate memory churning
|
|
PageStore.junkyard = [];
|
|
PageStore.junkyardMax = 10;
|
|
|
|
/******************************************************************************/
|
|
|
|
export { PageStore };
|