@@ -56,15 +56,15 @@ const redirectableResources = new Map([
5656 [ 'addthis_widget.js' , {
5757 alias : 'addthis.com/addthis_widget.js' ,
5858 } ] ,
59+ [ 'amazon_ads.js' , {
60+ alias : 'amazon-adsystem.com/aax2/amzn_ads.js' ,
61+ } ] ,
5962 [ 'ampproject_v0.js' , {
6063 alias : 'ampproject.org/v0.js' ,
6164 } ] ,
6265 [ 'chartbeat.js' , {
6366 alias : 'static.chartbeat.com/chartbeat.js' ,
6467 } ] ,
65- [ 'amazon_ads.js' , {
66- alias : 'amazon-adsystem.com/aax2/amzn_ads.js' ,
67- } ] ,
6868 [ 'disqus_embed.js' , {
6969 alias : 'disqus.com/embed.js' ,
7070 } ] ,
@@ -74,6 +74,9 @@ const redirectableResources = new Map([
7474 [ 'doubleclick_instream_ad_status.js' , {
7575 alias : 'doubleclick.net/instream/ad_status.js' ,
7676 } ] ,
77+ [ 'empty' , {
78+ data : 'text' , // Important!
79+ } ] ,
7780 [ 'google-analytics_analytics.js' , {
7881 alias : 'google-analytics.com/analytics.js' ,
7982 } ] ,
@@ -165,6 +168,15 @@ const extToMimeMap = new Map([
165168 [ 'txt' , 'text/plain' ] ,
166169] ) ;
167170
171+ const typeToMimeMap = new Map ( [
172+ [ 'main_frame' , 'text/html' ] ,
173+ [ 'other' , 'text/plain' ] ,
174+ [ 'script' , 'application/javascript' ] ,
175+ [ 'stylesheet' , 'text/css' ] ,
176+ [ 'sub_frame' , 'text/html' ] ,
177+ [ 'xmlhttprequest' , 'text/plain' ] ,
178+ ] ) ;
179+
168180const validMimes = new Set ( extToMimeMap . values ( ) ) ;
169181
170182const mimeFromName = function ( name ) {
@@ -177,67 +189,67 @@ const mimeFromName = function(name) {
177189/******************************************************************************/
178190/******************************************************************************/
179191
180- const RedirectEntry = function ( ) {
181- this . mime = '' ;
182- this . data = '' ;
183- this . warURL = undefined ;
184- } ;
185-
186- /******************************************************************************/
187-
188- // Prevent redirection to web accessible resources when the request is
189- // of type 'xmlhttprequest', because XMLHttpRequest.responseURL would
190- // cause leakage of extension id. See:
191- // - https://stackoverflow.com/a/8056313
192- // - https://bugzilla.mozilla.org/show_bug.cgi?id=998076
193-
194- RedirectEntry . prototype . toURL = function ( fctxt , asDataURI = false ) {
195- if (
196- this . warURL !== undefined &&
197- asDataURI !== true &&
198- fctxt instanceof Object &&
199- fctxt . type !== 'xmlhttprequest'
200- ) {
201- return `${ this . warURL } ${ vAPI . warSecret ( ) } ` ;
202- }
203- if ( this . data === undefined ) { return ; }
204- if ( this . data . startsWith ( 'data:' ) === false ) {
205- this . data = `data:${ this . mime } ;base64,${ btoa ( this . data ) } ` ;
206- }
207- return this . data ;
208- } ;
192+ const RedirectEntry = class {
193+ constructor ( ) {
194+ this . mime = '' ;
195+ this . data = '' ;
196+ this . warURL = undefined ;
197+ }
209198
210- /******************************************************************************/
199+ // Prevent redirection to web accessible resources when the request is
200+ // of type 'xmlhttprequest', because XMLHttpRequest.responseURL would
201+ // cause leakage of extension id. See:
202+ // - https://stackoverflow.com/a/8056313
203+ // - https://bugzilla.mozilla.org/show_bug.cgi?id=998076
211204
212- RedirectEntry . prototype . toContent = function ( ) {
213- if ( this . data . startsWith ( 'data:' ) ) {
214- const pos = this . data . indexOf ( ',' ) ;
215- const base64 = this . data . endsWith ( ';base64' , pos ) ;
216- this . data = this . data . slice ( pos + 1 ) ;
217- if ( base64 ) {
218- this . data = atob ( this . data ) ;
205+ toURL ( fctxt , asDataURI = false ) {
206+ if (
207+ this . warURL !== undefined &&
208+ asDataURI !== true &&
209+ fctxt instanceof Object &&
210+ fctxt . type !== 'xmlhttprequest'
211+ ) {
212+ return `${ this . warURL } ${ vAPI . warSecret ( ) } ` ;
213+ }
214+ if ( this . data === undefined ) { return ; }
215+ // https://github.com/uBlockOrigin/uBlock-issues/issues/701
216+ if ( this . data === '' ) {
217+ const mime = typeToMimeMap . get ( fctxt . type ) ;
218+ if ( mime === undefined ) { return ; }
219+ return `data:${ mime } ,` ;
220+ }
221+ if ( this . data . startsWith ( 'data:' ) === false ) {
222+ this . data = `data:${ this . mime } ;base64,${ btoa ( this . data ) } ` ;
219223 }
224+ return this . data ;
220225 }
221- return this . data ;
222- } ;
223-
224- /******************************************************************************/
225226
226- RedirectEntry . fromContent = function ( mime , content ) {
227- const r = new RedirectEntry ( ) ;
228- r . mime = mime ;
229- r . data = content ;
230- return r ;
231- } ;
227+ toContent ( ) {
228+ if ( this . data . startsWith ( 'data:' ) ) {
229+ const pos = this . data . indexOf ( ',' ) ;
230+ const base64 = this . data . endsWith ( ';base64' , pos ) ;
231+ this . data = this . data . slice ( pos + 1 ) ;
232+ if ( base64 ) {
233+ this . data = atob ( this . data ) ;
234+ }
235+ }
236+ return this . data ;
237+ }
232238
233- /******************************************************************************/
239+ static fromContent ( mime , content ) {
240+ const r = new RedirectEntry ( ) ;
241+ r . mime = mime ;
242+ r . data = content ;
243+ return r ;
244+ }
234245
235- RedirectEntry . fromSelfie = function ( selfie ) {
236- const r = new RedirectEntry ( ) ;
237- r . mime = selfie . mime ;
238- r . data = selfie . data ;
239- r . warURL = selfie . warURL ;
240- return r ;
246+ static fromSelfie ( selfie ) {
247+ const r = new RedirectEntry ( ) ;
248+ r . mime = selfie . mime ;
249+ r . data = selfie . data ;
250+ r . warURL = selfie . warURL ;
251+ return r ;
252+ }
241253} ;
242254
243255/******************************************************************************/
@@ -248,7 +260,13 @@ const RedirectEngine = function() {
248260 this . resources = new Map ( ) ;
249261 this . reset ( ) ;
250262 this . resourceNameRegister = '' ;
251- this . _desAll = [ ] ; // re-use better than re-allocate
263+
264+ // Internal use
265+ this . _missedQueryHash = '' ;
266+ this . _src = '' ;
267+ this . _srcAll = [ '*' ] ;
268+ this . _des = '' ;
269+ this . _desAll = [ '*' ] ;
252270} ;
253271
254272/******************************************************************************/
@@ -278,35 +296,60 @@ RedirectEngine.prototype.toBroaderHostname = function(hostname) {
278296
279297/******************************************************************************/
280298
281- RedirectEngine . prototype . lookup = function ( fctxt ) {
282- const type = fctxt . type ;
283- if ( this . ruleTypes . has ( type ) === false ) { return ; }
284- const desAll = this . _desAll ;
285- const reqURL = fctxt . url ;
286- let src = fctxt . getDocHostname ( ) ;
287- let des = fctxt . getHostname ( ) ;
288- let n = 0 ;
299+ RedirectEngine . prototype . decomposeHostname = function ( hn , dict , out ) {
300+ let i = 0 ;
289301 for ( ; ; ) {
290- if ( this . ruleDestinations . has ( des ) ) {
291- desAll [ n ] = des ; n += 1 ;
302+ if ( dict . has ( hn ) ) {
303+ out [ i ] = hn ; i += 1 ;
292304 }
293- des = this . toBroaderHostname ( des ) ;
294- if ( des === '' ) { break ; }
305+ hn = this . toBroaderHostname ( hn ) ;
306+ if ( hn === '' ) { break ; }
295307 }
296- if ( n === 0 ) { return ; }
297- for ( ; ; ) {
298- if ( this . ruleSources . has ( src ) ) {
299- for ( let i = 0 ; i < n ; i ++ ) {
300- const entries = this . rules . get ( `${ src } ${ desAll [ i ] } ${ type } ` ) ;
301- if ( entries === undefined ) { continue ; }
308+ out . length = i ;
309+ } ;
310+
311+ /******************************************************************************/
312+
313+ RedirectEngine . prototype . lookup = function ( fctxt ) {
314+ const src = fctxt . getDocHostname ( ) ;
315+ const des = fctxt . getHostname ( ) ;
316+ const type = fctxt . type ;
317+ const queryHash = `${ src } ${ des } ${ type } ` ;
318+ if ( queryHash === this . _missedQueryHash ) {
319+ return ;
320+ }
321+ if ( src !== this . _src ) {
322+ this . _src = src ;
323+ this . decomposeHostname ( src , this . ruleSources , this . _srcAll ) ;
324+ }
325+ if ( this . _srcAll . length === 0 ) {
326+ this . _missedQueryHash = queryHash ;
327+ return ;
328+ }
329+ if ( des !== this . _des ) {
330+ this . _des = des ;
331+ this . decomposeHostname ( des , this . ruleDestinations , this . _desAll ) ;
332+ }
333+ if ( this . _desAll . length === 0 ) {
334+ this . _missedQueryHash = queryHash ;
335+ return ;
336+ }
337+ const reqURL = fctxt . url ;
338+ for ( const src of this . _srcAll ) {
339+ for ( const des of this . _desAll ) {
340+ let entries = this . rules . get ( `${ src } ${ des } ${ type } ` ) ;
341+ if ( entries !== undefined ) {
302342 const rule = this . lookupRule ( entries , reqURL ) ;
303- if ( rule === undefined ) { continue ; }
304- return rule ;
343+ if ( rule !== undefined ) { return rule ; }
344+ }
345+ entries = this . rules . get ( `${ src } ${ des } *` ) ;
346+ if ( entries !== undefined ) {
347+ const rule = this . lookupRule ( entries , reqURL ) ;
348+ if ( rule !== undefined ) { return rule ; }
305349 }
306350 }
307- src = this . toBroaderHostname ( src ) ;
308- if ( src === '' ) { break ; }
309351 }
352+ this . _missedQueryHash = queryHash ;
310353} ;
311354
312355RedirectEngine . prototype . lookupRule = function ( entries , reqURL ) {
@@ -428,6 +471,10 @@ RedirectEngine.prototype.compileRuleFromStaticFilter = function(line) {
428471 redirect = option . slice ( 14 ) ;
429472 continue ;
430473 }
474+ if ( option === 'empty' ) {
475+ redirect = 'empty' ;
476+ continue ;
477+ }
431478 if ( option . startsWith ( 'domain=' ) ) {
432479 srchns = option . slice ( 7 ) . split ( '|' ) ;
433480 continue ;
@@ -448,7 +495,10 @@ RedirectEngine.prototype.compileRuleFromStaticFilter = function(line) {
448495 if ( redirect === '' ) { return ; }
449496
450497 // Need one single type -- not negated.
451- if ( type === undefined ) { return ; }
498+ if ( type === undefined ) {
499+ if ( redirect !== 'empty' ) { return ; }
500+ type = '*' ;
501+ }
452502
453503 if ( deshn === '' ) {
454504 deshn = '*' ;
@@ -649,12 +699,12 @@ const removeTopCommentBlock = function(text) {
649699/******************************************************************************/
650700
651701RedirectEngine . prototype . loadBuiltinResources = function ( ) {
652- this . resources = new Map ( ) ;
653- this . aliases = new Map ( ) ;
654-
655702 // TODO: remove once usage of uBO 1.20.4 is widespread.
656703 µBlock . assets . remove ( 'ublock-resources' ) ;
657704
705+ this . resources = new Map ( ) ;
706+ this . aliases = new Map ( ) ;
707+
658708 const fetches = [
659709 µBlock . assets . fetchText (
660710 '/assets/resources/scriptlets.js'
0 commit comments