@@ -5,7 +5,13 @@ const log = false;
55// globals
66const extensionNewTabPath = 'app.html' ;
77
8- // TODO: this also exists in options.js and could be moved to a separate helper file
8+ // TODO: this also exists in options.js and could be moved to a separate helper file.
9+ const removeIframeHeadersPermissions = [
10+ 'webRequest' ,
11+ 'webRequestBlocking' ,
12+ ] ;
13+
14+ // TODO: this also exists in options.js and could be moved to a separate helper file.
915const forceOpenInTopFramePermissions = [
1016 'webRequest' ,
1117 'webRequestBlocking' ,
@@ -15,13 +21,14 @@ const forceOpenInTopFramePermissions = [
1521// state
1622let options = {
1723 customNewTabUrl : '' ,
24+ removeIframeHeaders : false ,
1825 forceOpenInTopFrame : false ,
1926} ;
2027
2128
2229// Firefox API helpers
2330
24- // TODO: this also exists in options.js and could be moved to a separate helper file
31+ // TODO: this also exists in options.js and could be moved to a separate helper file.
2532const toMatchPattern = urlStr => {
2633 try {
2734 // Match patterns without paths must have a trailing slash.
@@ -34,30 +41,53 @@ const toMatchPattern = urlStr => {
3441 }
3542} ;
3643
44+ // TODO: this also exists in options.js and could be moved to a separate helper file.
45+ // Creates a matcher for any page on a given domain.
46+ // toWildcardDomainMatchPattern('https://example.com') // -> 'https://*.example.com/*'
47+ // toWildcardDomainMatchPattern('https://example.com:3000/foo/bar.html') // -> 'https://*.example.com:3000/*'
48+ const toWildcardDomainMatchPattern = urlStr => {
49+ try {
50+ const url = new URL ( urlStr ) ;
51+ const pattern = `${ url . protocol } //*.${ url . host } /*` ;
52+
53+ log && console . debug ( '#toWildcardDomainMatchPattern' , { pattern } ) ;
54+ return pattern ;
55+ } catch ( err ) {
56+ console . error ( '#toWildcardDomainMatchPattern' , err ) ;
57+ return false ;
58+ }
59+ } ;
60+
3761
3862const updateOptionsCache = opts => { options = Object . assign ( { } , options , opts ) ; } ;
3963
40- const refreshOptionsCache = async _ => await browser . storage . sync . get ( [ 'customNewTabUrl' , 'forceOpenInTopFrame' ] ) . then ( updateOptionsCache ) ;
64+ const refreshOptionsCache = async _ =>
65+ await browser . storage . sync . get ( [
66+ 'customNewTabUrl' ,
67+ 'removeIframeHeaders' ,
68+ 'forceOpenInTopFrame' ,
69+ ] ) . then ( updateOptionsCache ) ;
4170
4271const customNewTabUrlExists = _ => ( options . customNewTabUrl && options . customNewTabUrl . length !== 0 ) ;
4372
44- const applyFilter = details => {
45- log && console . debug ( '#applyFilter' , options , details ) ;
73+
74+ const forceOpenInTopFrameFilter = details => {
75+ log && console . debug ( '#forceOpenInTopFrameFilter' , options , details ) ;
4676
4777 if ( ! options . forceOpenInTopFrame ) {
4878 // Dont modify requests if the option is not enabled.
49- log && console . debug ( '#applyFilter // forceOpenInTopFrame option not enabled... skipping' ) ;
79+ log && console . debug ( '#forceOpenInTopFrameFilter // forceOpenInTopFrame option not enabled... skipping' ) ;
5080 return false ;
5181 }
5282
5383 if ( details . originUrl !== browser . extension . getURL ( extensionNewTabPath ) ) {
5484 // Don't modify requests outside of the extension new tab page.
5585 // This is still needed because the `customNewTabUrl` scope used in the onBeforeRequest listener doesnt guarantee the request is coming from this extension.
56- log && console . debug ( '#applyFilter // outside of extension new tab page... skipping' ) ;
86+ log && console . debug ( '#forceOpenInTopFrameFilter // outside of extension new tab page... skipping' ) ;
5787 return false ;
5888 }
5989
60- log && console . debug ( '#applyFilter // modifying' , { url : details . url , originUrl : details . originUrl } ) ;
90+ log && console . debug ( '#forceOpenInTopFrameFilter // modifying' , { url : details . url , originUrl : details . originUrl } ) ;
6191
6292 const decoder = new TextDecoder ( 'utf-8' ) ;
6393 const encoder = new TextEncoder ( ) ;
@@ -77,23 +107,70 @@ const applyFilter = details => {
77107 return true ;
78108} ;
79109
110+ const removeIframeHeadersFilter = details => {
111+ log && console . debug ( '#removeIframeHeadersFilter' , options , details ) ;
112+
113+ if ( ! options . removeIframeHeaders ) {
114+ // Dont modify requests if the option is not enabled.
115+ log && console . debug ( '#removeIframeHeadersFilter // removeIframeHeaders option not enabled... skipping' ) ;
116+ return false ;
117+ }
118+
119+ if ( details . originUrl !== browser . extension . getURL ( extensionNewTabPath ) ) {
120+ // Don't modify requests outside of the extension new tab page.
121+ // This is still needed because the `customNewTabUrl` scope used in the onHeadersReceived listener doesnt guarantee the request is coming from this extension.
122+ log && console . debug ( '#removeIframeHeadersFilter // outside of extension new tab page... skipping' ) ;
123+ return false ;
124+ }
125+
126+ log && console . debug ( '#removeIframeHeadersFilter // modifying' , { url : details . url , originUrl : details . originUrl } ) ;
127+
128+ const responseHeaders = details . responseHeaders . filter ( header => {
129+ return header . name . toLowerCase ( ) !== 'x-frame-options' ;
130+ } ) ;
131+
132+ log && console . debug ( '#removeIframeHeadersFilter // new response headers' , responseHeaders ) ;
133+
134+ // NOTE: changes made here will **not** show up in the browser DevTools network tab.
135+ return { responseHeaders } ;
136+ } ;
137+
138+
139+ const onBeforeRequestListener = async details => {
140+ try {
141+ return forceOpenInTopFrameFilter ( details ) ;
142+ } catch ( err ) {
143+ console . error ( '#onBeforeRequestListener' , err ) ;
144+ return false ;
145+ }
146+ } ;
147+
148+
149+ const onBeforeRequestListenerCleanup = _ => {
150+ const hasListener = browser . webRequest . onBeforeRequest . hasListener ( onBeforeRequestListener ) ;
151+ log && console . debug ( '#onBeforeRequestListenerCleanup' , { hasListener } ) ;
80152
81- const listener = async details => {
153+ if ( hasListener ) {
154+ browser . webRequest . onBeforeRequest . removeListener ( onBeforeRequestListener ) ;
155+ }
156+ } ;
157+
158+ const onHeadersReceivedListener = async details => {
82159 try {
83- return applyFilter ( details ) ;
160+ return removeIframeHeadersFilter ( details ) ;
84161 } catch ( err ) {
85- console . error ( '#listener ' , err ) ;
162+ console . error ( '#onHeadersReceivedListener ' , err ) ;
86163 return false ;
87164 }
88165} ;
89166
90167
91- const removeRequestListener = _ => {
92- const hasListener = browser . webRequest . onBeforeRequest . hasListener ( listener ) ;
93- log && console . debug ( '#removeRequestListener ' , { hasListener } ) ;
168+ const onHeadersReceivedListenerCleanup = _ => {
169+ const hasListener = browser . webRequest . onHeadersReceived . hasListener ( onHeadersReceivedListener ) ;
170+ log && console . debug ( '#onHeadersReceivedListenerCleanup ' , { hasListener } ) ;
94171
95172 if ( hasListener ) {
96- browser . webRequest . onBeforeRequest . removeListener ( listener ) ;
173+ browser . webRequest . onHeadersReceived . removeListener ( onHeadersReceivedListener ) ;
97174 }
98175} ;
99176
@@ -104,37 +181,69 @@ const addRequestListener = _ => {
104181 return false ;
105182 }
106183
107- const customNewTabUrlMatchPattern = toMatchPattern ( options . customNewTabUrl ) ;
108- log && console . debug ( '#addRequestListener' , { customNewTabUrlMatchPattern } ) ;
109-
110- if ( ! customNewTabUrlMatchPattern ) {
111- return false ;
184+ // TODO: this logic is duplicated in options.js and should be centralized somewhere.
185+ const forceOpenInTopFrameCustomNewTabUrlMatchPattern = toMatchPattern ( options . customNewTabUrl ) ;
186+ log && console . debug ( '#addRequestListener' , { forceOpenInTopFrameCustomNewTabUrlMatchPattern } ) ;
187+
188+ onBeforeRequestListenerCleanup ( ) ;
189+
190+ if ( forceOpenInTopFrameCustomNewTabUrlMatchPattern ) {
191+ browser . webRequest . onBeforeRequest . addListener (
192+ onBeforeRequestListener ,
193+ {
194+ urls : [
195+ browser . extension . getURL ( extensionNewTabPath ) ,
196+ forceOpenInTopFrameCustomNewTabUrlMatchPattern ,
197+ ] ,
198+ types : [ 'sub_frame' ] ,
199+ } ,
200+ [ 'blocking' ] ,
201+ ) ;
112202 }
113203
114- // clean up old listeners
115- removeRequestListener ( ) ;
116-
117- browser . webRequest . onBeforeRequest . addListener (
118- listener ,
119- {
120- urls : [
121- browser . extension . getURL ( extensionNewTabPath ) ,
122- customNewTabUrlMatchPattern ,
123- ] ,
124- types : [ 'sub_frame' ] ,
125- } ,
126- [ 'blocking' ] ,
127- ) ;
204+ // TODO: this logic is duplicated in options.js and should be centralized somewhere.
205+ const removeIframeHeadersCustomNewTabUrlMatchPattern = toWildcardDomainMatchPattern ( options . customNewTabUrl ) ;
206+ log && console . debug ( '#addRequestListener' , { removeIframeHeadersCustomNewTabUrlMatchPattern } ) ;
207+
208+ onHeadersReceivedListenerCleanup ( ) ;
209+
210+ if ( removeIframeHeadersCustomNewTabUrlMatchPattern ) {
211+ browser . webRequest . onHeadersReceived . addListener (
212+ onHeadersReceivedListener ,
213+ {
214+ urls : [
215+ browser . extension . getURL ( extensionNewTabPath ) ,
216+ removeIframeHeadersCustomNewTabUrlMatchPattern ,
217+ ] ,
218+ types : [ 'sub_frame' ] ,
219+ } ,
220+ [ 'blocking' , 'responseHeaders' ] ,
221+ ) ;
222+ }
128223
129224 return true ;
130225} ;
131226
132227
228+ // TODO: this logic is duplicated in options.js and should be centralized somewhere.
133229const hasPermissions = async _ => {
134230 try {
231+ let permissions = [ ] ;
232+ let origins = [ ] ;
233+
234+ if ( options . removeIframeHeaders ) {
235+ permissions = permissions . concat ( removeIframeHeadersPermissions ) ;
236+ origins = origins . concat ( toWildcardDomainMatchPattern ( options . customNewTabUrl ) ) ;
237+ }
238+
239+ if ( options . forceOpenInTopFrame ) {
240+ permissions = permissions . concat ( forceOpenInTopFramePermissions ) ;
241+ origins = origins . concat ( toMatchPattern ( options . customNewTabUrl ) ) ;
242+ }
243+
135244 const requiredPermissions = {
136- permissions : forceOpenInTopFramePermissions ,
137- origins : [ toMatchPattern ( options . customNewTabUrl ) ] ,
245+ permissions,
246+ origins,
138247 } ;
139248
140249 const hasPermissions_ = await browser . permissions . contains ( requiredPermissions ) ;
@@ -155,7 +264,7 @@ const init = async _ => {
155264 await refreshOptionsCache ( ) ;
156265 log && console . debug ( '#init // got options' , options ) ;
157266
158- if ( options . forceOpenInTopFrame && customNewTabUrlExists ( ) && await hasPermissions ( ) ) {
267+ if ( ( options . removeIframeHeaders || options . forceOpenInTopFrame ) && customNewTabUrlExists ( ) && await hasPermissions ( ) ) {
159268 log && console . debug ( '#init // has permissions, url exists, option set -- adding request listener' ) ;
160269 addRequestListener ( ) ;
161270 } else {
0 commit comments