Skip to content

Commit 5916920

Browse files
committed
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: - #2688 - #3619 - #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
1 parent ba0b62e commit 5916920

File tree

9 files changed

+238
-22
lines changed

9 files changed

+238
-22
lines changed

src/css/click-to-load.css

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
uBlock Origin - a browser extension to block requests.
3+
Copyright (C) 2014-present Raymond Hill
4+
5+
This program is free software: you can redistribute it and/or modify
6+
it under the terms of the GNU General Public License as published by
7+
the Free Software Foundation, either version 3 of the License, or
8+
(at your option) any later version.
9+
10+
This program is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU General Public License for more details.
14+
15+
You should have received a copy of the GNU General Public License
16+
along with this program. If not, see {http://www.gnu.org/licenses/}.
17+
18+
Home: https://github.com/gorhill/uBlock
19+
*/
20+
21+
body {
22+
align-items: center;
23+
background-color: var(--default-surface);
24+
border: 1px solid var(--ubo-red);
25+
box-sizing: border-box;
26+
display: flex;
27+
flex-direction: column;
28+
justify-content: space-evenly;
29+
position: relative;
30+
}
31+
32+
.logo {
33+
left: 0;
34+
padding: 2px;
35+
position: absolute;
36+
top: 0;
37+
}
38+
39+
#frameURL {
40+
font-family: monospace;
41+
font-size: 90%;
42+
overflow: hidden;
43+
word-break: break-all;
44+
}
45+
46+
#clickToLoad {
47+
cursor: default;
48+
}

src/css/themes/default.css

