Add support for click-to-load of embedded frames

Additionally, as a requirement to support click-to-load
feature, redirected resources will from now on no
longer be collapsed.

Related issues:
- https://github.com/gorhill/uBlock/issues/2688
- https://github.com/gorhill/uBlock/issues/3619
- https://github.com/gorhill/uBlock/issues/1899

This new feature should considered in its draft
stage and it needs to be fine-tuned as per
feedback.

Important: Only embedded frames can be converted
into click-to-load widgets, as only these can be
properly shieded from access by page content.

Examples of usage:

    ||youtube.com/embed/$3p,frame,redirect=clicktoload
    ||scribd.com/embeds/$3p,frame,redirect=clicktoload
    ||player.vimeo.com/video/$3p,frame,redirect=clicktoload
This commit is contained in:
Raymond Hill 2020-10-09 13:50:54 -04:00
parent ba0b62ec97
commit 5916920985
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
9 changed files with 238 additions and 22 deletions

48
src/css/click-to-load.css Normal file
View file

@ -0,0 +1,48 @@
/**
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
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
*/
body {
align-items: center;
background-color: var(--default-surface);
border: 1px solid var(--ubo-red);
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-evenly;
position: relative;
}
.logo {
left: 0;
padding: 2px;
position: absolute;
top: 0;
}
#frameURL {
font-family: monospace;
font-size: 90%;
overflow: hidden;
word-break: break-all;
}
#clickToLoad {
cursor: default;
}

View file

