├── .gitignore ├── icons ├── icon-16x16.png ├── icon-48x48.png ├── storeIcon.png └── icon-128x128.png ├── src ├── background │ ├── init.js │ ├── util.js │ ├── background.html │ ├── requestIdTracker.js │ ├── tabUrlTracker.js │ ├── extractMime.js │ ├── mainStorage.js │ ├── requestHandling.js │ ├── match.js │ ├── headerHandling.js │ ├── background.js │ └── keyvalDB.js ├── ui │ ├── devtools.html │ ├── init.js │ ├── devtools.js │ ├── 404Switch.js │ ├── onOffSwitch.js │ ├── fileRule.js │ ├── webRule.js │ ├── injectRule.js │ ├── importExport.js │ ├── headers.js │ ├── options.js │ ├── css │ │ ├── on-off-switch.css │ │ └── style.css │ ├── util.js │ ├── devtoolstab.js │ ├── headerRule.js │ ├── headerEditor.js │ ├── moveableRules.js │ ├── editor.js │ ├── tabGroup.js │ ├── suggest.js │ └── devtoolstab.html └── inject │ └── scriptInjector.js ├── .editorconfig ├── README.md ├── manifest.json ├── LICENSE ├── maintenance_notice.md ├── todo.txt ├── lib └── ace │ ├── theme-monokai.js │ ├── ext-searchbox.js │ ├── mode-xml.js │ ├── mode-css.js │ └── mode-javascript.js └── test └── matchTest.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | *.crx 3 | *.xpi 4 | -------------------------------------------------------------------------------- /icons/icon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyleladd/ResourceOverride/master/icons/icon-16x16.png -------------------------------------------------------------------------------- /icons/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyleladd/ResourceOverride/master/icons/icon-48x48.png -------------------------------------------------------------------------------- /icons/storeIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyleladd/ResourceOverride/master/icons/storeIcon.png -------------------------------------------------------------------------------- /icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyleladd/ResourceOverride/master/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/background/init.js: -------------------------------------------------------------------------------- 1 | { 2 | window.bgapp = {}; 3 | window.browser = window.browser ? window.browser : window.chrome; 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | insert_final_newline = true 8 | 9 | [*.json] 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /src/ui/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/ui/init.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | /* globals $ */ 5 | 6 | const ui = {}; 7 | 8 | // pre-fetch all major ui. 9 | $("[id]").each(function(idx, el) { 10 | ui[el.id] = $(el); 11 | }); 12 | 13 | const app = { 14 | ui: ui 15 | }; 16 | window.app = app; 17 | })(); 18 | -------------------------------------------------------------------------------- /src/ui/devtools.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.sendMessage({action: "getSetting", setting: "devTools"}, function(data) { 2 | if (data === "true") { 3 | chrome.devtools.panels.create("Overrides", 4 | "", //image file 5 | "/src/ui/devtoolstab.html", 6 | function(panel) {} 7 | ); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resource Override 2 | 3 | ## ~~ This extension is now in maintenance mode!! ~~ 4 | 5 | See what this means here: [maintenance_notice.md](maintenance_notice.md). 6 | 7 | Resource Override is an extension to help you gain full control of any website by redirecting traffic, replacing, editing, or inserting new content. 8 | 9 | [Get the chrome extension here](https://chrome.google.com/webstore/detail/resource-override/pkoacgokdfckfpndoffpifphamojphii). 10 | 11 | ### Now on firefox! 12 | [Get the FireFox extension here](https://addons.mozilla.org/en-US/firefox/addon/resourceoverride/) 13 | -------------------------------------------------------------------------------- /src/background/util.js: -------------------------------------------------------------------------------- 1 | /* global bgapp, chrome */ 2 | { 3 | bgapp.util = {}; 4 | 5 | bgapp.util.logOnTab = function(tabId, message, important) { 6 | if (localStorage.showLogs === "true") { 7 | important = !!important; 8 | chrome.tabs.sendMessage(tabId, { 9 | action: "log", 10 | message: message, 11 | important: important 12 | }); 13 | } 14 | }; 15 | 16 | bgapp.util.simpleError = function(err) { 17 | if (err.stack) { 18 | console.error("=== Printing Stack ==="); 19 | console.error(err.stack); 20 | } 21 | console.error(err); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/background/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/ui/404Switch.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | /* globals $ */ 5 | 6 | const ui = window.app.ui; 7 | 8 | Object.defineProperty(HTMLElement.prototype, "isAll", { 9 | get: function() { 10 | const input = this.querySelector("input"); 11 | if (input) { 12 | return input.checked; 13 | } 14 | }, 15 | set: function(val) { 16 | const input = this.querySelector("input"); 17 | if (input) { 18 | input.checked = !!val; 19 | } 20 | } 21 | }); 22 | 23 | window.app.create404Switch = function() { 24 | return document.importNode(ui.notFoundSwitchTemplate[0].content, true); 25 | }; 26 | })(); 27 | -------------------------------------------------------------------------------- /src/ui/onOffSwitch.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | /* globals $ */ 5 | 6 | const ui = window.app.ui; 7 | 8 | Object.defineProperty(HTMLElement.prototype, "isOn", { 9 | get: function() { 10 | const input = this.querySelector("input"); 11 | if (input) { 12 | return input.checked; 13 | } 14 | }, 15 | set: function(val) { 16 | const input = this.querySelector("input"); 17 | if (input) { 18 | input.checked = !!val; 19 | } 20 | } 21 | }); 22 | 23 | window.app.createOnOffSwitch = function() { 24 | return document.importNode(ui.onOffSwitchTemplate[0].content, true); 25 | }; 26 | })(); 27 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Resource Override", 3 | "version": "1.3.0", 4 | "description": "An extension to help you gain full control of any website by redirecting traffic, replacing, editing, or inserting new content.", 5 | "icons": { 6 | "16": "icons/icon-16x16.png", 7 | "48": "icons/icon-48x48.png", 8 | "128": "icons/icon-128x128.png" 9 | }, 10 | "browser_action": { 11 | "default_icon": { 12 | "16": "icons/icon-16x16.png" 13 | } 14 | }, 15 | "devtools_page": "src/ui/devtools.html", 16 | "options_ui": { 17 | "page": "src/ui/devtoolstab.html" 18 | }, 19 | "background": { 20 | "page": "src/background/background.html" 21 | }, 22 | "content_scripts": [{ 23 | "matches" : ["*://*/*"], 24 | "js": ["src/inject/scriptInjector.js"], 25 | "all_frames": true, 26 | "run_at": "document_start" 27 | }], 28 | "permissions": ["storage", "webRequest", "webRequestBlocking", "", "tabs"], 29 | "manifest_version": 2 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | --------------------- 3 | 4 | Copyright (c) 2014 Kyle Paulsen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/background/requestIdTracker.js: -------------------------------------------------------------------------------- 1 | /* global bgapp */ 2 | { 3 | bgapp.requestIdTracker = (function() { 4 | let head; 5 | let tail; 6 | let length = 0; 7 | const tracker = {}; 8 | const maxSize = 1000; 9 | 10 | function pop() { 11 | const val = head.val; 12 | head = head.next; 13 | length--; 14 | delete tracker[val]; 15 | } 16 | 17 | function push(obj) { 18 | const newNode = { 19 | val: obj, 20 | next: undefined 21 | }; 22 | if (length > 0) { 23 | tail.next = newNode; 24 | tail = newNode; 25 | } else { 26 | head = newNode; 27 | tail = newNode; 28 | } 29 | length++; 30 | tracker[obj] = true; 31 | while (length > maxSize) { 32 | pop(); 33 | } 34 | } 35 | 36 | function has(id) { 37 | return tracker[id]; 38 | } 39 | 40 | return { 41 | push: push, 42 | has: has 43 | }; 44 | })(); 45 | } 46 | -------------------------------------------------------------------------------- /maintenance_notice.md: -------------------------------------------------------------------------------- 1 | # The Future of Resource Override 2 | 3 | My top priority for Resource Override has always been to provide an ad-free, open source, useful tool that costs nothing to the end user. 4 | 5 | I plan to keep it this way. 6 | 7 | Resource Override has unofficially been in maintenance mode for a while now. I have realized that my free time has become quite limited, so from this point forward, Resource Override is now officially in maintenance mode. 8 | 9 | What this means: 10 | 11 | * I will continue to fix any critical bugs that cause basic functionality to break. 12 | 13 | * No new features will be added and any features that were never working in the first place (if any) wont be fixed. 14 | 15 | * It's unlikely I will be accepting pull requests because I simply don't have the time to test and make sure everything works. I really don't want to introduce any bugs. Changing code for features is too risky for me at this point. 16 | 17 | * It's unlikely I will be able to support my user base at this time, but if you have a simple question about Resource Override, I can try my best to answer. Feel free to email me at `kyle.a.paulsen(at)gmail.com` 18 | 19 | Again, it is important to me that you have an ad-free, free, open source option for this kind of tool. If you are still using Resource Override, thanks for trying it out and I'm glad it may have helped you. 20 | 21 | \- Kyle Paulsen 22 | -------------------------------------------------------------------------------- /src/background/tabUrlTracker.js: -------------------------------------------------------------------------------- 1 | /* globals chrome, bgapp */ 2 | { 3 | // http://stackoverflow.com/questions/15124995/how-to-wait-for-an-asynchronous-methods-callback-return-value 4 | bgapp.tabUrlTracker = (function() { 5 | // All opened urls 6 | const urls = {}; 7 | const closeListeners = []; 8 | 9 | const queryTabsCallback = function(allTabs) { 10 | if (allTabs) { 11 | allTabs.forEach(function(tab) { 12 | urls[tab.id] = tab.url; 13 | }); 14 | } 15 | }; 16 | 17 | const updateTabCallback = function(tabId, changeinfo, tab) { 18 | urls[tabId] = tab.url; 19 | }; 20 | 21 | // Not all tabs will fire an update event. If the page is pre-rendered, 22 | // a replace will happen instead. 23 | const tabReplacedCallback = function(newTabId, oldTabId) { 24 | delete urls[oldTabId]; 25 | chrome.tabs.get(newTabId, function(tab) { 26 | urls[tab.id] = tab.url; 27 | }); 28 | }; 29 | 30 | const removeTabCallback = function(tabId) { 31 | closeListeners.forEach(function(fn) { 32 | fn(urls[tabId]); 33 | }); 34 | delete urls[tabId]; 35 | }; 36 | 37 | // init 38 | chrome.tabs.query({}, queryTabsCallback); 39 | chrome.tabs.onUpdated.addListener(updateTabCallback); 40 | chrome.tabs.onRemoved.addListener(removeTabCallback); 41 | chrome.tabs.onReplaced.addListener(tabReplacedCallback); 42 | 43 | return { 44 | getUrlFromId: function(id) { 45 | return urls[id]; 46 | } 47 | }; 48 | })(); 49 | } 50 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | Consider adding autocomplete to editor. 2 | - might need a way to turn that off. 3 | 4 | Add in the collapse domain arrows again. 5 | - save the state 6 | 7 | Bug with scripts not being executed first or last in the document. 8 | for body scripts: wait for the window 'load' event to inject. 9 | http://stackoverflow.com/questions/8965953/how-to-inject-content-script-when-page-loads-while-having-a-popup-page 10 | cant really use a static content script. 11 | Might have to do something like this: 12 | chrome.tabs.executeScript(null, {code: 'eval(decodeURIComponent("' + encodeURIComponent($0.value) + '"));'}); 13 | or consider using this: https://github.com/joliss/js-string-escape 14 | 15 | Fix delete button looking like crap on mac. 16 | 17 | Make example for how to override a buncha stuff except for some things. 18 | - do we need a tips popup? 19 | 20 | Close editors when we get a sync update. 21 | 22 | Maybe make a request url tester that shows what rule it will hit 23 | 24 | Bug where suggestions are wrong after navigation 25 | - This is kind of a hard one. Need to implement background page -> devtools messaging. 26 | - https://developer.chrome.com/extensions/devtools#content-script-to-devtools 27 | - Should probably refactor all of messaging. 28 | - // on devtools page: 29 | var backgroundPagePort = chrome.runtime.connect({name: "asd"}); 30 | backgroundPagePort.onMessage.addListener(function(data, port) { 31 | var func = backgroundPageFuncs[data.action]; 32 | if (func) { 33 | func(data, port); 34 | } 35 | }); 36 | // on background page: 37 | chrome.runtime.onConnect.addListener(function(port) { 38 | // store port with some id. Might need to have devtools page send one. 39 | }); 40 | somePort.postMessage({asd: "Qwe"}); 41 | 42 | Add a way to know the rule type after added? 43 | Add ability to drag tab url groups? 44 | Make help section suck less. No one understands how *s work. 45 | Clean up this disgusting project 46 | 47 | -------------------------------------------------------------------------------- /src/background/extractMime.js: -------------------------------------------------------------------------------- 1 | /* global bgapp */ 2 | { 3 | // This function will try to guess the mime type. 4 | // The goal is to use the highest ranking mime type that it gets from these sources (highest 5 | // ranking sources first): The user provided mime type on the first line of the file, the url 6 | // file extension, the file looks like html, the file looks like xml, the file looks like, 7 | // JavaScript, the file looks like CSS, can't tell what the file is so default to text/plain. 8 | bgapp.extractMimeType = function(requestUrl, file) { 9 | file = file || ""; 10 | const possibleExt = (requestUrl.match(/\.[A-Za-z]{2,4}$/) || [""])[0]; 11 | const looksLikeCSSRegex = /[#.@][^\s\{]+\s*\{/; 12 | const looksLikeJSRegex = /(var|const|let|function)\s+.+/; 13 | const looksLikeXMLRegex = /<\?xml(\s+.+\s*)?\?>/i; 14 | const looksLikeHTMLRegex = //i; 15 | const mimeInFileRegex = /\/\* *mime: *([-\w\/]+) *\*\//i; 16 | const firstLine = (file.match(/.*/) || [""])[0]; 17 | let userMime = firstLine.match(mimeInFileRegex); 18 | userMime = userMime ? userMime[1] : null; 19 | const extToMime = { 20 | ".js": "text/javascript", 21 | ".html": "text/html", 22 | ".css": "text/css", 23 | ".xml": "text/xml" 24 | }; 25 | let mime = extToMime[possibleExt]; 26 | if (!mime) { 27 | if (looksLikeHTMLRegex.test(file)) { 28 | mime = "text/html"; 29 | } else if (looksLikeXMLRegex.test(file)) { 30 | mime = "text/xml"; 31 | } else if (looksLikeJSRegex.test(file)) { 32 | mime = "text/javascript"; 33 | } else if (looksLikeCSSRegex.test(file)) { 34 | mime = "text/css"; 35 | } else { 36 | mime = "text/plain"; 37 | } 38 | } 39 | if (userMime) { 40 | mime = userMime; 41 | file = file.replace(mimeInFileRegex, ""); 42 | } 43 | return {mime: mime, file: file}; 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /lib/ace/theme-monokai.js: -------------------------------------------------------------------------------- 1 | define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-monokai",t.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai {background-color: #272822;color: #F8F8F2}.ace-monokai .ace_cursor {color: #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta.ace_tag,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_punctuation,.ace-monokai .ace_punctuation.ace_tag {color: #fff}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_entity.ace_other.ace_attribute-name,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /src/inject/scriptInjector.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | var fileTypeToTag = { 5 | js: "script", 6 | css: "style" 7 | }; 8 | 9 | var processDomain = function(domain) { 10 | var rules = domain.rules || []; 11 | rules.forEach(function(rule) { 12 | if (rule.on && rule.type === "fileInject") { 13 | var newEl = document.createElement(fileTypeToTag[rule.fileType] || "script"); 14 | newEl.appendChild(document.createTextNode(rule.file)); 15 | if (rule.injectLocation === "head") { 16 | var firstEl = document.head.children[0]; 17 | if (firstEl) { 18 | document.head.insertBefore(newEl, firstEl); 19 | } else { 20 | document.head.appendChild(newEl); 21 | } 22 | } else { 23 | if (document.body) { 24 | document.body.appendChild(newEl); 25 | } else { 26 | document.addEventListener("DOMContentLoaded", function() { 27 | document.body.appendChild(newEl); 28 | }); 29 | } 30 | } 31 | } 32 | }); 33 | }; 34 | 35 | chrome.runtime.sendMessage({action: "getDomains"}, function(domains) { 36 | domains = domains || []; 37 | domains.forEach(function(domain) { 38 | if (domain.on) { 39 | chrome.runtime.sendMessage({ 40 | action: "match", 41 | domainUrl: domain.matchUrl, 42 | windowUrl: location.href 43 | }, function(result) { 44 | if (result) { 45 | processDomain(domain); 46 | } 47 | }); 48 | } 49 | }); 50 | }); 51 | 52 | chrome.runtime.onMessage.addListener(function(msg) { 53 | if (msg.action === 'log') { 54 | var logStyle = "color: #007182; font-weight: bold;"; 55 | if (msg.important) { 56 | logStyle += "background: #AAFFFF;"; 57 | } 58 | console.log("%c[Resource Override] " + msg.message, logStyle); 59 | } 60 | }); 61 | })(); 62 | -------------------------------------------------------------------------------- /src/ui/fileRule.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | /* globals $ */ 5 | 6 | const app = window.app; 7 | const util = app.util; 8 | const ui = app.ui; 9 | 10 | function createFileOverrideMarkup(savedData, saveFunc) { 11 | savedData = savedData || {}; 12 | saveFunc = saveFunc || function() {}; 13 | 14 | const override = util.instanceTemplate(ui.fileOverrideTemplate); 15 | const matchInput = override.find(".matchInput"); 16 | const editBtn = override.find(".edit-btn"); 17 | const ruleOnOff = override.find(".onoffswitch"); 18 | const deleteBtn = override.find(".sym-btn"); 19 | 20 | matchInput.val(savedData.match || ""); 21 | util.makeFieldRequired(matchInput); 22 | ruleOnOff[0].isOn = savedData.on === false ? false : true; 23 | 24 | if (savedData.on === false) { 25 | override.addClass("disabled"); 26 | } 27 | 28 | editBtn.on("click", function() { 29 | app.editor.open(override[0].id, matchInput.val(), false, saveFunc); 30 | }); 31 | 32 | deleteBtn.on("click", function() { 33 | if (!util.deleteButtonIsSure(deleteBtn)) { 34 | return; 35 | } 36 | override.css("transition", "none"); 37 | override.fadeOut(function() { 38 | override.remove(); 39 | delete app.files[override[0].id]; 40 | saveFunc(); 41 | }); 42 | }); 43 | 44 | deleteBtn.on("mouseout", function() { 45 | util.deleteButtonIsSureReset(deleteBtn); 46 | }); 47 | 48 | app.mainSuggest.init(matchInput); 49 | 50 | matchInput.on("keyup", saveFunc); 51 | ruleOnOff.on("click change", function() { 52 | override.toggleClass("disabled", !ruleOnOff[0].isOn); 53 | saveFunc(); 54 | }); 55 | 56 | let id = savedData.fileId || util.getNextId($(".ruleContainer"), "f"); 57 | if (app.files[id]) { 58 | id = util.getNextId($(".ruleContainer"), "f"); 59 | } 60 | override[0].id = id; 61 | 62 | if (savedData.file) { 63 | app.files[id] = savedData.file; 64 | } 65 | 66 | return override; 67 | } 68 | 69 | app.createFileOverrideMarkup = createFileOverrideMarkup; 70 | 71 | })(); 72 | -------------------------------------------------------------------------------- /src/ui/webRule.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | const app = window.app; 5 | const util = app.util; 6 | const ui = app.ui; 7 | 8 | function createWebOverrideMarkup(savedData, saveFunc) { 9 | savedData = savedData || {}; 10 | saveFunc = saveFunc || function() {}; 11 | 12 | const override = util.instanceTemplate(ui.overrideTemplate); 13 | const matchInput = override.find(".matchInput"); 14 | const replaceInput = override.find(".replaceInput"); 15 | // was const ruleOnOff = override.find(".ruleOnOff"); 16 | const ruleOnOff = override.find(".onoffswitch"); 17 | const allOr404Only = override.find(".allOr404Only"); 18 | const deleteBtn = override.find(".sym-btn"); 19 | 20 | matchInput.val(savedData.match || ""); 21 | replaceInput.val(savedData.replace || ""); 22 | util.makeFieldRequired(matchInput); 23 | util.makeFieldRequired(replaceInput); 24 | ruleOnOff[0].isOn = savedData.on === false ? false : true; 25 | allOr404Only[0].isAll = savedData.all === false ? false : true; 26 | 27 | if (savedData.on === false) { 28 | override.addClass("disabled"); 29 | } 30 | 31 | deleteBtn.on("click", function() { 32 | if (!util.deleteButtonIsSure(deleteBtn)) { 33 | return; 34 | } 35 | override.css("transition", "none"); 36 | override.fadeOut(function() { 37 | override.remove(); 38 | saveFunc(); 39 | app.skipNextSync = true; 40 | }); 41 | }); 42 | 43 | deleteBtn.on("mouseout", function() { 44 | util.deleteButtonIsSureReset(deleteBtn); 45 | }); 46 | 47 | app.mainSuggest.init(matchInput); 48 | 49 | matchInput.on("keyup", saveFunc); 50 | replaceInput.on("keyup", saveFunc); 51 | ruleOnOff.on("click change", function() { 52 | override.toggleClass("disabled", !ruleOnOff[0].isOn); 53 | saveFunc(); 54 | }); 55 | allOr404Only.on("click change", function() { 56 | override.toggleClass("disabled", !allOr404Only[0].isAll); 57 | saveFunc(); 58 | }); 59 | 60 | return override; 61 | } 62 | 63 | app.createWebOverrideMarkup = createWebOverrideMarkup; 64 | 65 | })(); 66 | -------------------------------------------------------------------------------- /src/ui/injectRule.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | /* globals $ */ 5 | 6 | const app = window.app; 7 | const util = app.util; 8 | const ui = app.ui; 9 | 10 | function createFileInjectMarkup(savedData, saveFunc) { 11 | savedData = savedData || {}; 12 | saveFunc = saveFunc || function() {}; 13 | 14 | const override = util.instanceTemplate(ui.fileInjectTemplate); 15 | const fileName = override.find(".fileName"); 16 | const injectLocation = override.find(".injectLocationSelect"); 17 | const fileType = override.find(".fileTypeSelect"); 18 | const editBtn = override.find(".edit-btn"); 19 | const ruleOnOff = override.find(".onoffswitch"); 20 | const deleteBtn = override.find(".sym-btn"); 21 | 22 | fileName.val(savedData.fileName || ""); 23 | injectLocation.val(savedData.injectLocation || "head"); 24 | fileType.val(savedData.fileType || "js"); 25 | ruleOnOff[0].isOn = savedData.on === false ? false : true; 26 | 27 | if (savedData.on === false) { 28 | override.addClass("disabled"); 29 | } 30 | 31 | editBtn.on("click", function() { 32 | app.editor.open(override[0].id, fileName.val(), true, saveFunc); 33 | }); 34 | 35 | deleteBtn.on("click", function() { 36 | if (!util.deleteButtonIsSure(deleteBtn)) { 37 | return; 38 | } 39 | override.css("transition", "none"); 40 | override.fadeOut(function() { 41 | override.remove(); 42 | delete app.files[override[0].id]; 43 | saveFunc(); 44 | }); 45 | }); 46 | 47 | deleteBtn.on("mouseout", function() { 48 | util.deleteButtonIsSureReset(deleteBtn); 49 | }); 50 | 51 | fileName.on("keyup", saveFunc); 52 | ruleOnOff.on("click change", function() { 53 | override.toggleClass("disabled", !ruleOnOff[0].isOn); 54 | saveFunc(); 55 | }); 56 | injectLocation.on("change", saveFunc); 57 | fileType.on("change", saveFunc); 58 | 59 | let id = savedData.fileId || util.getNextId($(".ruleContainer"), "f"); 60 | if (app.files[id]) { 61 | id = util.getNextId($(".ruleContainer"), "f"); 62 | } 63 | override[0].id = id; 64 | 65 | if (savedData.file) { 66 | app.files[id] = savedData.file; 67 | } 68 | 69 | return override; 70 | } 71 | 72 | app.createFileInjectMarkup = createFileInjectMarkup; 73 | 74 | })(); 75 | -------------------------------------------------------------------------------- /src/background/mainStorage.js: -------------------------------------------------------------------------------- 1 | /* global bgapp, keyvalDB */ 2 | { 3 | bgapp.mainStorage = (function() { 4 | const db = keyvalDB("OverrideDB", [{store: "domains", key: "id"}], 1); 5 | const domainStore = db.usingStore("domains"); 6 | 7 | const put = function(domainData) { 8 | return new Promise(function(res, rej) { 9 | db.open(function(err) { 10 | if (err) { 11 | console.error(err); 12 | rej(err); 13 | } else { 14 | domainStore.upsert(domainData.id, domainData, function(err) { 15 | if (err) { 16 | console.error(err); 17 | rej(err); 18 | } else { 19 | res(); 20 | } 21 | }); 22 | } 23 | }); 24 | }); 25 | }; 26 | 27 | const getDomains = function() { 28 | return new Promise(function(res, rej) { 29 | db.open(function(err) { 30 | if (err) { 31 | console.error(err); 32 | rej(err); 33 | } else { 34 | domainStore.getAll(function(err, ans) { 35 | if (err) { 36 | console.error(err); 37 | rej(err); 38 | } else { 39 | res(ans); 40 | } 41 | }); 42 | } 43 | }); 44 | }); 45 | }; 46 | 47 | const deleteDomain = function(id) { 48 | return new Promise(function(res, rej) { 49 | db.open(function(err) { 50 | if (err) { 51 | console.error(err); 52 | rej(err); 53 | } else { 54 | domainStore.delete(id, function(err) { 55 | if (err) { 56 | console.error(err); 57 | rej(err); 58 | } else { 59 | res(); 60 | } 61 | }); 62 | } 63 | }); 64 | }); 65 | }; 66 | 67 | return { 68 | put: put, 69 | getAll: getDomains, 70 | delete: deleteDomain 71 | }; 72 | })(); 73 | } 74 | -------------------------------------------------------------------------------- /src/ui/importExport.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | /* globals $, chrome */ 5 | 6 | const app = window.app; 7 | const util = app.util; 8 | 9 | function checkRule(rule) { 10 | let valid = true; 11 | if (rule.type === "normalOverride") { 12 | valid = valid && rule.match !== undefined; 13 | valid = valid && rule.replace !== undefined; 14 | valid = valid && rule.on !== undefined; 15 | } else if (rule.type === "fileOverride") { 16 | valid = valid && rule.match !== undefined; 17 | valid = valid && rule.file !== undefined; 18 | valid = valid && (/^f[0-9]+$/).test(rule.fileId); 19 | valid = valid && rule.on !== undefined; 20 | } else if (rule.type === "fileInject") { 21 | valid = valid && rule.fileName !== undefined; 22 | valid = valid && rule.file !== undefined; 23 | valid = valid && (/^f[0-9]+$/).test(rule.fileId); 24 | valid = valid && rule.fileType !== undefined; 25 | valid = valid && rule.injectLocation !== undefined; 26 | valid = valid && rule.on !== undefined; 27 | } else if (rule.type === "headerRule") { 28 | valid = valid && rule.match !== undefined; 29 | valid = valid && rule.requestRules !== undefined; 30 | valid = valid && rule.responseRules !== undefined; 31 | valid = valid && rule.on !== undefined; 32 | } 33 | return valid; 34 | } 35 | 36 | function checkDomain(domain) { 37 | let valid = (/^d[0-9]+$/).test(domain.id); 38 | valid = valid && domain.matchUrl !== undefined; 39 | valid = valid && domain.on !== undefined; 40 | valid = valid && $.isArray(domain.rules); 41 | valid = valid && domain.rules.every(checkRule); 42 | return valid; 43 | } 44 | 45 | function importData(data, version) { 46 | // check data first. 47 | if ($.isArray(data) && data.every(checkDomain)) { 48 | // this will call the sync function so stuff will get re-rendered. 49 | chrome.runtime.sendMessage({action: "import", data: data}); 50 | util.showToast("Load Succeeded!"); 51 | } else { 52 | util.showToast("Load Failed: Invalid Resource Override JSON."); 53 | } 54 | } 55 | 56 | function exportData() { 57 | const allData = []; 58 | $(".domainContainer").each(function(idx, domain) { 59 | allData.push(app.getDomainData($(domain))); 60 | }); 61 | return {v: 1, data: allData}; 62 | } 63 | 64 | app.import = importData; 65 | app.export = exportData; 66 | 67 | })(); 68 | -------------------------------------------------------------------------------- /src/ui/headers.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | const app = window.app; 5 | 6 | const requestHeaders = [ 7 | "Accept", 8 | "Accept-Charset", 9 | "Accept-Datetime", 10 | "Accept-Encoding", 11 | "Accept-Language", 12 | "Authorization", 13 | "Cache-Control", 14 | "Connection", 15 | "Content-Length", 16 | "Content-MD5", 17 | "Content-Type", 18 | "Cookie", 19 | "Date", 20 | "Expect", 21 | "Forwarded", 22 | "From", 23 | "Front-End-Https", 24 | "Host", 25 | "If-Match", 26 | "If-Modified-Since", 27 | "If-None-Match", 28 | "If-Range", 29 | "If-Unmodified-Since", 30 | "Max-Forwards", 31 | "Origin", 32 | "Pragma", 33 | "Proxy-Authorization", 34 | "Range", 35 | "Referer", 36 | "TE", 37 | "Trailer", 38 | "Transfer-Encoding", 39 | "Upgrade", 40 | "User-Agent", 41 | "Via", 42 | "Warning", 43 | "X-Csrf-Token", 44 | "X-Forwarded-For", 45 | "X-Forwarded-Host", 46 | "X-Forwarded-Port", 47 | "X-Forwarded-Proto", 48 | "X-Http-Method-Override" 49 | ]; 50 | 51 | const responseHeaders = [ 52 | "Accept-Patch", 53 | "Accept-Ranges", 54 | "Access-Control-Allow-Origin", 55 | "Age", 56 | "Allow", 57 | "Cache-Control", 58 | "Connection", 59 | "Content-Disposition", 60 | "Content-Encoding", 61 | "Content-Language", 62 | "Content-Length", 63 | "Content-Location", 64 | "Content-MD5", 65 | "Content-Range", 66 | "Content-Security-Policy", 67 | "Content-Type", 68 | "Date", 69 | "ETag", 70 | "Expires", 71 | "Last-Modified", 72 | "Link", 73 | "Location", 74 | "Pragma", 75 | "Proxy-Authenticate", 76 | "Refresh", 77 | "Retry-After", 78 | "Server", 79 | "Set-Cookie", 80 | "Status", 81 | "Strict-Transport-Security", 82 | "TSV", 83 | "Trailer", 84 | "Transfer-Encoding", 85 | "Upgrade", 86 | "Upgrade-Insecure-Requests", 87 | "Vary", 88 | "Via", 89 | "WWW-Authenticate", 90 | "Warning", 91 | "X-Content-Duration", 92 | "X-Content-Security-Policy", 93 | "X-Content-Type-Options", 94 | "X-Frame-Options", 95 | "X-Powered-By", 96 | "X-UA-Compatible", 97 | "X-WebKit-CSP", 98 | "X-XSS-Protection" 99 | ]; 100 | 101 | app.headersLists = { 102 | requestHeaders: requestHeaders, 103 | responseHeaders: responseHeaders 104 | }; 105 | })(); 106 | -------------------------------------------------------------------------------- /src/background/requestHandling.js: -------------------------------------------------------------------------------- 1 | /* global bgapp, match, matchReplace, browser */ 2 | { 3 | const logOnTab = bgapp.util.logOnTab; 4 | 5 | const replaceContent = (requestId, mimeAndFile) => { 6 | if (browser.webRequest.filterResponseData) { 7 | // browsers that support filterResponseData 8 | browser.webRequest.filterResponseData(requestId).onstart = e => { 9 | const encoder = new TextEncoder(); 10 | e.target.write(encoder.encode(mimeAndFile.file)); 11 | e.target.disconnect(); 12 | }; 13 | return { 14 | responseHeaders: [{ 15 | name: "Content-Type", 16 | value: mimeAndFile.mime 17 | }] 18 | }; 19 | } 20 | 21 | // browsers that dont support filterResponseData 22 | return { 23 | // unescape is a easy solution to the utf-8 problem: 24 | // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa#Unicode_Strings 25 | redirectUrl: "data:" + mimeAndFile.mime + ";charset=UTF-8;base64," + 26 | btoa(unescape(encodeURIComponent(mimeAndFile.file))) 27 | }; 28 | }; 29 | 30 | bgapp.handleRequest = function(requestUrl, tabUrl, tabId, requestId, statusCode) { 31 | for (const key in bgapp.ruleDomains) { 32 | const domainObj = bgapp.ruleDomains[key]; 33 | if (domainObj.on && match(domainObj.matchUrl, tabUrl).matched) { 34 | const rules = domainObj.rules || []; 35 | for (let x = 0, len = rules.length; x < len; ++x) { 36 | const ruleObj = rules[x]; 37 | if (ruleObj.on) { 38 | if (ruleObj.type === "normalOverride" && (typeof ruleObj.all === "undefined" || ruleObj.all === true || (statusCode === 404))) { 39 | const matchedObj = match(ruleObj.match, requestUrl); 40 | const newUrl = matchReplace(matchedObj, ruleObj.replace, requestUrl); 41 | if (matchedObj.matched) { 42 | logOnTab(tabId, "URL Override Matched: " + requestUrl + 43 | " to: " + newUrl + " match url: " + ruleObj.match, true); 44 | if (requestUrl !== newUrl) { 45 | return {redirectUrl: newUrl}; 46 | } else { 47 | // allow redirections to the original url (aka do nothing). 48 | // This allows for "redirect all of these except this." 49 | return; 50 | } 51 | } 52 | } else if (ruleObj.type === "fileOverride" && 53 | match(ruleObj.match, requestUrl).matched) { 54 | 55 | logOnTab(tabId, "File Override Matched: " + requestUrl + " match url: " + 56 | ruleObj.match, true); 57 | 58 | const mimeAndFile = bgapp.extractMimeType(requestUrl, ruleObj.file); 59 | return replaceContent(requestId, mimeAndFile); 60 | } 61 | } 62 | } 63 | logOnTab(tabId, "No override match for: " + requestUrl); 64 | } else { 65 | logOnTab(tabId, "Rule is off or tab URL does not match: " + domainObj.matchUrl); 66 | } 67 | } 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /test/matchTest.js: -------------------------------------------------------------------------------- 1 | var matchReplace = require("../src/background/match"); 2 | 3 | function assert(val, expected) { 4 | if (val !== expected) { 5 | throw new Error("An assert failed! " + val + " !== " + expected); 6 | } 7 | } 8 | 9 | assert(matchReplace("asd", "qwe", "asd"), "qwe"); 10 | assert(matchReplace("asd", "qwe", "asd "), "asd "); 11 | assert(matchReplace("asd*", "qwe", "asd"), "qwe"); 12 | assert(matchReplace("asd*", "qwe", "asd "), "qwe"); 13 | assert(matchReplace("asd*", "qwe", "asdzxc"), "qwe"); 14 | assert(matchReplace("asd*", "qwe", " asd"), " asd"); 15 | assert(matchReplace("*a", "qwe", "asda"), "asda"); // matching is greedy! 16 | assert(matchReplace("*asd", "qwe", "asd"), "qwe"); 17 | assert(matchReplace("*asd", "qwe", "asd "), "asd "); 18 | assert(matchReplace("*asd", "qwe", "zxcasd"), "qwe"); 19 | assert(matchReplace("*asd*", "qwe", "asd"), "qwe"); 20 | assert(matchReplace("*asd*", "qwe", "zxcasd"), "qwe"); 21 | assert(matchReplace("*asd*", "qwe", "asdzxc"), "qwe"); 22 | assert(matchReplace("*asd*", "qwe", "zxcasd123"), "qwe"); 23 | assert(matchReplace("*asd*", "qwe", "zxca5d123"), "zxca5d123"); 24 | assert(matchReplace("asd*", "qwe*", "asd"), "qwe"); 25 | assert(matchReplace("asd*", "qwe*", "asdzxc"), "qwezxc"); 26 | assert(matchReplace("asd*", "qwe*", "a2dzxc"), "a2dzxc"); 27 | assert(matchReplace("asd*", "*qwe", "asd"), "qwe"); 28 | assert(matchReplace("asd*", "*qwe", "asdzxc"), "zxcqwe"); 29 | assert(matchReplace("asd*", "*qwe", "1asdzxc"), "1asdzxc"); 30 | assert(matchReplace("asd*", "*qwe", "a2dzxc"), "a2dzxc"); 31 | assert(matchReplace("*asd", "qwe*", "asd"), "qwe"); 32 | assert(matchReplace("*asd", "qwe*", "zxcasd"), "qwezxc"); 33 | assert(matchReplace("*asd", "qwe*", "asdz"), "asdz"); 34 | assert(matchReplace("*asd", "qwe*", "a2d"), "a2d"); 35 | assert(matchReplace("*asd*", "*qwe*", "asd"), "qwe"); 36 | assert(matchReplace("*asd*", "*qwe*", "zxcasd123"), "zxcqwe123"); 37 | assert(matchReplace("*asd*", "*qwe*", "zxca2d123"), "zxca2d123"); 38 | assert(matchReplace("asd*qwe**", "123*456**", "asdqwe"), "123456"); 39 | assert(matchReplace("asd*qwe**", "123*456**", "asdfghqwe"), "123fgh456"); 40 | assert(matchReplace("asd*qwe**", "123*456**", "asdqwefgh"), "123456fgh"); 41 | assert(matchReplace("asd*qwe**", "123*456**", "asdfghqwerty"), "123fgh456rty"); 42 | assert(matchReplace("asd*qwe**", "123*456**", "qasdqwe"), "qasdqwe"); 43 | assert(matchReplace("asd*qwe**", "123*456**", "qasdnnqwe"), "qasdnnqwe"); 44 | assert(matchReplace("asd**qwe*", "123*456**", "asdqwe"), "123456"); 45 | assert(matchReplace("asd**qwe*", "123*456**", "asdfghqwe"), "123456fgh"); 46 | assert(matchReplace("asd**qwe*", "123*456**", "asdqwefgh"), "123fgh456"); 47 | assert(matchReplace("asd**qwe*", "123*456**", "asdfghqwerty"), "123rty456fgh"); 48 | assert(matchReplace("asd**qwe*", "123*456**", "qasdqwe"), "qasdqwe"); 49 | assert(matchReplace("asd**qwe*", "123*456**", "qasdnnqwe"), "qasdnnqwe"); 50 | assert(matchReplace("asd**qwe*zxc**Q", "123**456**789*0", "asd$%$qwe^&zxc!@Q"), "123$%$456!@789^&0"); 51 | assert(matchReplace("asd**qwe*zxc**Q", "123**456**789*0", "asd$%$qwe^&zxcQ"), "123$%$456789^&0"); 52 | assert(matchReplace("asd**qwe*zxc**Q", "123**456**789*0", "asd$%$qWe^&zxc!@Q"), "asd$%$qWe^&zxc!@Q"); 53 | assert(matchReplace("a*b**c***d", "e***f*g**h****", "a1b2c3d"), "e3f1g2h****"); 54 | assert(matchReplace("a*b**c***d", "e***f*g**h****", "a1bc3d"), "e3f1gh****"); 55 | assert(matchReplace("a*b**c***d", "e***f*g**h****", "aaabbccccd"), "ecccfaagbh****"); 56 | assert(matchReplace("a*b**c***d", "e***f*g**h****", "aaaccccd"), "aaaccccd"); 57 | assert(matchReplace("a*b**c***d", "e***f*g**h****", "a**b***c*d"), "e*f**g***h****"); 58 | 59 | console.log("All tests succeeded!"); 60 | -------------------------------------------------------------------------------- /src/ui/options.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | /* globals $, chrome */ 5 | 6 | const app = window.app; 7 | const ui = app.ui; 8 | const util = app.util; 9 | 10 | $(window).on("click", function(e) { 11 | const $target = $(e.target); 12 | if (e.target.id === "optionsBtn") { 13 | ui.optionsPopOver.toggle(); 14 | ui.helpOverlay.hide(); 15 | } else { 16 | if ($target.closest("#optionsPopOver").length === 0) { 17 | ui.optionsPopOver.hide(); 18 | } 19 | } 20 | }); 21 | 22 | ui.showDevTools.on("click", function() { 23 | chrome.runtime.sendMessage({ 24 | action: "setSetting", 25 | setting: "devTools", 26 | value: ui.showDevTools.prop("checked") 27 | }); 28 | }); 29 | 30 | ui.showSuggestions.on("click", function() { 31 | chrome.runtime.sendMessage({ 32 | action: "setSetting", 33 | setting: "showSuggestions", 34 | value: ui.showSuggestions.prop("checked") 35 | }); 36 | app.mainSuggest.setShouldSuggest(ui.showSuggestions.prop("checked")); 37 | }); 38 | 39 | if (!util.isChrome()) { 40 | ui.showSuggestions.closest(".optionRow").remove(); 41 | } 42 | 43 | ui.showLogs.on("click", function() { 44 | chrome.runtime.sendMessage({ 45 | action: "setSetting", 46 | setting: "showLogs", 47 | value: ui.showLogs.prop("checked") 48 | }); 49 | }); 50 | 51 | ui.saveRulesLink.on("click", function(e) { 52 | e.preventDefault(); 53 | const data = app.export(); 54 | const json = JSON.stringify(data); 55 | const blob = new Blob([json], {type: "text/plain"}); 56 | const downloadLink = document.createElement("a"); 57 | downloadLink.download = "resource_override_rules.json"; 58 | downloadLink.href = window.URL.createObjectURL(blob); 59 | downloadLink.click(); 60 | ui.optionsPopOver.hide(); 61 | }); 62 | 63 | 64 | ui.loadRulesLink.on("click", function(e) { 65 | e.preventDefault(); 66 | ui.loadRulesInput.click(); 67 | ui.optionsPopOver.hide(); 68 | }); 69 | 70 | ui.loadRulesInput.on("change", function(e) { 71 | const reader = new FileReader(); 72 | reader.onload = function() { 73 | const text = reader.result; 74 | try { 75 | const importedObj = JSON.parse(text); 76 | app.import(importedObj.data, importedObj.v); 77 | } catch (e) { 78 | util.showToast("Load Failed: Invalid JSON in file."); 79 | } 80 | }; 81 | reader.readAsText(ui.loadRulesInput[0].files[0]); 82 | ui.loadRulesInput.val(""); 83 | }); 84 | 85 | chrome.runtime.sendMessage({ 86 | action: "getSetting", 87 | setting: "devTools" 88 | }, function(data) { 89 | ui.showDevTools.prop("checked", data === "true"); 90 | }); 91 | 92 | chrome.runtime.sendMessage({ 93 | action: "getSetting", 94 | setting: "showSuggestions" 95 | }, function(data) { 96 | const shouldSuggest = util.isChrome() && data !== "false"; 97 | ui.showSuggestions.prop("checked", shouldSuggest); 98 | app.mainSuggest.setShouldSuggest(shouldSuggest); 99 | }); 100 | 101 | chrome.runtime.sendMessage({ 102 | action: "getSetting", 103 | setting: "showLogs" 104 | }, function(data) { 105 | ui.showLogs.prop("checked", data === "true"); 106 | }); 107 | 108 | })(); 109 | -------------------------------------------------------------------------------- /src/ui/css/on-off-switch.css: -------------------------------------------------------------------------------- 1 | /******************** 2 | On Off Switch 3 | https://proto.io/freebies/onoff/ 4 | ********************/ 5 | .onoffswitch { 6 | position: relative; 7 | width: 80px; 8 | display: inline-block; 9 | -webkit-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | } 13 | .allOr404Only { 14 | position: relative; 15 | width: 80px; 16 | display: inline-block; 17 | -webkit-user-select: none; 18 | -moz-user-select: none; 19 | -ms-user-select: none; 20 | } 21 | input[type="checkbox"].onoffswitch-checkbox { 22 | margin: 0; 23 | width: 80px; 24 | height: 34px; 25 | opacity: 0; 26 | position: absolute; 27 | cursor: pointer; 28 | z-index: 10; 29 | left: 0; 30 | } 31 | .onoffswitch-checkbox:hover + .onoffswitch-label .onoffswitch-on { 32 | background-color: #34a7c1; 33 | } 34 | .onoffswitch-checkbox:hover + .onoffswitch-label .onoffswitch-off { 35 | background-color: #dddddd; 36 | } 37 | .onoffswitch-label { 38 | display: block; 39 | overflow: hidden; 40 | cursor: pointer; 41 | border: 2px solid #999999; 42 | border-radius: 5px; 43 | } 44 | .onoffswitch-inner { 45 | display: block; 46 | width: 200%; 47 | margin-left: -100%; 48 | -moz-transition: margin 0.3s ease-in 0s; 49 | -webkit-transition: margin 0.3s ease-in 0s; 50 | -o-transition: margin 0.3s ease-in 0s; 51 | transition: margin 0.3s ease-in 0s; 52 | } 53 | .onoffswitch-on { 54 | display: block; 55 | float: left; 56 | width: 50%; 57 | height: 30px; 58 | padding: 0; 59 | line-height: 30px; 60 | font-size: 14px; 61 | color: white; 62 | font-family: Trebuchet, Arial, sans-serif; 63 | font-weight: bold; 64 | -moz-box-sizing: border-box; 65 | -webkit-box-sizing: border-box; 66 | box-sizing: border-box; 67 | padding-left: 10px; 68 | background-color: #54b7d1; 69 | color: #ffffff; 70 | text-align: justify; 71 | } 72 | .onoffswitch-off { 73 | display: block; 74 | float: left; 75 | width: 50%; 76 | height: 30px; 77 | padding: 0; 78 | line-height: 30px; 79 | font-size: 14px; 80 | color: white; 81 | font-family: Trebuchet, Arial, sans-serif; 82 | font-weight: bold; 83 | -moz-box-sizing: border-box; 84 | -webkit-box-sizing: border-box; 85 | box-sizing: border-box; 86 | padding-right: 10px; 87 | background-color: #eeeeee; 88 | color: #999999; 89 | text-align: right; 90 | } 91 | .onoffswitch-inner:before, 92 | .onoffswitch-inner:after { 93 | display: block; 94 | float: left; 95 | width: 50%; 96 | height: 30px; 97 | padding: 0; 98 | line-height: 30px; 99 | font-size: 14px; 100 | color: white; 101 | font-family: Trebuchet, Arial, sans-serif; 102 | font-weight: bold; 103 | -moz-box-sizing: border-box; 104 | -webkit-box-sizing: border-box; 105 | box-sizing: border-box; 106 | } 107 | .onoffswitch-switch { 108 | display: block; 109 | width: 18px; 110 | margin: 6px; 111 | background: #ffffff; 112 | border: 2px solid #999999; 113 | border-radius: 5px; 114 | position: absolute; 115 | top: 0; 116 | bottom: 0; 117 | right: 46px; 118 | -moz-transition: all 0.3s ease-in 0s; 119 | -webkit-transition: all 0.3s ease-in 0s; 120 | -o-transition: all 0.3s ease-in 0s; 121 | transition: all 0.3s ease-in 0s; 122 | } 123 | .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { 124 | margin-left: 0; 125 | } 126 | .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { 127 | right: 0px; 128 | } 129 | -------------------------------------------------------------------------------- /src/background/match.js: -------------------------------------------------------------------------------- 1 | function tokenize(str) { 2 | "use strict"; 3 | var ans = str.split(/(\*+)/g); 4 | if (ans[0] === "") { 5 | ans.shift(); 6 | } 7 | if (ans[ans.length - 1] === "") { 8 | ans.pop(); 9 | } 10 | return ans; 11 | } 12 | 13 | function match(pattern, str) { 14 | "use strict"; 15 | var patternTokens = tokenize(pattern); 16 | var freeVars = {}; 17 | var varGroup; 18 | var strParts = str; 19 | var matchAnything = false; 20 | var completeMatch = patternTokens.every(function(token) { 21 | if (token.charAt(0) === "*") { 22 | matchAnything = true; 23 | varGroup = token.length; 24 | freeVars[varGroup] = freeVars[varGroup] || []; 25 | } else { 26 | var matches = strParts.split(token); 27 | if (matches.length > 1) { 28 | // The token was found in the string. 29 | var possibleFreeVar = matches.shift(); 30 | if (matchAnything) { 31 | // Found a possible candidate for the *. 32 | freeVars[varGroup].push(possibleFreeVar); 33 | } else { 34 | if (possibleFreeVar !== "") { 35 | // But if we haven't seen a * for this freeVar, 36 | // the string doesnt match the pattern. 37 | return false; 38 | } 39 | } 40 | 41 | matchAnything = false; 42 | // We matched up part of the pattern to the string 43 | // prepare to look at the next part of the string. 44 | strParts = matches.join(token); 45 | } else { 46 | // The token wasn't found in the string. Pattern doesn't match. 47 | return false; 48 | } 49 | } 50 | return true; 51 | }); 52 | 53 | if (matchAnything) { 54 | // If we still need to match a string part up to a star, 55 | // match the rest of the string. 56 | freeVars[varGroup].push(strParts); 57 | } else { 58 | if (strParts !== "") { 59 | // There is still some string part that didn't match up to the pattern. 60 | completeMatch = false; 61 | } 62 | } 63 | 64 | return { 65 | matched: completeMatch, 66 | freeVars: freeVars 67 | }; 68 | } 69 | 70 | function replaceAfter(str, idx, match, replace) { 71 | "use strict"; 72 | return str.substring(0, idx) + str.substring(idx).replace(match, replace); 73 | } 74 | 75 | function matchReplace(pattern, replacePattern, str) { 76 | "use strict"; 77 | var matchData; 78 | if (pattern.matched !== undefined && pattern.freeVars !== undefined) { 79 | // accept match objects. 80 | matchData = pattern; 81 | } else { 82 | matchData = match(pattern, str); 83 | } 84 | 85 | if (!matchData.matched) { 86 | // If the pattern didn't match. 87 | return str; 88 | } 89 | 90 | // Plug in the freevars in place of the stars. 91 | var starGroups = replacePattern.match(/\*+/g) || []; 92 | var currentStarGroupIdx = 0; 93 | var freeVar; 94 | var freeVarGroup; 95 | starGroups.forEach(function(starGroup) { 96 | freeVarGroup = matchData.freeVars[starGroup.length] || []; 97 | freeVar = freeVarGroup.shift(); 98 | freeVar = freeVar === undefined ? starGroup : freeVar; 99 | replacePattern = replaceAfter(replacePattern, currentStarGroupIdx, starGroup, freeVar); 100 | currentStarGroupIdx = replacePattern.indexOf(freeVar) + freeVar.length; 101 | }); 102 | 103 | return replacePattern; 104 | } 105 | 106 | if (typeof module === "object" && module.exports) { 107 | module.exports = matchReplace; 108 | } 109 | -------------------------------------------------------------------------------- /src/ui/util.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | /* globals $, chrome */ 5 | 6 | const app = window.app; 7 | const ui = app.ui; 8 | const util = {}; 9 | 10 | util.instanceTemplate = function(template) { 11 | // We have to fetch first child (the container element) because 12 | // document fragments are not supported by JQ 13 | const newDom = document.importNode(template[0].content, true).children[0]; 14 | const switchDiv = newDom.querySelector(".switch"); 15 | if (switchDiv) { 16 | switchDiv.appendChild(app.createOnOffSwitch()); 17 | } 18 | const notFoundSwitchDiv = newDom.querySelector(".notFoundSwitch"); 19 | if (notFoundSwitchDiv) { 20 | notFoundSwitchDiv.appendChild(app.create404Switch()); 21 | } 22 | return $(newDom); 23 | }; 24 | 25 | util.deleteButtonIsSure = function(deleteBtn) { 26 | if (!deleteBtn.data("isSure")) { 27 | deleteBtn.data("isSure", 1); 28 | deleteBtn.css("font-size", "10px"); 29 | deleteBtn.text("Sure?"); 30 | return false; 31 | } 32 | return true; 33 | }; 34 | 35 | util.deleteButtonIsSureReset = function(deleteBtn) { 36 | deleteBtn.text("\u00d7"); 37 | deleteBtn.css("font-size", "28px"); 38 | deleteBtn.data("isSure", 0); 39 | }; 40 | 41 | util.debounce = function(fn, amt) { 42 | let timeout; 43 | return function() { 44 | if (timeout) { 45 | clearTimeout(timeout); 46 | } 47 | timeout = setTimeout(fn, amt); 48 | }; 49 | }; 50 | 51 | util.getNextId = function(jqResults, prefix) { 52 | let maxId = 0; 53 | jqResults.each(function(idx, el) { 54 | const id = parseInt(el.id.substring(1), 10); 55 | if (!isNaN(id) && id > maxId) { 56 | maxId = id; 57 | } 58 | }); 59 | return prefix.charAt(0) + (maxId + 1); 60 | }; 61 | 62 | util.makeFieldRequired = function(input) { 63 | const checkRequiredField = function() { 64 | if (input.val() === "") { 65 | input.css("background", "#ffaaaa"); 66 | } else { 67 | input.css("background", "#ffffff"); 68 | } 69 | }; 70 | input.on("keyup", checkRequiredField); 71 | checkRequiredField(); 72 | }; 73 | 74 | util.shortenString = function(str, limit) { 75 | const over = str.length - limit; 76 | if (over > 0) { 77 | const halfPos = str.length / 2; 78 | const firstOffset = Math.floor(over / 2 + 2); 79 | const secondOffset = Math.ceil(over / 2 + 3); 80 | return str.substring(0, halfPos - firstOffset) + " ... " + 81 | str.substring(halfPos + secondOffset); 82 | } 83 | return str; 84 | }; 85 | 86 | util.getTabResources = function(cb) { 87 | if (chrome.devtools && chrome.devtools.inspectedWindow && chrome.devtools.inspectedWindow.getResources) { 88 | chrome.devtools.inspectedWindow.getResources(function(resourceList) { 89 | if (resourceList) { 90 | const filteredList = resourceList.filter(function(resource) { 91 | const url = resource.url.trim(); 92 | if (url) { 93 | const validStart = (/^http/).test(url); 94 | const invalidFormat = (/\.jpg$|\.jpeg$|\.gif$|\.png$/).test(url); 95 | return validStart && !invalidFormat; 96 | } 97 | return false; 98 | }).map(function(resource) { 99 | return resource.url; 100 | }); 101 | cb(filteredList.sort()); 102 | } 103 | }); 104 | } else { 105 | app.mainSuggest.setShouldSuggest(false); 106 | } 107 | }; 108 | 109 | util.showToast = function(message) { 110 | ui.generalToast.html(message); 111 | ui.generalToast.fadeIn(); 112 | setTimeout(function() { 113 | ui.generalToast.fadeOut(); 114 | }, 3500); 115 | }; 116 | 117 | util.isChrome = function() { 118 | return navigator.userAgent.indexOf("Chrome") > -1; 119 | }; 120 | 121 | app.util = util; 122 | })(); 123 | -------------------------------------------------------------------------------- /src/ui/devtoolstab.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | /* globals chrome */ 5 | 6 | const app = window.app; 7 | const ui = app.ui; 8 | const util = app.util; 9 | 10 | app.mainSuggest = app.suggest(); 11 | app.requestHeadersSuggest = app.suggest(); 12 | app.responseHeadersSuggest = app.suggest(); 13 | app.files = {}; 14 | app.skipNextSync = false; 15 | 16 | function renderData() { 17 | app.files = {}; 18 | ui.domainDefs.children().remove(); 19 | chrome.runtime.sendMessage({action: "getDomains"}, function(domains) { 20 | if (domains.length) { 21 | domains.forEach(function(domain) { 22 | const domainMarkup = app.createDomainMarkup(domain); 23 | ui.domainDefs.append(domainMarkup); 24 | }); 25 | } else { 26 | const newDomain = app.createDomainMarkup({rules: [{type: "normalOverride"}]}); 27 | ui.domainDefs.append(newDomain); 28 | newDomain.find(".domainMatchInput").val("*"); 29 | chrome.runtime.sendMessage({ 30 | action: "saveDomain", 31 | data: app.getDomainData(newDomain) 32 | }); 33 | app.skipNextSync = true; 34 | } 35 | util.getTabResources(function(res) { 36 | app.mainSuggest.fillOptions(res); 37 | }); 38 | }); 39 | } 40 | 41 | function setupSynchronizeConnection() { 42 | chrome.runtime.sendMessage({action: "syncMe"}, function() { 43 | if (!app.skipNextSync) { 44 | renderData(); 45 | } 46 | app.skipNextSync = false; 47 | setupSynchronizeConnection(); 48 | }); 49 | } 50 | 51 | function init() { 52 | app.mainSuggest.init(); 53 | app.requestHeadersSuggest.init(); 54 | app.responseHeadersSuggest.init(); 55 | app.requestHeadersSuggest.fillOptions(app.headersLists.requestHeaders); 56 | app.responseHeadersSuggest.fillOptions(app.headersLists.responseHeaders); 57 | 58 | setupSynchronizeConnection(); 59 | 60 | renderData(); 61 | 62 | ui.addDomainBtn.on("click", function() { 63 | const newDomain = app.createDomainMarkup(); 64 | newDomain.find(".domainMatchInput").val("*"); 65 | ui.domainDefs.append(newDomain); 66 | chrome.runtime.sendMessage({action: "saveDomain", data: app.getDomainData(newDomain)}); 67 | app.skipNextSync = true; 68 | }); 69 | 70 | ui.helpBtn.on("click", function() { 71 | ui.helpOverlay.toggle(); 72 | }); 73 | 74 | ui.helpCloseBtn.on("click", function() { 75 | ui.helpOverlay.hide(); 76 | }); 77 | 78 | if (!chrome.devtools) { 79 | ui.showSuggestions.hide(); 80 | ui.showSuggestionsText.hide(); 81 | chrome.runtime.sendMessage({ 82 | action: "getSetting", 83 | setting: "tabPageNotice" 84 | }, function(data) { 85 | 86 | if (data !== "true") { 87 | ui.tabPageNotice.find("a").on("click", function(e) { 88 | e.preventDefault(); 89 | chrome.runtime.sendMessage({ 90 | action: "setSetting", 91 | setting: "tabPageNotice", 92 | value: "true" 93 | }); 94 | ui.tabPageNotice.fadeOut(); 95 | }); 96 | ui.tabPageNotice.fadeIn(); 97 | setTimeout(function() { 98 | ui.tabPageNotice.fadeOut(); 99 | }, 6000); 100 | } 101 | }); 102 | } 103 | 104 | if (navigator.userAgent.indexOf("Firefox") > -1 && !!chrome.devtools) { 105 | // Firefox is really broken with the "/" and "'" keys. They just dont work. 106 | // So try to fix them here.. wow.. just wow. I can't believe I'm fixing the ability to type. 107 | const brokenKeys = { "/": 1, "?": 1, "'": 1, '"': 1 }; 108 | window.addEventListener("keydown", e => { 109 | const brokenKey = brokenKeys[e.key]; 110 | const activeEl = document.activeElement; 111 | if (brokenKey && (activeEl.nodeName === "INPUT" || activeEl.nodeName === "TEXTAREA") && 112 | activeEl.className !== "ace_text-input") { 113 | 114 | e.preventDefault(); 115 | const start = activeEl.selectionStart; 116 | const end = activeEl.selectionEnd; 117 | activeEl.value = activeEl.value.substring(0, start) + e.key + 118 | activeEl.value.substring(end, activeEl.value.length); 119 | activeEl.selectionStart = start + 1; 120 | activeEl.selectionEnd = start + 1; 121 | } 122 | }); 123 | } 124 | } 125 | 126 | init(); 127 | 128 | })(); 129 | -------------------------------------------------------------------------------- /src/ui/headerRule.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | /* globals $ */ 5 | 6 | const app = window.app; 7 | const util = app.util; 8 | const ui = app.ui; 9 | 10 | // This is what shows up *outside* the header editor. 11 | function createHeaderRuleMarkup(savedData, saveFunc) { 12 | savedData = savedData || {}; 13 | saveFunc = saveFunc || function() {}; 14 | 15 | const override = util.instanceTemplate(ui.headerRuleTemplate); 16 | const matchInput = override.find(".matchInput"); 17 | const requestRulesInput = override.find(".requestRules"); 18 | const responseRulesInput = override.find(".responseRules"); 19 | const editBtn = override.find(".edit-btn"); 20 | const ruleOnOff = override.find(".onoffswitch"); 21 | const deleteBtn = override.find(".sym-btn"); 22 | 23 | matchInput.val(savedData.match || ""); 24 | util.makeFieldRequired(matchInput); 25 | 26 | const updateHeaderInput = function(input, ruleStr) { 27 | input.val(decodeURIComponent(ruleStr.replace(/\;/g, "; "))); 28 | input.attr("title", decodeURIComponent(ruleStr.replace(/\;/g, "\n"))); 29 | input.data("rules", ruleStr); 30 | }; 31 | 32 | updateHeaderInput(requestRulesInput, savedData.requestRules || ""); 33 | updateHeaderInput(responseRulesInput, savedData.responseRules || ""); 34 | 35 | ruleOnOff[0].isOn = savedData.on === false ? false : true; 36 | 37 | if (savedData.on === false) { 38 | override.addClass("disabled"); 39 | } 40 | 41 | const editorSaveFunc = function() { 42 | const rules = app.headerEditor.getRules(); 43 | updateHeaderInput(requestRulesInput, rules.requestRules.join(";")); 44 | updateHeaderInput(responseRulesInput, rules.responseRules.join(";")); 45 | saveFunc(); 46 | }; 47 | 48 | const editFunc = function() { 49 | const reqStr = requestRulesInput.data("rules") || ""; 50 | const resStr = responseRulesInput.data("rules") || ""; 51 | app.headerEditor.open(reqStr, resStr, matchInput.val(), editorSaveFunc); 52 | }; 53 | 54 | app.mainSuggest.init(matchInput); 55 | 56 | matchInput.on("keyup", saveFunc); 57 | 58 | override.on("click", function(e) { 59 | if ($(e.target).hasClass("headerRuleInput")) { 60 | editFunc(); 61 | } 62 | }); 63 | editBtn.on("click", editFunc); 64 | 65 | deleteBtn.on("click", function() { 66 | if (!util.deleteButtonIsSure(deleteBtn)) { 67 | return; 68 | } 69 | override.css("transition", "none"); 70 | override.fadeOut(function() { 71 | override.remove(); 72 | saveFunc(); 73 | }); 74 | }); 75 | 76 | deleteBtn.on("mouseout", function() { 77 | util.deleteButtonIsSureReset(deleteBtn); 78 | }); 79 | 80 | ruleOnOff.on("click change", function() { 81 | override.toggleClass("disabled", !ruleOnOff[0].isOn); 82 | saveFunc(); 83 | }); 84 | 85 | return override; 86 | } 87 | 88 | // This is a header rule that shows up *inside* the editor. 89 | function createHeaderEditorRuleMarkup(savedData, saveFunc, type) { 90 | savedData = savedData || {operation: "set"}; 91 | saveFunc = saveFunc || function() {}; 92 | 93 | const override = util.instanceTemplate(ui.headerEditorRuleTemplate); 94 | const operation = override.find(".operationSelect"); 95 | const headerName = override.find(".headerName"); 96 | const headerValue = override.find(".headerValue"); 97 | const deleteBtn = override.find(".sym-btn"); 98 | 99 | operation.val(savedData.operation); 100 | headerName.val(savedData.header); 101 | headerValue.val(savedData.value || ""); 102 | 103 | if (savedData.operation === "remove") { 104 | headerValue[0].disabled = true; 105 | } 106 | 107 | operation.on("change", function() { 108 | if (operation.val() === "remove") { 109 | headerValue.val(""); 110 | headerValue[0].disabled = true; 111 | } else { 112 | headerValue[0].disabled = false; 113 | } 114 | saveFunc(); 115 | }); 116 | headerName.on("keyup", saveFunc); 117 | headerValue.on("keyup", saveFunc); 118 | 119 | deleteBtn.on("click", function() { 120 | if (!util.deleteButtonIsSure(deleteBtn)) { 121 | return; 122 | } 123 | override.css("transition", "none"); 124 | override.fadeOut(function() { 125 | override.remove(); 126 | saveFunc(); 127 | }); 128 | }); 129 | 130 | deleteBtn.on("mouseout", function() { 131 | util.deleteButtonIsSureReset(deleteBtn); 132 | }); 133 | 134 | if (type === "request") { 135 | app.requestHeadersSuggest.init(headerName, false, true); 136 | } else { 137 | app.responseHeadersSuggest.init(headerName, false, true); 138 | } 139 | 140 | return override; 141 | } 142 | 143 | app.createHeaderRuleMarkup = createHeaderRuleMarkup; 144 | app.createHeaderEditorRuleMarkup = createHeaderEditorRuleMarkup; 145 | 146 | })(); 147 | -------------------------------------------------------------------------------- /src/background/headerHandling.js: -------------------------------------------------------------------------------- 1 | /* global bgapp, match */ 2 | { 3 | bgapp.makeHeaderHandler = function(type) { 4 | return function (details) { 5 | if (details.tabId > -1 && type === "response" && details.statusCode === 404) { 6 | let tabUrl = bgapp.tabUrlTracker.getUrlFromId(details.tabId); 7 | if (details.type === "main_frame") { 8 | // a new tab must have just been created. 9 | tabUrl = details.url; 10 | } 11 | if (tabUrl) { 12 | return bgapp.handleRequest(details.url, tabUrl, details.tabId, details.requestId, details.statusCode); 13 | } 14 | } 15 | const headers = details[type + "Headers"]; 16 | if (details.tabId > -1 && headers) { 17 | let tabUrl = bgapp.tabUrlTracker.getUrlFromId(details.tabId); 18 | if (details.type === "main_frame") { 19 | // a new tab must have just been created. 20 | tabUrl = details.url; 21 | } 22 | if (tabUrl) { 23 | return handleHeaders(type, details.url, tabUrl, headers, details.tabId); 24 | } 25 | } 26 | }; 27 | }; 28 | 29 | const parseHeaderDataStr = function(headerDataStr) { 30 | const ans = []; 31 | const rules = headerDataStr.split(";"); 32 | const len = rules.length; 33 | for (let x = 0; x < len; x++) { 34 | const rule = rules[x]; 35 | const ruleParts = rule.split(": "); 36 | if (ruleParts[0].indexOf("set") === 0) { 37 | if (ruleParts.length === 2) { 38 | ans.push({ 39 | operation: "set", 40 | name: decodeURIComponent(ruleParts[0].substring(4)).toLowerCase(), 41 | value: decodeURIComponent(ruleParts[1]) 42 | }); 43 | } 44 | } else if (ruleParts[0].indexOf("remove") === 0) { 45 | ans.push({ 46 | operation: "remove", 47 | name: decodeURIComponent(ruleParts[0].substring(7)).toLowerCase() 48 | }); 49 | } 50 | } 51 | return ans; 52 | }; 53 | 54 | const makeHeadersObject = function(headers) { 55 | const ans = {}; 56 | const len = headers.length; 57 | for (let x = 0; x < len; x++) { 58 | const header = headers[x]; 59 | ans[header.name.toLowerCase()] = header; 60 | } 61 | return ans; 62 | }; 63 | 64 | const processRule = function(ruleObj, type, requestUrl, tabUrl, headers, tabId) { 65 | const headerObjToReturn = {}; 66 | const headerObjToReturnKey = type + "Headers"; 67 | headerObjToReturn[headerObjToReturnKey] = headers; 68 | if (ruleObj.on && ruleObj.type === "headerRule" && match(ruleObj.match, requestUrl).matched) { 69 | const rulesStr = ruleObj[type + "Rules"]; 70 | bgapp.util.logOnTab(tabId, "Header Rule Matched: " + requestUrl + 71 | " applying rules: " + rulesStr, true); 72 | const headerRules = parseHeaderDataStr(rulesStr); 73 | const headersObj = makeHeadersObject(headers); 74 | const numRules = headerRules.length; 75 | for (let t = 0; t < numRules; t++) { 76 | const rule = headerRules[t]; 77 | if (rule.operation === "set") { 78 | headersObj[rule.name] = { 79 | name: rule.name, 80 | value: rule.value 81 | }; 82 | } else { 83 | if (headersObj[rule.name]) { 84 | headersObj[rule.name] = undefined; 85 | } 86 | } 87 | } 88 | const newHeaders = []; 89 | for (const headerKey in headersObj) { 90 | const header = headersObj[headerKey]; 91 | if (header) { 92 | newHeaders.push(header); 93 | } 94 | } 95 | headerObjToReturn[headerObjToReturnKey] = newHeaders; 96 | return headerObjToReturn; 97 | } 98 | }; 99 | 100 | const processTabGroup = function(domainObj, type, requestUrl, tabUrl, headers, tabId) { 101 | const headerObjToReturn = {}; 102 | const headerObjToReturnKey = type + "Headers"; 103 | headerObjToReturn[headerObjToReturnKey] = headers; 104 | if (domainObj.on && match(domainObj.matchUrl, tabUrl).matched) { 105 | const rules = domainObj.rules || []; 106 | for (let x = 0, len = rules.length; x < len; ++x) { 107 | const ruleObj = rules[x]; 108 | const ruleResults = processRule(ruleObj, type, requestUrl, tabUrl, headers, tabId); 109 | if (ruleResults) { 110 | return ruleResults; 111 | } 112 | } 113 | } 114 | return headerObjToReturn; 115 | }; 116 | 117 | const handleHeaders = function(type, requestUrl, tabUrl, headers, tabId) { 118 | const headerObjToReturn = {}; 119 | const headerObjToReturnKey = type + "Headers"; 120 | headerObjToReturn[headerObjToReturnKey] = headers; 121 | for (const key in bgapp.ruleDomains) { 122 | const domainObj = bgapp.ruleDomains[key]; 123 | return processTabGroup(domainObj, type, requestUrl, tabUrl, headers, tabId); 124 | } 125 | return headerObjToReturn; 126 | }; 127 | } 128 | -------------------------------------------------------------------------------- /src/ui/headerEditor.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | /* globals $ */ 5 | 6 | const app = window.app; 7 | const ui = app.ui; 8 | 9 | let saveFunc; 10 | 11 | function getRule(el) { 12 | const $el = $(el); 13 | const operation = $el.find(".operationSelect").val(); 14 | const headerName = encodeURIComponent($el.find(".headerName").val()); 15 | const headerValue = encodeURIComponent($el.find(".headerValue").val()); 16 | let nextRule; 17 | if (headerName) { 18 | if (operation === "set") { 19 | nextRule = "set "; 20 | nextRule += headerName + ": "; 21 | nextRule += headerValue; 22 | } else { 23 | nextRule = "remove "; 24 | nextRule += headerName; 25 | } 26 | } 27 | return nextRule; 28 | } 29 | 30 | function getHeaderEditRules() { 31 | const reqRules = []; 32 | const resRules = []; 33 | const $requestRules = ui.headerRequestRulesContainer.find(".headerEditorRule"); 34 | const $responseRules = ui.headerResponseRulesContainer.find(".headerEditorRule"); 35 | 36 | $requestRules.each(function(idx, el) { 37 | const rule = getRule(el); 38 | if (rule) { 39 | reqRules.push(rule); 40 | } 41 | }); 42 | $responseRules.each(function(idx, el) { 43 | const rule = getRule(el); 44 | if (rule) { 45 | resRules.push(rule); 46 | } 47 | }); 48 | return { 49 | requestRules: reqRules, 50 | responseRules: resRules 51 | }; 52 | } 53 | 54 | function parseHeaderDataStr(headerDataStr) { 55 | const ans = []; 56 | const rules = headerDataStr.split(";"); 57 | rules.forEach(function(rule) { 58 | const ruleParts = rule.split(": "); 59 | if (ruleParts[0].indexOf("set") === 0) { 60 | if (ruleParts.length === 2) { 61 | ans.push({ 62 | operation: "set", 63 | header: decodeURIComponent(ruleParts[0].substring(4)), 64 | value: decodeURIComponent(ruleParts[1]) 65 | }); 66 | } 67 | } else if (ruleParts[0].indexOf("remove") === 0) { 68 | ans.push({ 69 | operation: "remove", 70 | header: decodeURIComponent(ruleParts[0].substring(7)) 71 | }); 72 | } 73 | }); 74 | return ans; 75 | } 76 | 77 | function openHeaderEditor(requestHeaderDataStr, responseHeaderDataStr, matchRule, saveFunction) { 78 | saveFunc = saveFunction; 79 | const requestHeadersData = parseHeaderDataStr(requestHeaderDataStr); 80 | const responseHeadersData = parseHeaderDataStr(responseHeaderDataStr); 81 | ui.headerRequestRulesContainer.html(""); 82 | ui.headerResponseRulesContainer.html(""); 83 | requestHeadersData.forEach(function(data) { 84 | ui.headerRequestRulesContainer.append(app.createHeaderEditorRuleMarkup(data, saveFunc, "request")); 85 | }); 86 | responseHeadersData.forEach(function(data) { 87 | ui.headerResponseRulesContainer.append(app.createHeaderEditorRuleMarkup(data, saveFunc, "response")); 88 | }); 89 | ui.headerMatchContainer.text(matchRule || ""); 90 | ui.headerRuleOverlay.css("display", "flex"); 91 | ui.body.css("overflow", "hidden"); 92 | } 93 | 94 | ui.closeHeaderRuleEditorBtn.on("click", function() { 95 | ui.headerRuleOverlay.hide(); 96 | ui.body.css("overflow", "auto"); 97 | }); 98 | 99 | ui.addRequestHeaderBtn.on("click", function() { 100 | ui.headerRequestRulesContainer.append(app.createHeaderEditorRuleMarkup(undefined, saveFunc, "request")); 101 | }); 102 | 103 | ui.addResponseHeaderBtn.on("click", function() { 104 | ui.headerResponseRulesContainer.append(app.createHeaderEditorRuleMarkup(undefined, saveFunc, "response")); 105 | }); 106 | 107 | const resPresets = { 108 | cors: [{ 109 | operation: "set", 110 | header: "Access-Control-Allow-Origin", 111 | value: "*" 112 | }], 113 | noInline: [{ 114 | operation: "set", 115 | header: "Content-Security-Policy", 116 | value: "script-src * 'nonce-ResourceOverride'" 117 | }], 118 | allowFrames: [{ 119 | operation: "remove", 120 | header: "X-Frame-Options" 121 | }], 122 | allowContent: [{ 123 | operation: "remove", 124 | header: "Content-Security-Policy" 125 | }, { 126 | operation: "remove", 127 | header: "X-Content-Security-Policy" 128 | }] 129 | }; 130 | 131 | ui.headerPresetsSelect.on("change", function() { 132 | const presetName = ui.headerPresetsSelect.val(); 133 | const presets = resPresets[presetName]; 134 | ui.headerPresetsSelect.val(""); 135 | if (presets) { 136 | // Right now only response presets are allowed. 137 | presets.forEach(function(preset) { 138 | const markup = app.createHeaderEditorRuleMarkup(preset, saveFunc, "response"); 139 | ui.headerResponseRulesContainer.append(markup); 140 | }); 141 | saveFunc(); 142 | } 143 | }); 144 | 145 | app.headerEditor = { 146 | open: openHeaderEditor, 147 | getRules: getHeaderEditRules 148 | }; 149 | 150 | })(); 151 | -------------------------------------------------------------------------------- /src/background/background.js: -------------------------------------------------------------------------------- 1 | /* globals chrome, bgapp, unescape, match */ 2 | { 3 | bgapp.ruleDomains = {}; 4 | bgapp.syncFunctions = []; 5 | 6 | const simpleError = bgapp.util.simpleError; 7 | 8 | // Called when the user clicks on the browser action icon. 9 | chrome.browserAction.onClicked.addListener(function() { 10 | // open or focus options page. 11 | const optionsUrl = chrome.runtime.getURL("src/ui/devtoolstab.html"); 12 | chrome.tabs.query({}, function(extensionTabs) { 13 | let found = false; 14 | for (let i = 0, len = extensionTabs.length; i < len; i++) { 15 | if (optionsUrl === extensionTabs[i].url) { 16 | found = true; 17 | chrome.tabs.update(extensionTabs[i].id, {selected: true}); 18 | break; 19 | } 20 | } 21 | if (found === false) { 22 | chrome.tabs.create({url: optionsUrl}); 23 | } 24 | }); 25 | }); 26 | 27 | const syncAllInstances = function() { 28 | // Doing this weird dance because I cant figure out how to 29 | // send data from this script to the dev tools script. 30 | // Nothing seems to work (even the examples!). 31 | bgapp.syncFunctions.forEach(function(fn) { 32 | try { 33 | fn(); 34 | } catch (e) { /**/ } 35 | }); 36 | bgapp.syncFunctions = []; 37 | }; 38 | 39 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { 40 | if (request.action === "saveDomain") { 41 | bgapp.mainStorage.put(request.data) 42 | .then(syncAllInstances) 43 | .catch(simpleError); 44 | bgapp.ruleDomains[request.data.id] = request.data; 45 | } else if (request.action === "getDomains") { 46 | bgapp.mainStorage.getAll().then(function(domains) { 47 | sendResponse(domains || []); 48 | }).catch(simpleError); 49 | } else if (request.action === "deleteDomain") { 50 | bgapp.mainStorage.delete(request.id) 51 | .then(syncAllInstances) 52 | .catch(simpleError); 53 | delete bgapp.ruleDomains[request.id]; 54 | } else if (request.action === "import") { 55 | let maxId = 0; 56 | for (const id in bgapp.ruleDomains) { 57 | maxId = Math.max(maxId, parseInt(id.substring(1))); 58 | } 59 | maxId++; 60 | Promise.all(request.data.map(function(domainData) { 61 | // dont overwrite any pre-existing domains. 62 | domainData.id = "d" + maxId++; 63 | bgapp.ruleDomains[domainData.id] = domainData; 64 | return bgapp.mainStorage.put(domainData); 65 | })) 66 | .then(syncAllInstances) 67 | .catch(simpleError); 68 | } else if (request.action === "makeGetRequest") { 69 | const xhr = new XMLHttpRequest(); 70 | xhr.open("GET", request.url, true); 71 | xhr.onreadystatechange = function() { 72 | if (xhr.readyState === 4) { 73 | sendResponse(xhr.responseText); 74 | } 75 | }; 76 | xhr.send(); 77 | } else if (request.action === "setSetting") { 78 | localStorage[request.setting] = request.value; 79 | } else if (request.action === "getSetting") { 80 | sendResponse(localStorage[request.setting]); 81 | } else if (request.action === "syncMe") { 82 | bgapp.syncFunctions.push(sendResponse); 83 | } else if (request.action === "match") { 84 | sendResponse(match(request.domainUrl, request.windowUrl).matched); 85 | } else if (request.action === "extractMimeType") { 86 | sendResponse(bgapp.extractMimeType(request.fileName, request.file)); 87 | } 88 | 89 | // !!!Important!!! Need to return true for sendResponse to work. 90 | return true; 91 | }); 92 | 93 | chrome.webRequest.onBeforeRequest.addListener(function(details) { 94 | if (!bgapp.requestIdTracker.has(details.requestId)) { 95 | if (details.tabId > -1) { 96 | let tabUrl = bgapp.tabUrlTracker.getUrlFromId(details.tabId); 97 | if (details.type === "main_frame") { 98 | // a new tab must have just been created. 99 | tabUrl = details.url; 100 | } 101 | if (tabUrl) { 102 | const result = bgapp.handleRequest(details.url, tabUrl, details.tabId, details.requestId); 103 | if (result) { 104 | // make sure we don't try to redirect again. 105 | bgapp.requestIdTracker.push(details.requestId); 106 | } 107 | return result; 108 | } 109 | } 110 | } 111 | }, { 112 | urls: [""] 113 | }, ["blocking"]); 114 | 115 | chrome.webRequest.onHeadersReceived.addListener(bgapp.makeHeaderHandler("response"), { 116 | urls: [""] 117 | }, ["blocking", "responseHeaders"]); 118 | 119 | chrome.webRequest.onBeforeSendHeaders.addListener(bgapp.makeHeaderHandler("request"), { 120 | urls: [""] 121 | }, ["blocking", "requestHeaders"]); 122 | 123 | //init settings 124 | if (localStorage.devTools === undefined) { 125 | localStorage.devTools = "true"; 126 | } 127 | if (localStorage.showSuggestions === undefined) { 128 | localStorage.showSuggestions = "true"; 129 | } 130 | if (localStorage.showLogs === undefined) { 131 | localStorage.showLogs = "false"; 132 | } 133 | 134 | // init domain storage 135 | bgapp.mainStorage.getAll().then(function(domains) { 136 | if (domains) { 137 | domains.forEach(function(domainObj) { 138 | bgapp.ruleDomains[domainObj.id] = domainObj; 139 | }); 140 | } 141 | }).catch(simpleError); 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/ui/moveableRules.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | const app = window.app; 5 | 6 | function moveableRules(parent, handleSelector) { 7 | const placeholder = document.createElement("div"); 8 | const handles = Array.prototype.slice.call(parent.querySelectorAll(handleSelector)); 9 | let children; 10 | let mouseDown = false; 11 | let currentMovingEl; 12 | let offsetY; 13 | let currentIndex; 14 | let grid; 15 | let onMove = function() {}; 16 | placeholder.className = "sortable-placeholder"; 17 | 18 | const regetChildren = function() { 19 | children = Array.prototype.slice.call(parent.children); 20 | }; 21 | regetChildren(); 22 | 23 | const setBorders = function() { 24 | children.forEach(function(el) { 25 | el.style.background = "transparent"; 26 | }); 27 | }; 28 | 29 | const getElFullHeight = function(el) { 30 | const innerHeight = el.getBoundingClientRect().height; 31 | const compStyle = window.getComputedStyle(el); 32 | return innerHeight + parseInt(compStyle["margin-top"]); 33 | }; 34 | 35 | const getChildIndex = function(el) { 36 | const children = parent.children; 37 | let idx = children.length; 38 | while (idx-- > 0) { 39 | if (children[idx] === el) { 40 | return idx; 41 | } 42 | } 43 | return null; 44 | }; 45 | 46 | const getQualifiedChild = function(el) { 47 | let lastEl; 48 | while (el && el !== parent) { 49 | lastEl = el; 50 | el = el.parentElement; 51 | } 52 | return lastEl; 53 | }; 54 | 55 | const after = function(parent, newEl, targetEl) { 56 | const nextSib = targetEl.nextElementSibling; 57 | if (!nextSib) { 58 | parent.appendChild(newEl); 59 | } else { 60 | parent.insertBefore(newEl, nextSib); 61 | } 62 | }; 63 | 64 | const remove = function(el) { 65 | el.parentElement.removeChild(el); 66 | }; 67 | 68 | const makeGrid = function() { 69 | grid = []; 70 | regetChildren(); 71 | let currentOffset = 0; 72 | children.forEach(function(el) { 73 | if (el !== currentMovingEl) { 74 | currentOffset += getElFullHeight(el); 75 | grid.push({ 76 | el: el, 77 | offset: currentOffset 78 | }); 79 | } 80 | }); 81 | }; 82 | 83 | const getElFromGridWithY = function(y) { 84 | if (y < 0) { 85 | return null; 86 | } 87 | for (let x = 0, len = grid.length; x < len; ++x) { 88 | if (y < grid[x].offset) { 89 | return grid[x].el; 90 | } 91 | } 92 | return null; 93 | }; 94 | 95 | let lastDropTarget; 96 | document.addEventListener("mousemove", function(e) { 97 | if (mouseDown) { 98 | e.preventDefault(); 99 | currentMovingEl.style.top = e.pageY - offsetY + "px"; 100 | const mouseParentY = e.clientY - parent.getBoundingClientRect().top; 101 | const dropTarget = getElFromGridWithY(mouseParentY); 102 | if (dropTarget !== lastDropTarget) { 103 | setBorders(); 104 | if (dropTarget) { 105 | dropTarget.style.background = "#dddddd"; 106 | } 107 | lastDropTarget = dropTarget; 108 | } 109 | } 110 | }); 111 | 112 | document.addEventListener("mouseup", function(e) { 113 | mouseDown = false; 114 | if (currentMovingEl) { 115 | const mouseParentY = e.clientY - parent.getBoundingClientRect().top; 116 | const dropTarget = getElFromGridWithY(mouseParentY) || placeholder; 117 | const dropIndex = getChildIndex(dropTarget); 118 | if (dropIndex > currentIndex) { 119 | after(parent, currentMovingEl, dropTarget); 120 | } else { 121 | parent.insertBefore(currentMovingEl, dropTarget); 122 | } 123 | // this prevents a reflow from changing scroll position. 124 | setTimeout(function() { 125 | remove(placeholder); 126 | }, 1); 127 | 128 | currentMovingEl.style.position = ""; 129 | currentMovingEl.style.width = ""; 130 | currentMovingEl.style.height = ""; 131 | currentMovingEl.style.opacity = ""; 132 | currentMovingEl = null; 133 | setBorders(); 134 | onMove(); 135 | } 136 | }); 137 | 138 | const assignHandleListener = function(handle) { 139 | const el = getQualifiedChild(handle); 140 | const compStyle = window.getComputedStyle(el); 141 | 142 | handle.addEventListener("mousedown", function(e) { 143 | const boundingRect = el.getBoundingClientRect(); 144 | mouseDown = true; 145 | currentMovingEl = el; 146 | currentIndex = getChildIndex(el); 147 | offsetY = e.offsetY + parseInt(compStyle["margin-top"]); 148 | el.style.position = "absolute"; 149 | el.parentElement.insertBefore(placeholder, el); 150 | el.style.width = boundingRect.width + "px"; 151 | el.style.height = boundingRect.height + "px"; 152 | el.style.top = e.pageY - offsetY + "px"; 153 | el.style.opacity = 0.5; 154 | makeGrid(); 155 | }); 156 | }; 157 | 158 | handles.forEach(assignHandleListener); 159 | 160 | return { 161 | assignHandleListener: assignHandleListener, 162 | onMove: function(fn) { 163 | onMove = fn; 164 | } 165 | }; 166 | } 167 | 168 | app.moveableRules = moveableRules; 169 | })(); 170 | -------------------------------------------------------------------------------- /src/ui/editor.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | /* globals $, chrome, ace, js_beautify */ 5 | 6 | const app = window.app; 7 | const ui = app.ui; 8 | const util = app.util; 9 | 10 | let editor; 11 | let editingFile; 12 | let saveFunc; 13 | 14 | function updateSaveButtons(edited) { 15 | if (edited) { 16 | ui.fileSaveAndCloseBtn.css("color", "#ff0000"); 17 | ui.fileSaveBtn.css("color", "#ff0000"); 18 | } else { 19 | ui.fileSaveAndCloseBtn.css("color", "#000000"); 20 | ui.fileSaveBtn.css("color", "#000000"); 21 | } 22 | } 23 | 24 | function setEditorVal(str) { 25 | editor.off("change", updateSaveButtons); 26 | editor.setValue(str); 27 | editor.gotoLine(0, 0, false); 28 | editor.on("change", updateSaveButtons); 29 | } 30 | 31 | function editorGuessMode(fileName, file) { 32 | chrome.runtime.sendMessage({ 33 | action: "extractMimeType", 34 | file: file, 35 | fileName: fileName 36 | }, function(data) { 37 | const mimeToEditorSyntax = { 38 | "text/javascript": "javascript", 39 | "text/html": "html", 40 | "text/css": "css", 41 | "text/xml": "xml" 42 | }; 43 | const mode = mimeToEditorSyntax[data.mime] || "javascript"; 44 | ui.syntaxSelect.val(mode); 45 | editor.getSession().setMode("ace/mode/" + mode); 46 | }); 47 | } 48 | 49 | function saveFile() { 50 | updateSaveButtons(); 51 | app.files[editingFile] = editor.getValue(); 52 | saveFunc(); 53 | } 54 | 55 | function saveFileAndClose() { 56 | saveFile(); 57 | ui.editorOverlay.hide(); 58 | ui.body.css("overflow", "auto"); 59 | } 60 | 61 | function setupEditor() { 62 | editor = ace.edit("editor"); 63 | editor.setTheme("ace/theme/monokai"); 64 | editor.setShowPrintMargin(false); 65 | 66 | editor.on("change", updateSaveButtons); 67 | editor.commands.addCommand({ 68 | name: "multiEdit", 69 | bindKey: { 70 | win: "Ctrl-D", 71 | mac: "Command-D" 72 | }, 73 | exec: function(editor, line) { 74 | editor.selectMore(1); 75 | }, 76 | readOnly: true 77 | }); 78 | editor.commands.addCommand({ 79 | name: "save", 80 | bindKey: { 81 | win: "Ctrl-S", 82 | mac: "Command-S" 83 | }, 84 | exec: function(editor, line) { 85 | saveFile(); 86 | }, 87 | readOnly: true 88 | }); 89 | editor.commands.addCommand({ 90 | name: "saveAndClose", 91 | bindKey: { 92 | win: "Ctrl-Shift-S", 93 | mac: "Command-Shift-S" 94 | }, 95 | exec: function(editor, line) { 96 | saveFileAndClose(); 97 | }, 98 | readOnly: true 99 | }); 100 | } 101 | 102 | function openEditor(fileId, match, isInjectFile, saveFunction) { 103 | saveFunc = saveFunction; 104 | editingFile = fileId; 105 | updateSaveButtons(); 106 | ui.editorOverlay.css("display", "flex"); 107 | ui.body.css("overflow", "hidden"); 108 | if (!editor) { 109 | setupEditor(); 110 | } 111 | match = match || ""; 112 | ui.editLabel.text(isInjectFile ? "Editing file:" : "Editing file for match:"); 113 | ui.matchContainer.text(match); 114 | 115 | editorGuessMode(match, app.files[fileId]); 116 | 117 | if (chrome.devtools && util.isChrome()) { 118 | ui.loadSelect.show(); 119 | util.getTabResources(function(filteredList) { 120 | ui.loadSelect.html(""); 121 | filteredList.forEach(function(url) { 122 | const $newOpt = $("