custom_functions.js (5957B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /* globals browser */ 8 9 const replaceStringInRequest = ( 10 requestId, 11 inString, 12 outString, 13 inEncoding = "utf-8" 14 ) => { 15 const filter = browser.webRequest.filterResponseData(requestId); 16 const decoder = new TextDecoder(inEncoding); 17 const encoder = new TextEncoder(); 18 const RE = new RegExp(inString, "g"); 19 const carryoverLength = inString.length; 20 let carryover = ""; 21 22 filter.ondata = event => { 23 const replaced = ( 24 carryover + decoder.decode(event.data, { stream: true }) 25 ).replace(RE, outString); 26 filter.write(encoder.encode(replaced.slice(0, -carryoverLength))); 27 carryover = replaced.slice(-carryoverLength); 28 }; 29 30 filter.onstop = () => { 31 if (carryover.length) { 32 filter.write(encoder.encode(carryover)); 33 } 34 filter.close(); 35 }; 36 }; 37 38 const interventionListeners = new Map(); 39 40 function rememberListener(intervention, key, listener) { 41 if (!interventionListeners.has(intervention)) { 42 interventionListeners.set(intervention, new Map()); 43 } 44 const map = interventionListeners.get(intervention); 45 if (map.has(key)) { 46 throw new Error(`multiple custom listeners have the same key ${key}`); 47 } 48 map.set(key, listener); 49 } 50 51 function forgetListener(intervention, key) { 52 const map = interventionListeners.get(intervention); 53 if (!map) { 54 return undefined; 55 } 56 const listener = map.get(key); 57 map.delete(key); 58 return listener; 59 } 60 61 function makeHeaderAlterer(headerType, webRequestAPI) { 62 return { 63 details: ["headers", "replacement"], 64 optionalDetails: ["fallback", "replace", "types", "urls"], 65 getKey(config) { 66 return `alter_${headerType}_headers:${JSON.stringify(config)}`; 67 }, 68 enable(config, intervention) { 69 let { fallback, headers, replace, replacement, types, urls } = config; 70 if (!urls) { 71 urls = Object.values(intervention.bugs) 72 .map(bug => bug.matches) 73 .flat() 74 .filter(v => v !== undefined); 75 } 76 const regex = 77 replace === null ? null : new RegExp(replace ?? "^.*$", "gi"); 78 const listener = evt => { 79 let found = false; 80 const finalHeaders = []; 81 for (const header of evt[`${headerType}Headers`]) { 82 if (headers.includes(header.name.toLowerCase())) { 83 found = true; 84 if ( 85 regex !== null && 86 replacement !== null && 87 replacement !== undefined 88 ) { 89 const value = header.value.replaceAll(regex, replacement); 90 finalHeaders.push({ name: header.name, value }); 91 } else if (replacement !== null) { 92 finalHeaders.push(header); 93 } 94 } else { 95 finalHeaders.push(header); 96 } 97 } 98 if (!found && (replace === undefined || typeof fallback === "string")) { 99 const value = fallback ?? replacement; 100 if (value !== null) { 101 finalHeaders.push({ 102 name: headers[0], 103 value, 104 }); 105 } 106 } 107 const retval = {}; 108 retval[`${headerType}Headers`] = finalHeaders; 109 return retval; 110 }; 111 browser.webRequest[webRequestAPI].addListener(listener, { types, urls }, [ 112 "blocking", 113 `${headerType}Headers`, 114 ]); 115 rememberListener(intervention, this.getKey(config), listener); 116 }, 117 disable(config, intervention) { 118 const listener = forgetListener(intervention, this.getKey(config)); 119 if (listener) { 120 browser.webRequest[webRequestAPI].removeListener(listener); 121 } 122 }, 123 }; 124 } 125 126 var CUSTOM_FUNCTIONS = { 127 alter_request_headers: makeHeaderAlterer("request", "onBeforeSendHeaders"), 128 alter_response_headers: makeHeaderAlterer("response", "onHeadersReceived"), 129 replace_string_in_request: { 130 details: ["find", "replace", "urls"], 131 optionalDetails: ["types"], 132 enable(details) { 133 const { find, replace, urls, types } = details; 134 const listener = (details.listener = ({ requestId }) => { 135 replaceStringInRequest(requestId, find, replace); 136 return {}; 137 }); 138 browser.webRequest.onBeforeRequest.addListener( 139 listener, 140 { urls, types }, 141 ["blocking"] 142 ); 143 }, 144 disable(details) { 145 const { listener } = details; 146 browser.webRequest.onBeforeRequest.removeListener(listener); 147 delete details.listener; 148 }, 149 }, 150 run_script_before_request: { 151 details: ["message", "urls", "script"], 152 optionalDetails: ["types"], 153 enable(details, intervention) { 154 const { bug } = intervention; 155 const { message, script, types, urls } = details; 156 const warning = `${message} See https://bugzilla.mozilla.org/show_bug.cgi?id=${bug} for details.`; 157 158 const listener = (details.listener = evt => { 159 const { tabId, frameId } = evt; 160 return browser.tabs 161 .executeScript(tabId, { 162 file: script, 163 frameId, 164 runAt: "document_start", 165 }) 166 .then(() => { 167 browser.tabs.executeScript(tabId, { 168 code: `console.warn(${JSON.stringify(warning)})`, 169 runAt: "document_start", 170 }); 171 }) 172 .catch(err => { 173 console.error( 174 "Error running script before request for webcompat intervention for bug", 175 bug, 176 err 177 ); 178 }); 179 }); 180 181 browser.webRequest.onBeforeRequest.addListener( 182 listener, 183 { urls, types: types || ["script"] }, 184 ["blocking"] 185 ); 186 }, 187 disable(details) { 188 const { listener } = details; 189 browser.webRequest.onBeforeRequest.removeListener(listener); 190 delete details.listener; 191 }, 192 }, 193 };