diff --git a/platform/chromium/vapi-background-ext.js b/platform/chromium/vapi-background-ext.js index 29de305846647..9ee69d8199ae5 100644 --- a/platform/chromium/vapi-background-ext.js +++ b/platform/chromium/vapi-background-ext.js @@ -19,10 +19,6 @@ Home: https://github.com/gorhill/uBlock */ -/* globals browser */ - -'use strict'; - /******************************************************************************/ // https://github.com/uBlockOrigin/uBlock-issues/issues/1659 @@ -90,71 +86,47 @@ vAPI.Tabs = class extends vAPI.Tabs { ['gif','image'],['ico','image'],['jpeg','image'],['jpg','image'],['png','image'],['webp','image'] ]); - const headerValue = (headers, name) => { - let i = headers.length; - while ( i-- ) { - if ( headers[i].name.toLowerCase() === name ) { - return headers[i].value.trim(); - } - } - return ''; - }; - const parsedURL = new URL('https://www.example.org/'); - // Extend base class to normalize as per platform. + // Extend base class to normalize as per platform vAPI.Net = class extends vAPI.Net { normalizeDetails(details) { // Chromium 63+ supports the `initiator` property, which contains - // the URL of the origin from which the network request was made. - if ( - typeof details.initiator === 'string' && - details.initiator !== 'null' - ) { + // the URL of the origin from which the network request was made + if ( details.initiator && details.initiator !== 'null' ) { details.documentUrl = details.initiator; } - - let type = details.type; - + const type = details.type; if ( type === 'imageset' ) { details.type = 'image'; return; } - - // The rest of the function code is to normalize type if ( type !== 'other' ) { return; } - - // Try to map known "extension" part of URL to request type. - parsedURL.href = details.url; - const path = parsedURL.pathname, - pos = path.indexOf('.', path.length - 6); - if ( pos !== -1 && (type = extToTypeMap.get(path.slice(pos + 1))) ) { - details.type = type; + // Try to map known "extension" part of URL to request type + if ( details.responseHeaders === undefined ) { + parsedURL.href = details.url; + const path = parsedURL.pathname; + const pos = path.indexOf('.', path.length - 6); + if ( pos !== -1 ) { + details.type = extToTypeMap.get(path.slice(pos + 1)) || type; + } return; } - - // Try to extract type from response headers if present. - if ( details.responseHeaders ) { - type = headerValue(details.responseHeaders, 'content-type'); - if ( type.startsWith('font/') ) { - details.type = 'font'; - return; - } - if ( type.startsWith('image/') ) { - details.type = 'image'; - return; - } - if ( type.startsWith('audio/') || type.startsWith('video/') ) { - details.type = 'media'; - return; - } + // Try to extract type from response headers + const ctype = this.headerValue(details.responseHeaders, 'content-type'); + if ( ctype.startsWith('font/') ) { + details.type = 'font'; + } else if ( ctype.startsWith('image/') ) { + details.type = 'image'; + } else if ( ctype.startsWith('audio/') || ctype.startsWith('video/') ) { + details.type = 'media'; } } // https://www.reddit.com/r/uBlockOrigin/comments/9vcrk3/ // Some types can be mapped from 'other', thus include 'other' if and - // only if the caller is interested in at least one of those types. + // only if the caller is interested in at least one of those types denormalizeTypes(types) { if ( types.length === 0 ) { return Array.from(this.validTypes); diff --git a/platform/common/vapi-background.js b/platform/common/vapi-background.js index 26507982a51e9..fac5121c00082 100644 --- a/platform/common/vapi-background.js +++ b/platform/common/vapi-background.js @@ -1382,6 +1382,14 @@ vAPI.Net = class { if ( this.suspendDepth !== 0 ) { return; } this.unsuspendAllRequests(discard); } + headerValue(headers, name) { + for ( const header of headers ) { + if ( header.name.toLowerCase() === name ) { + return header.value.trim(); + } + } + return ''; + } static canSuspend() { return false; } diff --git a/platform/firefox/vapi-background-ext.js b/platform/firefox/vapi-background-ext.js index 73a914d0ce650..a6032066d9258 100644 --- a/platform/firefox/vapi-background-ext.js +++ b/platform/firefox/vapi-background-ext.js @@ -110,18 +110,15 @@ vAPI.Net = class extends vAPI.Net { details.type = 'image'; return; } + if ( type !== 'object' ) { return; } + // Try to extract type from response headers if present. + if ( details.responseHeaders === undefined ) { return; } + const ctype = this.headerValue(details.responseHeaders, 'content-type'); // https://github.com/uBlockOrigin/uBlock-issues/issues/345 // Re-categorize an embedded object as a `sub_frame` if its // content type is that of a HTML document. - if ( type === 'object' && Array.isArray(details.responseHeaders) ) { - for ( const header of details.responseHeaders ) { - if ( header.name.toLowerCase() === 'content-type' ) { - if ( header.value.startsWith('text/html') ) { - details.type = 'sub_frame'; - } - break; - } - } + if ( ctype === 'text/html' ) { + details.type = 'sub_frame'; } } diff --git a/src/js/pagestore.js b/src/js/pagestore.js index 319367fe51b0d..1b94b1fef6d95 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -1019,48 +1019,51 @@ const PageStore = class { } // The caller is responsible to check whether filtering is enabled or not. - filterLargeMediaElement(fctxt, size) { + filterLargeMediaElement(fctxt, headers) { fctxt.filter = undefined; - - if ( this.allowLargeMediaElementsUntil === 0 ) { + if ( this.allowLargeMediaElementsUntil === 0 ) { return 0; } + if ( sessionSwitches.evaluateZ('no-large-media', fctxt.getTabHostname() ) !== true ) { + 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) - ) { + // XHR-based streaming is never blocked but we want to prevent autoplay + if ( fctxt.itype === fctxt.XMLHTTPREQUEST ) { + const ctype = headers.contentType; + if ( ctype.startsWith('audio/') || ctype.startsWith('video/') ) { + this.largeMediaTimer.on(500); + } 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('|')); + if ( fctxt.itype === fctxt.MEDIA ) { + const sources = this.allowLargeMediaElementsRegex instanceof RegExp + ? [ this.allowLargeMediaElementsRegex.source ] + : []; + sources.push('^' + µb.escapeRegex(fctxt.url)); + this.allowLargeMediaElementsRegex = new RegExp(sources.join('|')); + } return 0; } + // Disregard large media elements previously allowed: for example, to + // seek inside a previously allowed audio/video. if ( - sessionSwitches.evaluateZ( - 'no-large-media', - fctxt.getTabHostname() - ) !== true + this.allowLargeMediaElementsRegex instanceof RegExp && + this.allowLargeMediaElementsRegex.test(fctxt.url) ) { - this.allowLargeMediaElementsUntil = 0; return 0; } - if ( (size >>> 10) < µb.userSettings.largeMediaSize ) { - return 0; + // Regardless of whether a media is blocked, we want to prevent autoplay + if ( fctxt.itype === fctxt.MEDIA ) { + this.largeMediaTimer.on(500); } - + const size = headers.contentLength; + if ( isNaN(size) ) { 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; } diff --git a/src/js/scriptlets/load-large-media-interactive.js b/src/js/scriptlets/load-large-media-interactive.js index 4887616d9b0f9..6158124726c70 100644 --- a/src/js/scriptlets/load-large-media-interactive.js +++ b/src/js/scriptlets/load-large-media-interactive.js @@ -28,8 +28,6 @@ if ( typeof vAPI !== 'object' || vAPI.loadAllLargeMedia instanceof Function ) { return; } -/******************************************************************************/ - const largeMediaElementAttribute = 'data-' + vAPI.sessionId; const largeMediaElementSelector = ':root audio[' + largeMediaElementAttribute + '],\n' + @@ -37,25 +35,19 @@ const largeMediaElementSelector = ':root picture[' + largeMediaElementAttribute + '],\n' + ':root video[' + largeMediaElementAttribute + ']'; -/******************************************************************************/ +const isMediaElement = elem => + (/^(?:audio|img|picture|video)$/.test(elem.localName)); -const isMediaElement = function(elem) { - return /^(?:audio|img|picture|video)$/.test(elem.localName); -}; +const isPlayableMediaElement = elem => + (/^(?:audio|video)$/.test(elem.localName)); /******************************************************************************/ const mediaNotLoaded = function(elem) { switch ( elem.localName ) { case 'audio': - case 'video': { - const src = elem.src || ''; - if ( src.startsWith('blob:') ) { - elem.autoplay = false; - elem.pause(); - } + case 'video': return elem.readyState === 0 || elem.error !== null; - } case 'img': { if ( elem.naturalWidth !== 0 || elem.naturalHeight !== 0 ) { break; @@ -99,29 +91,29 @@ const surveyMissingMediaElements = function() { return largeMediaElementCount; }; -if ( surveyMissingMediaElements() === 0 ) { return; } - -// Insert CSS to highlight blocked media elements. -if ( vAPI.largeMediaElementStyleSheet === undefined ) { - vAPI.largeMediaElementStyleSheet = [ - largeMediaElementSelector + ' {', - 'border: 2px dotted red !important;', - 'box-sizing: border-box !important;', - 'cursor: zoom-in !important;', - 'display: inline-block;', - 'filter: none !important;', - 'font-size: 1rem !important;', - 'min-height: 1em !important;', - 'min-width: 1em !important;', - 'opacity: 1 !important;', - 'outline: none !important;', - 'transform: none !important;', - 'visibility: visible !important;', - 'z-index: 2147483647', - '}', - ].join('\n'); - vAPI.userStylesheet.add(vAPI.largeMediaElementStyleSheet); - vAPI.userStylesheet.apply(); +if ( surveyMissingMediaElements() ) { + // Insert CSS to highlight blocked media elements. + if ( vAPI.largeMediaElementStyleSheet === undefined ) { + vAPI.largeMediaElementStyleSheet = [ + largeMediaElementSelector + ' {', + 'border: 2px dotted red !important;', + 'box-sizing: border-box !important;', + 'cursor: zoom-in !important;', + 'display: inline-block;', + 'filter: none !important;', + 'font-size: 1rem !important;', + 'min-height: 1em !important;', + 'min-width: 1em !important;', + 'opacity: 1 !important;', + 'outline: none !important;', + 'transform: none !important;', + 'visibility: visible !important;', + 'z-index: 2147483647', + '}', + ].join('\n'); + vAPI.userStylesheet.add(vAPI.largeMediaElementStyleSheet); + vAPI.userStylesheet.apply(); + } } /******************************************************************************/ @@ -258,6 +250,27 @@ document.addEventListener('error', onLoadError, true); /******************************************************************************/ +const autoPausedMedia = new WeakMap(); + +for ( const elem of document.querySelectorAll('audio,video') ) { + elem.setAttribute('autoplay', 'false'); +} + +const preventAutoplay = function(ev) { + const elem = ev.target; + if ( isPlayableMediaElement(elem) === false ) { return; } + const currentSrc = elem.getAttribute('src') || ''; + const pausedSrc = autoPausedMedia.get(elem); + if ( pausedSrc === currentSrc ) { return; } + autoPausedMedia.set(elem, currentSrc); + elem.setAttribute('autoplay', 'false'); + elem.pause(); +}; + +document.addEventListener('timeupdate', preventAutoplay, true); + +/******************************************************************************/ + vAPI.loadAllLargeMedia = function() { document.removeEventListener('click', onMouseClick, true); document.removeEventListener('loadeddata', onLoadedData, true); diff --git a/src/js/traffic.js b/src/js/traffic.js index 55538fa7e425e..5de002797671e 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -488,7 +488,7 @@ const onHeadersReceived = function(details) { } if ( pageStore.getNetFilteringSwitch(fctxt) === false ) { return; } - if ( fctxt.itype === fctxt.IMAGE || fctxt.itype === fctxt.MEDIA ) { + if ( (fctxt.itype & foilLargeMediaElement.TYPE_BITS) !== 0 ) { const result = foilLargeMediaElement(details, fctxt, pageStore); if ( result !== undefined ) { return result; } } @@ -1124,15 +1124,12 @@ const injectPP = function(fctxt, pageStore, responseHeaders) { const foilLargeMediaElement = function(details, fctxt, pageStore) { if ( details.fromCache === true ) { return; } - let size = 0; - if ( µb.userSettings.largeMediaSize !== 0 ) { - const headers = details.responseHeaders; - const i = headerIndexFromName('content-length', headers); - if ( i === -1 ) { return; } - size = parseInt(headers[i].value, 10) || 0; - } + onDemandHeaders.setHeaders(details.responseHeaders); + + const result = pageStore.filterLargeMediaElement(fctxt, onDemandHeaders); + + onDemandHeaders.reset(); - const result = pageStore.filterLargeMediaElement(fctxt, size); if ( result === 0 ) { return; } if ( logger.enabled ) { @@ -1142,16 +1139,15 @@ const foilLargeMediaElement = function(details, fctxt, pageStore) { return { cancel: true }; }; +foilLargeMediaElement.TYPE_BITS = fc.IMAGE | fc.MEDIA | fc.XMLHTTPREQUEST; + /******************************************************************************/ // Caller must ensure headerName is normalized to lower case. const headerIndexFromName = function(headerName, headers) { - let i = headers.length; - while ( i-- ) { - if ( headers[i].name.toLowerCase() === headerName ) { - return i; - } + for ( let i = 0, n = headers.length; i < n; i++ ) { + if ( headers[i].name.toLowerCase() === headerName ) { return i; } } return -1; }; @@ -1161,6 +1157,24 @@ const headerValueFromName = function(headerName, headers) { return i !== -1 ? headers[i].value : ''; }; +const onDemandHeaders = { + headers: [], + get contentLength() { + const contentLength = headerValueFromName('content-length', this.headers); + if ( contentLength === '' ) { return Number.NaN; } + return parseInt(contentLength, 10) || 0; + }, + get contentType() { + return headerValueFromName('content-type', this.headers); + }, + setHeaders(headers) { + this.headers = headers; + }, + reset() { + this.headers = []; + } +}; + /******************************************************************************/ const strictBlockBypasser = {