@ -74,6 +74,8 @@
:root {
--font-size: 14px;
--ubo-red: #800000;
--default-ink: var(--ink-80);
--default-ink-a4: var(--ink-80-a4);
--default-ink-a50: var(--ink-80-a50);

63
src/js/click-to-load.js Normal file
View file

@ -0,0 +1,63 @@
/*******************************************************************************
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
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';
/******************************************************************************/
/******************************************************************************/
(( ) => {
/******************************************************************************/
if ( typeof vAPI !== 'object' ) { return; }
const url = new URL(self.location.href);
const frameURL = url.searchParams.get('url');
const frameURLElem = document.getElementById('frameURL');
frameURLElem.textContent = frameURL;
const onWindowResize = function() {
document.body.style.width = `${self.innerWidth}px`;
document.body.style.height = `${self.innerHeight}px`;
};
onWindowResize();
self.addEventListener('resize', onWindowResize);
document.body.addEventListener('click', ev => {
if ( ev.isTrusted === false ) { return; }
//if ( ev.target === frameURLElem ) { return; }
vAPI.messaging.send('default', {
what: 'clickToLoad',
frameURL,
}).then(ok => {
if ( ok ) {
self.location.replace(frameURL);
}
});
});
/******************************************************************************/
})();

View file

@ -35,7 +35,8 @@
this.aliasURL = undefined;
this.hostname = undefined;
this.domain = undefined;
this.docId = undefined;
this.docId = -1;
this.frameId = -1;
this.docOrigin = undefined;
this.docHostname = undefined;
this.docDomain = undefined;
@ -69,9 +70,13 @@
this.type = details.type;
this.setURL(details.url);
this.aliasURL = details.aliasURL || undefined;
this.docId = details.type !== 'sub_frame'
? details.frameId
: details.parentFrameId;
if ( details.type !== 'sub_frame' ) {
this.docId = details.frameId;
this.frameId = -1;
} else {
this.docId = details.parentFrameId;
this.frameId = details.frameId;
}
if ( this.tabId > 0 ) {
if ( this.docId === 0 ) {
this.docOrigin = this.tabOrigin;
@ -81,7 +86,7 @@
this.setDocOriginFromURL(details.documentUrl);
} else {
const pageStore = µBlock.pageStoreFromTabId(this.tabId);
const docStore = pageStore && pageStore.getFrame(this.docId);
const docStore = pageStore && pageStore.getFrameStore(this.docId);
if ( docStore ) {
this.setDocOriginFromURL(docStore.rawURL);
} else {
@ -109,6 +114,7 @@
this.hostname = other.hostname;
this.domain = other.domain;
this.docId = other.docId;
this.frameId = other.frameId;
this.docOrigin = other.docOrigin;
this.docHostname = other.docHostname;
this.docDomain = other.docDomain;

View file

@ -41,6 +41,15 @@
const µb = µBlock;
const clickToLoad = function(request, sender) {
const { tabId, frameId } = µb.getMessageSenderDetails(sender);
if ( tabId === undefined || frameId === undefined ) { return false; }
const pageStore = µb.pageStoreFromTabId(tabId);
if ( pageStore === null ) { return false; }
pageStore.clickToLoad(frameId, request.frameURL);
return true;
};
const getDomainNames = function(targets) {
const µburi = µb.URI;
return targets.map(target => {
@ -93,13 +102,17 @@ const onMessage = function(request, sender, callback) {
}
// Sync
var response;
let response;
switch ( request.what ) {
case 'applyFilterListSelection':
response = µb.applyFilterListSelection(request);
break;
case 'clickToLoad':
response = clickToLoad(request, sender);
break;
case 'createUserFilter':
µb.createUserFilters(request);
break;

View file

@ -84,6 +84,12 @@ const NetFilteringResultCache = class {
this.hash = now;
}
forgetResult(fctxt) {
const key = `${fctxt.getDocHostname()} ${fctxt.type} ${fctxt.url}`;
this.results.delete(key);
this.blocked.delete(key);
}
empty() {
this.blocked.clear();
this.results.clear();
@ -165,6 +171,7 @@ const FrameStore = class {
init(frameURL) {
this.t0 = Date.now();
this.exceptCname = undefined;
this.clickToLoad = 0;
this.rawURL = frameURL;
if ( frameURL !== undefined ) {
this.hostname = vAPI.hostnameFromURI(frameURL);
@ -253,7 +260,7 @@ const PageStore = class {
this.frameAddCount = 0;
this.frames = new Map();
this.setFrame(0, tabContext.rawURL);
this.setFrameURL(0, tabContext.rawURL);
// The current filtering context is cloned because:
// - We may be called with or without the current context having been
@ -308,7 +315,7 @@ const PageStore = class {
// As part of https://github.com/chrisaljoudi/uBlock/issues/405
// URL changed, force a re-evaluation of filtering switch
this.rawURL = tabContext.rawURL;
this.setFrame(0, this.rawURL);
this.setFrameURL(0, this.rawURL);
return this;
}
@ -353,20 +360,23 @@ const PageStore = class {
this.frames.clear();
}
getFrame(frameId) {
getFrameStore(frameId) {
return this.frames.get(frameId) || null;
}
setFrame(frameId, frameURL) {
const frameStore = this.frames.get(frameId);
setFrameURL(frameId, frameURL) {
let frameStore = this.frames.get(frameId);
if ( frameStore !== undefined ) {
frameStore.init(frameURL);
return;
} else {
frameStore = FrameStore.factory(frameURL);
this.frames.set(frameId, frameStore);
this.frameAddCount += 1;
if ( (this.frameAddCount & 0b111111) === 0 ) {
this.pruneFrames();
}
}
this.frames.set(frameId, FrameStore.factory(frameURL));
this.frameAddCount += 1;
if ( (this.frameAddCount & 0b111111) !== 0 ) { return; }
this.pruneFrames();
return frameStore;
}
// There is no event to tell us a specific subframe has been removed from
@ -597,6 +607,22 @@ const PageStore = class {
}
}
// Click-to-load:
// When frameId is not -1, the resource is always sub_frame.
if ( result === 1 && fctxt.frameId !== -1 ) {
const docStore = this.getFrameStore(fctxt.frameId);
if ( docStore !== null && docStore.clickToLoad !== 0 ) {
result = 2;
if ( µb.logger.enabled ) {
fctxt.setFilter({
result,
source: 'network',
raw: 'click-to-load',
});
}
}
}
if ( cacheableResult ) {
this.netFilteringCache.rememberResult(fctxt, result);
} else if (
@ -696,11 +722,19 @@ const PageStore = class {
return 1;
}
clickToLoad(frameId, frameURL) {
let frameStore = this.getFrameStore(frameId);
if ( frameStore === null ) {
frameStore = this.setFrameURL(frameId, frameURL);
}
frameStore.clickToLoad = Date.now();
}
shouldExceptCname(fctxt) {
let exceptCname;
let frameStore;
if ( fctxt.docId !== undefined ) {
frameStore = this.getFrame(fctxt.docId);
frameStore = this.getFrameStore(fctxt.docId);
if ( frameStore instanceof Object ) {
exceptCname = frameStore.exceptCname;
}
@ -742,10 +776,12 @@ const PageStore = class {
// 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)
const result = this.filterRequest(
fctxt.setType(resource.type).setURL(resource.url)
);
if ( result === 1 && µb.redirectEngine.toURL(fctxt) ) {
this.forgetBlockedResource(fctxt);
}
}
}
if ( this.netFilteringCache.hash === response.hash ) { return; }
@ -753,6 +789,11 @@ const PageStore = class {
response.blockedResources =
this.netFilteringCache.lookupAllBlocked(fctxt.getDocHostname());
}
forgetBlockedResource(fctxt) {
if ( this.collapsibleResources.has(fctxt.type) === false ) { return; }
this.netFilteringCache.forgetResult(fctxt);
}
};
PageStore.prototype.cacheableResults = new Set([

View file

@ -67,6 +67,10 @@ const redirectableResources = new Map([
[ 'chartbeat.js', {
alias: 'static.chartbeat.com/chartbeat.js',
} ],
[ 'click-to-load.html', {
alias: 'clicktoload',
params: [ 'url' ],
} ],
[ 'doubleclick_instream_ad_status.js', {
alias: 'doubleclick.net/instream/ad_status.js',
} ],
@ -191,6 +195,7 @@ const RedirectEntry = class {
this.mime = '';
this.data = '';
this.warURL = undefined;
this.params = undefined;
}
// Prevent redirection to web accessible resources when the request is
@ -208,7 +213,15 @@ const RedirectEntry = class {
fctxt instanceof Object &&
fctxt.type !== 'xmlhttprequest'
) {
return `${this.warURL}${vAPI.warSecret()}`;
let url = `${this.warURL}${vAPI.warSecret()}`;
if ( this.params !== undefined ) {
for ( const name of this.params ) {
const value = fctxt[name];
if ( value === undefined ) { continue; }
url += `&${name}=${encodeURIComponent(value)}`;
}
}
return url;
}
if ( this.data === undefined ) { return; }
// https://github.com/uBlockOrigin/uBlock-issues/issues/701
@ -251,6 +264,7 @@ const RedirectEntry = class {
r.mime = selfie.mime;
r.data = selfie.data;
r.warURL = selfie.warURL;
r.params = selfie.params;
return r;
}
};
@ -721,6 +735,7 @@ RedirectEngine.prototype.loadBuiltinResources = function() {
mime: mimeFromName(name),
data,
warURL: vAPI.getURL(`/web_accessible_resources/${name}`),
params: details.params,
});
this.resources.set(name, entry);
if ( details.alias !== undefined ) {

View file

@ -101,7 +101,7 @@ const onBeforeRequest = function(details) {
details.type === 'sub_frame' &&
details.aliasURL === undefined
) {
pageStore.setFrame(details.frameId, details.url);
pageStore.setFrameURL(details.frameId, details.url);
}
if ( result === 2 ) {
return { cancel: false };
@ -113,10 +113,13 @@ const onBeforeRequest = function(details) {
// https://github.com/gorhill/uBlock/issues/949
// Redirect blocked request?
// https://github.com/gorhill/uBlock/issues/3619
// Don't collapse redirected resources
if ( µb.hiddenSettings.ignoreRedirectFilters !== true ) {
const url = µb.redirectEngine.toURL(fctxt);
if ( url !== undefined ) {
pageStore.internalRedirectionCount += 1;
pageStore.forgetBlockedResource(fctxt);
if ( µb.logger.enabled ) {
fctxt.setRealm('redirect')
.setFilter({ source: 'redirect', raw: µb.redirectEngine.resourceNameRegister })

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html id="ublock0-clicktoload">
<head>
<meta charset="utf-8">
<title>uBlock Origin Click-to-Load</title>
<link rel="stylesheet" href="../css/themes/default.css">
<link rel="stylesheet" href="../css/common.css">
<link rel="stylesheet" href="../css/click-to-load.css">
</head>
<body>
<span class="logo"><img src="../img/ublock.svg"></span>
<span id="clickToLoad">Click to load</span>
<div id="frameURL"></div>
<script src="../js/vapi.js"></script>
<script src="../js/vapi-common.js"></script>
<script src="../js/vapi-client.js"></script>
<script src="../js/i18n.js"></script>
<script src="../js/click-to-load.js"></script>
</body>
</html>