├── image ├── icon16.png ├── icon48.png ├── icon48.xcf ├── icon128.png ├── icon128.xcf ├── mouse-pointer.xcf ├── icon16-disabled.png ├── icon16-injected.png ├── screenshot-20180525.png ├── screenshot-20180525.xcf ├── screenshot-20180817.png └── make.sh ├── .gitignore ├── AUTHORS.md ├── GITHUB.txt ├── popup.css ├── popup.html ├── popup.js ├── options.js ├── test.html ├── settings.js ├── manifest.json ├── options.css ├── options.html ├── CHANGELOG.md ├── background.js ├── content.js ├── README.md ├── ui.js └── LICENSE /image/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-st/chrome-injectjs/HEAD/image/icon16.png -------------------------------------------------------------------------------- /image/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-st/chrome-injectjs/HEAD/image/icon48.png -------------------------------------------------------------------------------- /image/icon48.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-st/chrome-injectjs/HEAD/image/icon48.xcf -------------------------------------------------------------------------------- /image/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-st/chrome-injectjs/HEAD/image/icon128.png -------------------------------------------------------------------------------- /image/icon128.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-st/chrome-injectjs/HEAD/image/icon128.xcf -------------------------------------------------------------------------------- /image/mouse-pointer.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-st/chrome-injectjs/HEAD/image/mouse-pointer.xcf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.bak 3 | *.tmp 4 | *.pem 5 | *.crx 6 | image/screenshots.xcf 7 | image/icons.svg 8 | out/ 9 | -------------------------------------------------------------------------------- /image/icon16-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-st/chrome-injectjs/HEAD/image/icon16-disabled.png -------------------------------------------------------------------------------- /image/icon16-injected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-st/chrome-injectjs/HEAD/image/icon16-injected.png -------------------------------------------------------------------------------- /image/screenshot-20180525.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-st/chrome-injectjs/HEAD/image/screenshot-20180525.png -------------------------------------------------------------------------------- /image/screenshot-20180525.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-st/chrome-injectjs/HEAD/image/screenshot-20180525.xcf -------------------------------------------------------------------------------- /image/screenshot-20180817.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-st/chrome-injectjs/HEAD/image/screenshot-20180817.png -------------------------------------------------------------------------------- /image/make.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Automatically creates image versions, e.g., disabled icons 4 | 5 | convert icon16.png -colorspace Gray -modulate 250% icon16-disabled.png 6 | 7 | 8 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors/Contributors 2 | 3 | | Name | Contact | Dev | i18n | Test | Doc | Release | 4 | |---------------|------------------------------|:---:|:------:|:----:|:---:|:-------:| 5 | | André St. | | X | en, de | X | X | X | 6 | | ... | | | | | | | 7 | -------------------------------------------------------------------------------- /GITHUB.txt: -------------------------------------------------------------------------------- 1 | Repository Name: 2 | 3 | chrome-injectjs 4 | 5 | 6 | Description: 7 | 8 | Customize a remote website when it doesn't offer a native setting and developers too busy. This extension injects your Javascript-based modifications on every visit. 9 | 10 | 11 | Website: 12 | 13 | - 14 | 15 | 16 | Topics: 17 | 18 | chrome 19 | chrome-extension 20 | extension 21 | plugin 22 | chrome-plugin 23 | productivity 24 | injection 25 | injector 26 | mixins 27 | javascript-injection 28 | css-inject 29 | remote 30 | stylish 31 | stylus 32 | addon 33 | redirector 34 | redirection 35 | url-redirector 36 | url-redirection 37 | url-redirect 38 | -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- 1 | 2 | button 3 | { 4 | width: 100%; 5 | min-width: 7em; 6 | padding: 0.45em 0 0.45em 0.5em; 7 | margin: 0.25em 0; 8 | background-color: #e5e5e5; 9 | text-align: left; 10 | } 11 | 12 | #btnOnOff:after 13 | { 14 | content: "\25FE\00A0\00A0 disable"; 15 | } 16 | 17 | .mixinsEnabledState #btnOnOff 18 | { 19 | background-color: #ffd3d3; 20 | } 21 | 22 | .mixinsDisabledState #btnOnOff 23 | { 24 | background-color: #a2efaa; 25 | } 26 | 27 | .mixinsDisabledState #btnOnOff:after 28 | { 29 | content: "\25ba\00A0\00A0 enable"; 30 | animation: blinker 1.5s ease infinite; 31 | } 32 | 33 | @keyframes blinker 34 | { 35 | 50% { opacity: 0.2; } 36 | } 37 | 38 | 39 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Inject 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | const MIXINS_STATES = [ "mixinsEnabledState", "mixinsDisabledState" ]; 5 | const MIXINS_DEFAULT_STATE = "mixinsEnabledState"; // First run, nothing stored 6 | 7 | 8 | nsUI.init( () => 9 | { 10 | nsUI.bind( "#btnOptions", "click", nsUI.openOptions ); 11 | 12 | nsUI.bind( "#btnOnOff", "click", () => 13 | { 14 | const newState = nsUI.hasState( "mixinsEnabledState" ) 15 | ? "mixinsDisabledState" 16 | : "mixinsEnabledState"; 17 | 18 | nsSettings.set({ "mixinsState": newState }, 19 | () => nsUI.setState( newState, MIXINS_STATES )); 20 | }); 21 | 22 | nsSettings.get([ "mixinsState" ], 23 | r => nsUI.setState( r.mixinsState || MIXINS_DEFAULT_STATE, MIXINS_STATES )); 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | const EDITOR_STATES = [ "editorDefaultState", "editorChangedState", "editorSavedState" ]; 5 | 6 | 7 | nsUI.init( () => 8 | { 9 | nsUI.tweakTextArea( "#scriptarea", 10 | { 11 | canTabs: true, 12 | canAutocomplete: true, 13 | onInput: () => nsUI.setState( "editorChangedState", EDITOR_STATES ) 14 | }); 15 | 16 | nsUI.bind( "#btnSave", "click", event => 17 | { 18 | const script = nsUI.elem( "#scriptarea" ).value; 19 | nsSettings.set({ "mixinsScript": script }, () => nsUI.setState( "editorSavedState", EDITOR_STATES )); 20 | }); 21 | 22 | nsUI.onCtrlS( () => nsUI.elem( "#btnSave" ).click() ); 23 | 24 | nsSettings.get([ "mixinsScript" ], stored => 25 | { 26 | if( stored.mixinsScript ) // Don't overwrite initial synopsis text 27 | nsUI.elem( "#scriptarea" ).value = stored.mixinsScript; 28 | }); 29 | }); 30 | 31 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test page 5 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @since 2021-02-03 4 | * @author https://github.com/andre-st 5 | * 6 | * Note: 7 | * - doc-comments conventions: http://usejsdoc.org/ 8 | * - members prefixed with an underscore are private members (_function, _attribute) 9 | * 10 | */ 11 | 12 | "use strict"; 13 | 14 | /** 15 | * @namespace 16 | * @description Chrome Extension Settings utils library 17 | * 18 | * 19 | * Note: 20 | * chrome.storage.local no limit 21 | * versus 22 | * chrome.storage.sync.QUOTA_BYTES_PER_ITEM = 8192 23 | * 24 | * 25 | */ 26 | const nsSettings = 27 | { 28 | addChangeListener: function( theCallback ) 29 | { 30 | chrome.storage.onChanged.addListener( theCallback ); // (changes,areaName) 31 | }, 32 | 33 | 34 | get: function( theKeys, theCallback ) 35 | { 36 | chrome.storage.local.get( theKeys, theCallback ); 37 | }, 38 | 39 | 40 | set: function( theStoreObj, theCallback ) 41 | { 42 | chrome.storage.local.set( theStoreObj, theCallback ); 43 | } 44 | } 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name" : "Tiny Javascript Injector", 5 | "short_name" : "InjectJS", 6 | "version" : "0.8.3", 7 | "version_name": "Release 2023-02-12", 8 | "description" : "Customize a remote website, without consent", 9 | "author" : "andre-st", 10 | "homepage_url": "https://github.com/andre-st/chrome-injectjs/blob/master/README.md", 11 | 12 | "offline_enabled" : true, 13 | "permissions" : [ "storage", "webRequest", "tabs", "webRequestBlocking", "downloads", "http://*/", "https://*/" ], 14 | "minimum_chrome_version": "24", 15 | 16 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 17 | 18 | "icons": 19 | { 20 | "16" : "image/icon16.png", 21 | "48" : "image/icon48.png", 22 | "128": "image/icon128.png" 23 | }, 24 | 25 | "browser_action": 26 | { 27 | "default_icon" : "image/icon16-disabled.png", 28 | "default_popup": "popup.html" 29 | }, 30 | 31 | "options_page": "options.html", 32 | 33 | "background": 34 | { 35 | "scripts" : [ "settings.js", "background.js" ], 36 | "persistent": true 37 | }, 38 | 39 | "content_scripts": 40 | [{ 41 | "js" : [ "settings.js", "content.js" ], 42 | "matches" : [ "" ], 43 | "all_frames": false, 44 | "run_at" : "document_end" 45 | }] 46 | } 47 | -------------------------------------------------------------------------------- /options.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | background-color: #efefef; 4 | font-size: 12pt; 5 | color: #444; 6 | padding: 0; 7 | margin: 0; 8 | } 9 | 10 | button 11 | { 12 | padding: 0.45em 0 0.45em 0.5em; 13 | background-color: #dfdfdf; 14 | } 15 | 16 | button.default 17 | { 18 | font-weight: bold; 19 | } 20 | 21 | fieldset 22 | { 23 | padding: 0.35em 0.5em 0.35em 0.5em ; 24 | border: 0 none; 25 | } 26 | 27 | #btnSave 28 | { 29 | margin: 0.25em 0 0 0; 30 | width: 20em; 31 | } 32 | 33 | #toolbar a 34 | { 35 | display: inline-block; 36 | width: 10em; 37 | text-align: right; 38 | position: absolute; 39 | right: 1em; 40 | top: 1em; 41 | } 42 | 43 | 44 | #btnSave:after 45 | { 46 | content: "Save and load script"; 47 | } 48 | 49 | .editorChangedState #btnSave:after 50 | { 51 | content: "Save and load changed script *"; 52 | } 53 | 54 | .editorSavedState #btnSave:after 55 | { 56 | content: "Script saved" 57 | } 58 | 59 | textarea 60 | { 61 | /* Remove default glow border if focused */ 62 | border: 0 none; 63 | overflow: auto; 64 | outline: none; 65 | -webkit-box-shadow: none; 66 | -moz-box-shadow: none; 67 | box-shadow: none; 68 | 69 | background-color: #fff; 70 | margin: 0; 71 | padding: 0.5em; 72 | font-size: 10pt; 73 | font-family: monospace; 74 | box-sizing: border-box; 75 | resize: none; 76 | width: calc( 100vw - 1.5em ); 77 | height: calc( 100vh - 5em ); 78 | } 79 | 80 | -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Options 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | Visit Github 21 | 22 |
23 | 24 |
25 | 26 | 52 | 53 |
54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | **Note:** 6 | This program is using the plugin-manifest V2, 7 | and chrome will display an (deprecation) "error" although there is no real error. 8 | It's just that Chrome's V2-support ends in 2023 and I'm not yet sure how to migrate to the very restricted V3. 9 | 10 | 11 | ## [0.8.3] - 2023-02-12 12 | ### Added: 13 | - handles URL changes typical for single-page applications, 14 | when the browser doesn't load a new page from the web 15 | but just updates the user interface (DOM) and the window-location URL 16 | (Note: URL redirection rules are ignored in such a case) 17 | 18 | 19 | ## [0.8.2] - 2022-04-06 20 | ### Changed: 21 | - minor code improvements 22 | ### Fixed: 23 | - no more spell checking in texteditor (red underlines) 24 | 25 | 26 | ## [0.8.0] - 2021-10-20 27 | ### Added: 28 | - saveUrl() allows your mixin-code to trigger the download dialog for any resource with an URL 29 | - simple text auto-completion with Ctrl+Space (too annoying without tabs and auto-completion) 30 | 31 | 32 | ## [0.6.0] - 2021-02-18 33 | ### Fixed: 34 | - sample script 'Goodreads ratings on Amazon' 35 | 36 | ### Added: 37 | - browser-action icon turns green if one of your mixins was injected into the active webpage 38 | 39 | 40 | ## [0.5.0] - 2021-02-03 41 | ### Fixed: 42 | - sample script 'Goodreads ratings on Amazon', also ui improvements 43 | 44 | ### Added: 45 | - getUrl() to replace Javascript's `fetch` command which causes Cross-Origin Read Blocking (CORB) errors in a mixin-function 46 | (or Chrome content scripts in general) 47 | 48 | ### Changed: 49 | - does not run mixins multiple times when there are multiple frames 50 | - mixins scripts are not synchronized on other devices with Chrome; 51 | otherwise there would be a limit to the script length 52 | 53 | 54 | ## [0.4.0] - 2018-08-28 55 | ### Added: 56 | 57 | - pass multiple URLs to `mixin(...)` 58 | - inject CSS code instead of Javascript by passing a template literal to `mixin(...)` 59 | (see script-example.js) 60 | 61 | 62 | ## [0.3.0] - 2018-08-17 63 | ### Added: 64 | 65 | - redirect requests with `redir( ... )` 66 | 67 | 68 | ## [0.2.0] - 2018-05-27 69 | 70 | 71 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | const REDIRABLE_URLS = [ "http://*/*", "https://*/*" ]; // vs "" 5 | 6 | const _redirRules = new Array(); // { regex: //, repl: '' } 7 | const _mixinUrls = new Array(); // String 8 | var _mixinsState = "mixinsEnabledState"; // First state if nothing saved 9 | 10 | 11 | 12 | function updateIconState() 13 | { 14 | chrome.tabs.query({ currentWindow: true, active: true }, tabs => 15 | { 16 | var path; 17 | if( _mixinsState == "mixinsDisabledState" ) 18 | path = "image/icon16-disabled.png" 19 | else if( tabs && tabs.length > 0 && _mixinUrls.some( u => tabs[0].url.startsWith( u ))) 20 | path = "image/icon16-injected.png" 21 | else 22 | path = "image/icon16.png"; 23 | 24 | chrome.browserAction.setIcon({ path: path }); 25 | }); 26 | } 27 | 28 | 29 | function runMixinsScript( theScript ) 30 | { 31 | function redir( theRegex, theReplace ) 32 | { 33 | _redirRules.push({ regex: theRegex, repl: theReplace }); 34 | }; 35 | 36 | function mixin( theUrl, theCallback ) 37 | { 38 | _mixinUrls.push( theUrl ); 39 | }; 40 | 41 | _redirRules.length = 0; 42 | _mixinUrls.length = 0; 43 | 44 | eval( theScript ); // Script call redir(), mixin() multiple times 45 | updateIconState(); 46 | } 47 | 48 | 49 | nsSettings.addChangeListener( changes => 50 | { 51 | if( "mixinsState" in changes ) 52 | { 53 | _mixinsState = changes.mixinsState.newValue; 54 | updateIconState(); 55 | } 56 | 57 | if( "mixinsScript" in changes ) 58 | runMixinsScript( changes.mixinsScript.newValue ); 59 | }); 60 | 61 | 62 | chrome.tabs .onActivated .addListener( updateIconState ); 63 | chrome.tabs .onUpdated .addListener( updateIconState ); 64 | chrome.windows.onFocusChanged.addListener( updateIconState ); 65 | 66 | 67 | chrome.webRequest.onBeforeRequest.addListener( details => 68 | { 69 | if( _mixinsState == "mixinsDisabledState" ) return {}; 70 | 71 | const rule = _redirRules.find( r => details.url.match( r.regex )); 72 | 73 | if( !rule ) return {}; 74 | 75 | const newUrl = details.url.replace( rule.regex, rule.repl ); 76 | 77 | if( newUrl == details.url ) return {}; 78 | 79 | return { redirectUrl: newUrl }; 80 | 81 | }, { urls: REDIRABLE_URLS }, [ "blocking" ]); 82 | 83 | 84 | nsSettings.get([ "mixinsScript", "mixinsState" ], stored => 85 | { 86 | _mixinsState = stored.mixinsState || _mixinsState; 87 | runMixinsScript( stored.mixinsScript ); 88 | }); 89 | 90 | 91 | chrome.runtime.onMessage.addListener( (request, sender, sendResponse) => 92 | { 93 | // To improve security, cross-origin fetches are disallowed from content scripts. 94 | // Such requests can be made from extension background pages instead, 95 | // and relayed to content scripts when needed. 96 | if( request.contentScriptQuery == "getUrl" ) 97 | { 98 | fetch ( request.url ) 99 | .then ( response => response.text() ) 100 | .then ( text => sendResponse( text )) 101 | .catch( error => console.log( error )) 102 | return true; // Will respond asynchronously 103 | } 104 | 105 | // sendResponse callback with { downloadId=undefined } on error 106 | if( request.contentScriptQuery == "saveUrl" ) 107 | { 108 | chrome.downloads.download({ url: request.url }, sendResponse ); 109 | return true; 110 | } 111 | }); 112 | 113 | 114 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | function runStoredMixinsScript() 5 | { 6 | nsSettings.get([ "mixinsScript", "mixinsState" ], stored => 7 | { 8 | if( stored.mixinsState == "mixinsDisabledState" ) return; 9 | 10 | 11 | // Use in mixins which run as content script in order to avoid XSS. 12 | // ECMAScript provides URI percent-encoding routines only, 13 | // so we have to define our own HTML-entities encoder: 14 | function unxss( theStr ) 15 | { 16 | return theStr.replace( /[\u00A0-\u99999<>\&]/gim, (i) => '&#' + i.charCodeAt( 0 ) + ';' ); 17 | } 18 | 19 | 20 | // Use in mixins to load cross-origin web resources without read blocking (CORB). 21 | // Details see "getUrl"-handler in background.js 22 | // Expects the 'runAsContentScript' mixin-option set true. 23 | function getUrl( theUrl, theCallback ) 24 | { 25 | chrome.runtime.sendMessage({ contentScriptQuery: "getUrl", url: theUrl }, theCallback ); 26 | } 27 | 28 | 29 | // Use in mixins to trigger the download dialog for a web resource from any origin. 30 | // Details see "saveUrl"-handler in background.js 31 | // Expects the 'runAsContentScript' mixin-option set true. 32 | function saveUrl( theUrl, theCallback ) 33 | { 34 | const absUrl = new URL( theUrl, document.baseURI ).href; // Absolute URL required 35 | chrome.runtime.sendMessage({ contentScriptQuery: "saveUrl", url: absUrl }, theCallback ); 36 | } 37 | 38 | 39 | function redir( theRegex, theReplace ) { /* implemented in background.js */ }; 40 | 41 | 42 | function mixin( theUrls, theCode, theOpts ) 43 | { 44 | const urls = Array.isArray( theUrls ) ? theUrls : [ theUrls ]; // Single URL as String? 45 | 46 | if( !urls.some( u => location.href.startsWith( u ))) return; 47 | 48 | if( typeof theCode === "string" || theCode instanceof String ) // Inject CSS 49 | { 50 | const s = document.createElement( "STYLE" ); 51 | s.textContent = theCode; 52 | ( document.head || document.documentElement ).appendChild( s ); 53 | return; 54 | } 55 | 56 | if( typeof theCode === "function" ) // Inject ECMAScript: 57 | { 58 | // Content scripts and the website share the DOM but no JS functions. 59 | // https://developer.chrome.com/extensions/content_scripts#isolated_world 60 | // But we can run user scripts in the world of the target website. 61 | // This allows us to call the website's Javascript functions and to strip 62 | // off some privileges: the remote page cannot access our extension. 63 | 64 | // Some code, however, needs more privileges, e.g., on CORS issues: 65 | if( theOpts && (theOpts.runAsContentScript || false) ) 66 | { 67 | theCode(); 68 | return; 69 | } 70 | 71 | const s = document.createElement( "SCRIPT" ); 72 | s.textContent = "(" + theCode + ")();"; 73 | ( document.head || document.documentElement ).appendChild( s ); 74 | s.remove(); // ?? 75 | return; 76 | } 77 | 78 | throw "Given code neither string nor function"; 79 | }; 80 | 81 | 82 | eval( stored.mixinsScript ); // Script calls mixin(), redir() multiple times 83 | // and prints SyntaxError.message to console 84 | }); 85 | } 86 | 87 | 88 | // Handle URL changes in single-page applications: 89 | window.addEventListener( 'popstate', runStoredMixinsScript ); 90 | 91 | 92 | // First page-load: 93 | runStoredMixinsScript(); 94 | 95 | 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny Javascript Injector for Google Chrome, v0.8.3 2 | 3 | ![Maintenance](https://img.shields.io/maintenance/yes/2023.svg) 4 | 5 | **Customize someone else's site when it's missing display options or features. [What's new?](CHANGELOG.md)** 6 | 7 | There are prettier code injection extensions, e.g., 8 | Dmitry Novikov's ["User Javascript and CSS"](https://chrome.google.com/webstore/detail/user-javascript-and-css/nbhcbdghjpllgmfilhnhkllmkecfmpld?hl=en-US). 9 | However, you give "sqdevil@yandex.ru", "junkycoder" etc. full control over everything you read on the web. 10 | You could also try userscript managers such as [Violentmonkey](https://violentmonkey.github.io/) or [Tampermonkey](https://tampermonkey.net/), with scripts from [Greasyfork](https://greasyfork.org/) or [OpenUserJS](https://openuserjs.org/). 11 | 12 | Chrome _content scripts_ can modify websites, e.g. [political](https://chrome.google.com/webstore/search/politics%20OR%20political%20OR%20activist%20OR%20activisim?hl=en&_category=extensions) content, and [exfiltrate](https://www.theregister.co.uk/2018/07/05/browsers_pull_stylish_but_invasive_browser_extension/) 13 | private information. So I made my own extension, which is small and easy to inspect if you consider using it. 14 | 15 | This extension also helps replace smaller Chrome extensions such as 16 | Rubén Martínez's ["Goodreads Ratings for Amazon"](https://chrome.google.com/webstore/detail/goodreads-ratings-for-ama/fkkcefhhadenobhjnngfdahhlodolkjg) 17 | by adding [an equivalent function](https://gist.github.com/andre-st/592825fe9a5b2eafc5a73feb80ade649) to the mixins script. 18 | The fewer strangers fiddling with my browser, the better. 19 | 20 | 21 | ## Program Features and Screenshots 22 | 23 | - supports **URL redirections** (replaces Einar Egilsson's [Redirector](https://chrome.google.com/webstore/detail/redirector/ocgpenflpmgnfapjedencafcfakcekcd) 24 | extension) 25 | - no automatic updates (unlike [this](https://www.theregister.co.uk/2018/07/05/browsers_pull_stylish_but_invasive_browser_extension/) 26 | or [this adware buy-out](https://www.bleepingcomputer.com/news/security/-particle-chrome-extension-sold-to-new-dev-who-immediately-turns-it-into-adware/)), 27 | see Installation section 28 | - tiny, kept to the bare minimum = little maintenance 29 | - easy to inspect if you consider using it -- no 3rd party libs, nothing [minified](https://en.wikipedia.org/wiki/Minification_(programming)), small files 30 | - apart from Tab for indentation, 31 | Ctrl+S for saving, 32 | Ctrl+Space for simple autocompletion 33 | and a few [helper functions](https://github.com/andre-st/chrome-injectjs/blob/master/options.html#L37) such as `getUrl()` and `saveUrl()`, 34 | it lacks any comfort and visual beauty: 35 | no syntax highlighting or validation (no Ace editor, jslint, ...); 36 | the browser console, however, proved sufficient for debugging 37 | - inject CSS or Javascript, for example: 38 | - [Force consistent Goodreads.com view settings](https://gist.github.com/andre-st/71c824fd1e8b61e6e29af2a962c60956) 39 | - [Show Goodreads.com ratings on Amazon](https://gist.github.com/andre-st/592825fe9a5b2eafc5a73feb80ade649) 40 | - [Price-filter for Amazon wishlists](https://gist.github.com/andre-st/ae556e9966738a5b3d7d2ff773196207) 41 | - Bypass paywall of your local newspaper 42 | - Add download buttons to streaming-websites 43 | - Remove transparent overlays which open ad-sites on click 44 | - [more scripts](https://gist.github.com/search?q=user%3Aandre-st+%23injectjs) 45 | - flexible and easy to extend due to its script-based configuration 46 | (rather than having a complex UI that tries to be as flexible as a programming language 47 | just use a programming language); you can add comments everywhere, too 48 | 49 | ![Screenshot](image/screenshot-20180817.png) 50 | 51 | - your mixin scripts run in the [context of the target website](https://developer.chrome.com/extensions/content_scripts#isolated_world) 52 | - so you can not just access its DOM, but its Javascript variables and functions too 53 | - the targeted website cannot easily hijack your extension 54 | - everything outside mixin() runs in the context of the extension, with other privileges 55 | - export/import = copy/paste the content of the text area 56 | 57 | 58 | ## Installation 59 | 60 | 1. not available in Chrome's Web Store 61 | 2. you cannot easily install CRX-files permanently from other sites 62 | 3. clone Git repository or [save as Zip-file](https://github.com/andre-st/chrome-injectjs/archive/master.zip) 63 | 4. Chrome > Settings > Extensions > [x] Developer mode (upper right corner) 64 | 5. Chrome > Settings > Extensions > click Load unpacked extension 65 | 6. browse to the source directory of the downloaded, unarchived release and confirm 66 | 67 | 68 | ## Feedback 69 | 70 | Use [GitHub](https://github.com/andre-st/chrome-injectjs/issues) or see [AUTHORS.md](AUTHORS.md) file 71 | -------------------------------------------------------------------------------- /ui.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @since 2018-05-25 4 | * @author https://github.com/andre-st 5 | * 6 | * Note: 7 | * - doc-comments conventions: http://usejsdoc.org/ 8 | * - members prefixed with an underscore are private members (_function, _attribute) 9 | * 10 | */ 11 | 12 | "use strict"; 13 | 14 | /** 15 | * @namespace 16 | * @description Lightweight Chrome Extension UI utils library 17 | */ 18 | const nsUI = 19 | { 20 | /** 21 | * @callback stateCallback 22 | * @param {string} theState - CSS classname, @see setState() 23 | * @return {void} 24 | * @public 25 | */ 26 | 27 | /** 28 | * @callback actionCallback 29 | * @return {void} 30 | * @public 31 | */ 32 | 33 | stateListeners: [], /** Notified by setState(): @see stateCallback */ 34 | 35 | /** 36 | * @param {actionCallback} theCallback 37 | * @return {void} 38 | * @public 39 | */ 40 | init: function( theCallback ) 41 | { 42 | document.addEventListener( "DOMContentLoaded", theCallback ); 43 | }, 44 | 45 | 46 | /** 47 | * @param {DOMString} theSelector 48 | * @return {Element} 49 | * @public 50 | */ 51 | elem: function( theSelector ) 52 | { 53 | return document.querySelector( theSelector ); 54 | // return [].slice.call( document.querySelectorAll( theSelector )); elem().forEach(... 55 | }, 56 | 57 | 58 | /** 59 | * @param {DOMString} theSelector - CSS selector 60 | * @param {string} theEventName - DOM event type: "click", "keydown" etc 61 | * @param {EventListener} theEventHandler - function which handles EventTarget 62 | * @return {void} 63 | * @public 64 | */ 65 | bind: function( theSelector, theEventName, theEventHandler ) 66 | { 67 | const a = document.querySelectorAll( theSelector ); 68 | for( var i = 0; i < a.length; i++ ) 69 | a[i].addEventListener( theEventName, theEventHandler ); 70 | }, 71 | 72 | 73 | /** 74 | * @param {string} theState - Any string included in the possible states parameter 75 | * @param {string[]} thePossibleStates 76 | * @param {DOMString} [theSelector] - CSS selector 77 | * @return {void} 78 | * @public 79 | * 80 | *
 
 81 | 	 * .textChangedState #btnSave { background-color: yellow; }
 82 | 	 * .textSavedState   #btnSave { background-color: green;  }
 83 | 	 * 