+2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
:root {
7575
--font-size: 14px;
7676

77+
--ubo-red: #800000;
78+
7779
--default-ink: var(--ink-80);
7880
--default-ink-a4: var(--ink-80-a4);
7981
--default-ink-a50: var(--ink-80-a50);

src/js/click-to-load.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*******************************************************************************
2+
3+
uBlock Origin - a browser extension to block requests.
4+
Copyright (C) 2014-present Raymond Hill
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with this program. If not, see {http://www.gnu.org/licenses/}.
18+
19+
Home: https://github.com/gorhill/uBlock
20+
*/
21+
22+
'use strict';
23+
24+
/******************************************************************************/
25+
/******************************************************************************/
26+
27+
(( ) => {
28+
29+
/******************************************************************************/
30+
31+
if ( typeof vAPI !== 'object' ) { return; }
32+
33+
const url = new URL(self.location.href);
34+
const frameURL = url.searchParams.get('url');
35+
const frameURLElem = document.getElementById('frameURL');
36+
37+
frameURLElem.textContent = frameURL;
38+
39+
const onWindowResize = function() {
40+
document.body.style.width = `${self.innerWidth}px`;
41+
document.body.style.height = `${self.innerHeight}px`;
42+
};
43+
44+
onWindowResize();
45+
46+
self.addEventListener('resize', onWindowResize);
47+
48+
document.body.addEventListener('click', ev => {
49+
if ( ev.isTrusted === false ) { return; }
50+
//if ( ev.target === frameURLElem ) { return; }
51+
vAPI.messaging.send('default', {
52+
what: 'clickToLoad',
53+
frameURL,
54+
}).then(ok => {
55+
if ( ok ) {
56+
self.location.replace(frameURL);
57+
}
58+
});
59+
});
60+
61+
/******************************************************************************/
62+
63+
})();

src/js/filtering-context.js

+11-5
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
this.aliasURL = undefined;
3636
this.hostname = undefined;
3737
this.domain = undefined;
38-
this.docId = undefined;
38+
this.docId = -1;
39+
this.frameId = -1;
3940
this.docOrigin = undefined;
4041
this.docHostname = undefined;
4142
this.docDomain = undefined;
@@ -69,9 +70,13 @@
6970
this.type = details.type;
7071
this.setURL(details.url);
7172
this.aliasURL = details.aliasURL || undefined;
72-
this.docId = details.type !== 'sub_frame'
73-
? details.frameId
74-
: details.parentFrameId;
73+
if ( details.type !== 'sub_frame' ) {
74+
this.docId = details.frameId;
75+
this.frameId = -1;
76+
} else {
77+
this.docId = details.parentFrameId;
78+
this.frameId = details.frameId;
79+
}
7580
if ( this.tabId > 0 ) {
7681
if ( this.docId === 0 ) {
7782
this.docOrigin = this.tabOrigin;
@@ -81,7 +86,7 @@
8186
this.setDocOriginFromURL(details.documentUrl);
8287
} else {
8388
const pageStore = µBlock.pageStoreFromTabId(this.tabId);
84-
const docStore = pageStore && pageStore.getFrame(this.docId);
89+
const docStore = pageStore && pageStore.getFrameStore(this.docId);
8590
if ( docStore ) {
8691
this.setDocOriginFromURL(docStore.rawURL);
8792
} else {
@@ -109,6 +114,7 @@
109114
this.hostname = other.hostname;
110115
this.domain = other.domain;
111116
this.docId = other.docId;
117+
this.frameId = other.frameId;
112118
this.docOrigin = other.docOrigin;
113119
this.docHostname = other.docHostname;
114120
this.docDomain = other.docDomain;

src/js/messaging.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@
4141

4242
const µb = µBlock;
4343

44+
const clickToLoad = function(request, sender) {
45+
const { tabId, frameId } = µb.getMessageSenderDetails(sender);
46+
if ( tabId === undefined || frameId === undefined ) { return false; }
47+
const pageStore = µb.pageStoreFromTabId(tabId);
48+
if ( pageStore === null ) { return false; }
49+
pageStore.clickToLoad(frameId, request.frameURL);
50+
return true;
51+
};
52+
4453
const getDomainNames = function(targets) {
4554
const µburi = µb.URI;
4655
return targets.map(target => {
@@ -93,13 +102,17 @@ const onMessage = function(request, sender, callback) {
93102
}
94103

95104
// Sync
96-
var response;
105+
let response;
97106

98107
switch ( request.what ) {
99108
case 'applyFilterListSelection':
100109
response = µb.applyFilterListSelection(request);
101110
break;
102111

112+
case 'clickToLoad':
113+
response = clickToLoad(request, sender);
114+
break;
115+
103116
case 'createUserFilter':
104117
µb.createUserFilters(request);
105118
break;

src/js/pagestore.js

+55-14
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ const NetFilteringResultCache = class {
8484
this.hash = now;
8585
}
8686

87+
forgetResult(fctxt) {
88+
const key = `${fctxt.getDocHostname()} ${fctxt.type} ${fctxt.url}`;
89+
this.results.delete(key);
90+
this.blocked.delete(key);
91+
}
92+
8793
empty() {
8894
this.blocked.clear();
8995
this.results.clear();
@@ -165,6 +171,7 @@ const FrameStore = class {
165171
init(frameURL) {
166172
this.t0 = Date.now();
167173
this.exceptCname = undefined;
174+
this.clickToLoad = 0;
168175
this.rawURL = frameURL;
169176
if ( frameURL !== undefined ) {
170177
this.hostname = vAPI.hostnameFromURI(frameURL);
@@ -253,7 +260,7 @@ const PageStore = class {
253260

254261
this.frameAddCount = 0;
255262
this.frames = new Map();
256-
this.setFrame(0, tabContext.rawURL);
263+
this.setFrameURL(0, tabContext.rawURL);
257264

258265
// The current filtering context is cloned because:
259266
// - We may be called with or without the current context having been
@@ -308,7 +315,7 @@ const PageStore = class {
308315
// As part of https://github.com/chrisaljoudi/uBlock/issues/405
309316
// URL changed, force a re-evaluation of filtering switch
310317
this.rawURL = tabContext.rawURL;
311-
this.setFrame(0, this.rawURL);
318+
this.setFrameURL(0, this.rawURL);
312319
return this;
313320
}
314321

@@ -353,20 +360,23 @@ const PageStore = class {
353360
this.frames.clear();
354361
}
355362

356-
getFrame(frameId) {
363+
getFrameStore(frameId) {
357364
return this.frames.get(frameId) || null;
358365
}
359366

360-
setFrame(frameId, frameURL) {
361-
const frameStore = this.frames.get(frameId);
367+
setFrameURL(frameId, frameURL) {
368+
let frameStore = this.frames.get(frameId);
362369
if ( frameStore !== undefined ) {
363370
frameStore.init(frameURL);
364-
return;
371+
} else {
372+
frameStore = FrameStore.factory(frameURL);
373+
this.frames.set(frameId, frameStore);
374+
this.frameAddCount += 1;
375+
if ( (this.frameAddCount & 0b111111) === 0 ) {
376+
this.pruneFrames();
377+
}
365378
}
366-
this.frames.set(frameId, FrameStore.factory(frameURL));
367-
this.frameAddCount += 1;
368-
if ( (this.frameAddCount & 0b111111) !== 0 ) { return; }
369-
this.pruneFrames();
379+
return frameStore;
370380
}
371381

372382
// There is no event to tell us a specific subframe has been removed from
@@ -597,6 +607,22 @@ const PageStore = class {
597607
}
598608
}
599609

610+
// Click-to-load:
611+
// When frameId is not -1, the resource is always sub_frame.
612+
if ( result === 1 && fctxt.frameId !== -1 ) {
613+
const docStore = this.getFrameStore(fctxt.frameId);
614+
if ( docStore !== null && docStore.clickToLoad !== 0 ) {
615+
result = 2;
616+
if ( µb.logger.enabled ) {
617+
fctxt.setFilter({
618+
result,
619+
source: 'network',
620+
raw: 'click-to-load',
621+
});
622+
}
623+
}
624+
}
625+
600626
if ( cacheableResult ) {
601627
this.netFilteringCache.rememberResult(fctxt, result);
602628
} else if (
@@ -696,11 +722,19 @@ const PageStore = class {
696722
return 1;
697723
}
698724

725+
clickToLoad(frameId, frameURL) {
726+
let frameStore = this.getFrameStore(frameId);
727+
if ( frameStore === null ) {
728+
frameStore = this.setFrameURL(frameId, frameURL);
729+
}
730+
frameStore.clickToLoad = Date.now();
731+
}
732+
699733
shouldExceptCname(fctxt) {
700734
let exceptCname;
701735
let frameStore;
702736
if ( fctxt.docId !== undefined ) {
703-
frameStore = this.getFrame(fctxt.docId);
737+
frameStore = this.getFrameStore(fctxt.docId);
704738
if ( frameStore instanceof Object ) {
705739
exceptCname = frameStore.exceptCname;
706740
}
@@ -742,17 +776,24 @@ const PageStore = class {
742776
// content script-side (i.e. `iframes` -- unlike `img`).
743777
if ( Array.isArray(resources) && resources.length !== 0 ) {
744778
for ( const resource of resources ) {
745-
this.filterRequest(
746-
fctxt.setType(resource.type)
747-
.setURL(resource.url)
779+
const result = this.filterRequest(
780+
fctxt.setType(resource.type).setURL(resource.url)
748781
);
782+
if ( result === 1 && µb.redirectEngine.toURL(fctxt) ) {
783+
this.forgetBlockedResource(fctxt);
784+
}
749785
}
750786
}
751787
if ( this.netFilteringCache.hash === response.hash ) { return; }
752788
response.hash = this.netFilteringCache.hash;
753789
response.blockedResources =
754790
this.netFilteringCache.lookupAllBlocked(fctxt.getDocHostname());
755791
}
792+
793+
forgetBlockedResource(fctxt) {
794+
if ( this.collapsibleResources.has(fctxt.type) === false ) { return; }
795+
this.netFilteringCache.forgetResult(fctxt);
796+
}
756797
};
757798

758799
PageStore.prototype.cacheableResults = new Set([

src/js/redirect-engine.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ const redirectableResources = new Map([
6767
[ 'chartbeat.js', {
6868
alias: 'static.chartbeat.com/chartbeat.js',
6969
} ],
70+
[ 'click-to-load.html', {
71+
alias: 'clicktoload',
72+
params: [ 'url' ],
73+
} ],
7074
[ 'doubleclick_instream_ad_status.js', {
7175
alias: 'doubleclick.net/instream/ad_status.js',
7276
} ],
@@ -191,6 +195,7 @@ const RedirectEntry = class {
191195
this.mime = '';
192196
this.data = '';
193197
this.warURL = undefined;
198+
this.params = undefined;
194199
}
195200

196201
// Prevent redirection to web accessible resources when the request is
@@ -208,7 +213,15 @@ const RedirectEntry = class {
208213
fctxt instanceof Object &&
209214
fctxt.type !== 'xmlhttprequest'
210215
) {
211-
return `${this.warURL}${vAPI.warSecret()}`;
216+
let url = `${this.warURL}${vAPI.warSecret()}`;
217+
if ( this.params !== undefined ) {
218+
for ( const name of this.params ) {
219+
const value = fctxt[name];
220+
if ( value === undefined ) { continue; }
221+
url += `&${name}=${encodeURIComponent(value)}`;
222+
}
223+
}
224+
return url;
212225
}
213226
if ( this.data === undefined ) { return; }
214227
// https://github.com/uBlockOrigin/uBlock-issues/issues/701
@@ -251,6 +264,7 @@ const RedirectEntry = class {
251264
r.mime = selfie.mime;
252265
r.data = selfie.data;
253266
r.warURL = selfie.warURL;
267+
r.params = selfie.params;
254268
return r;
255269
}
256270
};
@@ -721,6 +735,7 @@ RedirectEngine.prototype.loadBuiltinResources = function() {
721735
mime: mimeFromName(name),
722736
data,
723737
warURL: vAPI.getURL(`/web_accessible_resources/${name}`),
738+
params: details.params,
724739
});
725740
this.resources.set(name, entry);
726741
if ( details.alias !== undefined ) {

0 commit comments

Comments
 (0)