From 59169209850c54c31d94990f0c956281fe43eb03 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Fri, 9 Oct 2020 13:50:54 -0400 Subject: [PATCH] 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 --- src/css/click-to-load.css | 48 +++++++++++++ src/css/themes/default.css | 2 + src/js/click-to-load.js | 63 +++++++++++++++++ src/js/filtering-context.js | 16 +++-- src/js/messaging.js | 15 +++- src/js/pagestore.js | 69 +++++++++++++++---- src/js/redirect-engine.js | 17 ++++- src/js/traffic.js | 5 +- .../click-to-load.html | 25 +++++++ 9 files changed, 238 insertions(+), 22 deletions(-) create mode 100644 src/css/click-to-load.css create mode 100644 src/js/click-to-load.js create mode 100644 src/web_accessible_resources/click-to-load.html diff --git a/src/css/click-to-load.css b/src/css/click-to-load.css new file mode 100644 index 0000000000000..e6e4d77fdd41f --- /dev/null +++ b/src/css/click-to-load.css @@ -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; + } diff --git a/src/css/themes/default.css b/src/css/themes/default.css index 8ba229500a0a5..00a2d10100213 100644 --- a/src/css/themes/default.css +++ b/src/css/themes/default.css @@ -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); diff --git a/src/js/click-to-load.js b/src/js/click-to-load.js new file mode 100644 index 0000000000000..b43e5cdb1d31f --- /dev/null +++ b/src/js/click-to-load.js @@ -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); + } + }); +}); + +/******************************************************************************/ + +})(); diff --git a/src/js/filtering-context.js b/src/js/filtering-context.js index 9623723059ea8..17f46b1226be3 100644 --- a/src/js/filtering-context.js +++ b/src/js/filtering-context.js @@ -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; diff --git a/src/js/messaging.js b/src/js/messaging.js index 89d1b2c960d32..44c4e810fdfdc 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -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; diff --git a/src/js/pagestore.js b/src/js/pagestore.js index c086352bee82d..f5cd7dd9c1675 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -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([ diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index fa3f26db02dd2..71ac4b0aa7602 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -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 ) { diff --git a/src/js/traffic.js b/src/js/traffic.js index 7079c3efb2264..fb1a4071bbb13 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -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 }) diff --git a/src/web_accessible_resources/click-to-load.html b/src/web_accessible_resources/click-to-load.html new file mode 100644 index 0000000000000..f8cfa99b0983a --- /dev/null +++ b/src/web_accessible_resources/click-to-load.html @@ -0,0 +1,25 @@ + + + + + +uBlock Origin Click-to-Load + + + + + + + + +Click to load +
+ + + + + + + + +