84 | */ 85 | setState: function( theState, thePossibleStates, theSelector ) 86 | { 87 | console.assert( thePossibleStates.includes( theState ), 88 | "Expect possible states to include '" + theState + "'" ); 89 | 90 | const a = document.querySelectorAll( theSelector || "body" ); 91 | for( var i = 0; i < a.length; i++ ) 92 | { 93 | a[i].classList.remove( ...thePossibleStates ); // Removes prev state 94 | a[i].classList.add( theState ); 95 | } 96 | 97 | nsUI.stateListeners.forEach( l => l( theState )); 98 | }, 99 | 100 | 101 | /** 102 | * @param {string} theState - @see setState() 103 | * @param {DOMString} theSelector - CSS selector 104 | * @return {boolean} 105 | * @public 106 | */ 107 | hasState: function( theState, theSelector ) 108 | { 109 | const e = nsUI.elem( theSelector || "body" ); 110 | return e ? e.classList.contains( theState ) : false; 111 | }, 112 | 113 | 114 | /** 115 | * @param {actionCallback} theCallback 116 | * @return {void} 117 | * @public 118 | */ 119 | onCtrlS: function( theCallback ) 120 | { 121 | nsUI.bind( "body", "keydown", event => 122 | { 123 | // CTRL+S habit to save current document 124 | if( event.ctrlKey && event.which === 83 ) 125 | { 126 | event.preventDefault(); 127 | theCallback(); 128 | return false; 129 | } 130 | }); 131 | }, 132 | 133 | 134 | /** 135 | * @param {HTMLTextAreaElement} DOM element with the word at cursor position to complete 136 | * @return {void} 137 | * @public 138 | */ 139 | autocomplete: function( theTextArea ) // Good enough auto-completion (just the previous keyword-variant) 140 | { 141 | const isLetter = x => x.toLowerCase() != x.toUpperCase(); 142 | const isStopChar = x => !isLetter( x ); 143 | const kwEnd = theTextArea.selectionStart; 144 | var kwStart = kwEnd - 1; 145 | var keyword = ""; 146 | var completion = ""; // Just the remainder 147 | 148 | // Get the yet incomplete string in question by searching backwards from the cursor position 149 | for(; kwStart > 0; kwStart-- ) 150 | { 151 | const c = theTextArea.value.charAt( kwStart ); 152 | if( isStopChar( c )) break; 153 | keyword = c + keyword; 154 | } 155 | 156 | // Find another occurence of the incomplete string just before the incomplete string at the cursor position. 157 | // In 90% it's the variant the user is looking for. 158 | // Otherwise the user has to add one or two additional characters to the inoomplete string in question and retry. 159 | const matchStart = theTextArea.value.lastIndexOf( keyword, kwStart - 1 ); 160 | 161 | // Follow that occurence until a stop-character to determine a variant of a complete string 162 | for( var matchEnd = matchStart + keyword.length; matchEnd < theTextArea.value.length; matchEnd++ ) 163 | { 164 | const c = theTextArea.value.charAt( matchEnd ); 165 | if( isStopChar( c )) break; 166 | completion += c; 167 | } 168 | 169 | if( completion.length > 30 ) return; // Something went wrong? 170 | 171 | // Insert found variant of a complete string at the cursor position 172 | theTextArea.value = theTextArea.value.substring( 0, kwEnd ) + completion + theTextArea.value.substring( kwEnd ); 173 | theTextArea.selectionEnd = theTextArea.selectionStart = kwEnd + completion.length; // Set cursor 174 | }, 175 | 176 | 177 | /** 178 | * @param {HTMLTextAreaElement} DOM element with the text to indent at the cursor position 179 | * @return {void} 180 | * @public 181 | */ 182 | indentText: function( theTextArea ) // Currently just adds tabs (more than native TA does) 183 | { 184 | const p1 = theTextArea.selectionStart; 185 | const p2 = theTextArea.selectionEnd; 186 | theTextArea.value = theTextArea.value.substring( 0, p1 ) + "\t" 187 | + theTextArea.value.substring( p2 ); 188 | 189 | theTextArea.selectionStart = theTextArea.selectionEnd = p1 + 1; 190 | }, 191 | 192 | 193 | /** 194 | * @param {DOMString} theSelector - CSS selector 195 | * @param {Object} theOptions - { onInput: Function, canTabs: Boolean, canAutocomplete: Boolean } 196 | * @return {void} 197 | * @public 198 | */ 199 | tweakTextArea: function( theSelector, theOptions ) 200 | { 201 | if( theOptions.onInput ) 202 | nsUI.bind( theSelector, "input", theOptions.onInput ); 203 | 204 | nsUI.bind( theSelector, "keydown", event => 205 | { 206 | const ta = event.target; 207 | 208 | // Enable text autcompletion with [CTRL]+[SPACE] keys. 209 | // Unfortunately, vim-like [CTRL]+[N] is already taken by the browser. 210 | if( theOptions.canAutocomplete && event.ctrlKey && event.keyCode === 32 ) 211 | { 212 | nsUI.autocomplete( ta ); 213 | event.preventDefault(); 214 | return false; 215 | } 216 | 217 | // Enable tabs for indentation (no native support) 218 | if( theOptions.canTabs && event.keyCode === 9 ) 219 | { 220 | nsUI.indentText( ta ); 221 | event.preventDefault(); 222 | return false; 223 | } 224 | }); 225 | }, 226 | 227 | 228 | /** 229 | * @return {void} 230 | * @public 231 | */ 232 | openOptions: function() 233 | { 234 | window.open( chrome.runtime.getURL( "options.html" )); 235 | } 236 | } 237 | 238 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | --------------------------------------------------------------------------------