I'm learning how to create Chrome extensions. I just started developing one to catch YouTube events. I want to use it with YouTube flash player (later I will try to make it compatible with HTML5).

manifest.json:

{
    "name": "MyExtension",
    "version": "1.0",
    "description": "Gotta catch Youtube events!",
    "permissions": ["tabs", "http://*/*"],
    "content_scripts" : [{
        "matches" : [ "www.youtube.com/*"],
        "js" : ["myScript.js"]
    }]
}

myScript.js:

function state() { console.log("State Changed!"); }
var player = document.getElementById("movie_player");
player.addEventListener("onStateChange", "state");
console.log("Started!");

The problem is that the console gives me the "Started!", but there is no "State Changed!" when I play/pause YouTube videos.

When this code is put in the console, it worked. What am I doing wrong?

10 upvote
  flag
try to remove the quotes around your function name: player.addEventListener("onStateChange", state); – Eduardo
upvote
  flag
It is also notable that when writing matches, do not forget to include https:// or http://, this www.youtube.com/* would not let you pack extension and would throw Missing scheme separator error – Nilay Vishwakarma
1 upvote
  flag

4 Answers 11

up vote 657 down vote accepted

Content scripts are executed in an "isolated world" environment. You have to inject your state() method into the page itself.

When you want to use one of the chrome.* APIs in the script, you have to implement a special event handler, as described in this answer: Chrome extension - retrieving Gmail's original message.

Otherwise, if you don't have to use chrome.* APIs, I strongly recommend to inject all of your JS code in the page via adding a <script> tag:

Table of contents

  • Method 1: Inject another file
  • Method 2: Inject embedded code
  • Method 2b: Using a function
  • Method 3: Using an inline event
  • Dynamic values in the injected code

Method 1: Inject another file

This is the easiest/best method when you have lots of code. Include your actual JS code in a file within your extension, say script.js. Then let your content script be as follows (explained here: Google Chome “Application Shortcut” Custom Javascript):

var s = document.createElement('script');
// TODO: add "script.js" to web_accessible_resources in manifest.json
s.src = chrome.extension.getURL('script.js');
s.onload = function() {
    this.remove();
};
(document.head || document.documentElement).appendChild(s);

Note: If you use this method, the injected script.js file has to be added to the "web_accessible_resources" section (example). If you do not, Chrome will refuse to load your script and display the following error in the console:

Denying load of chrome-extension://[EXTENSIONID]/script.js. Resources must be listed in the web_accessible_resources manifest key in order to be loaded by pages outside the extension.

Method 2: Inject embedded code

This method is useful when you want to quickly run a small piece of code. (See also: How to disable facebook hotkeys with Chrome extension?).

var actualCode = `// Code here.
// If you want to use a variable, use $ and curly braces.
// For example, to use a fixed random number:
var someFixedRandomValue = ${ Math.random() };
// NOTE: Do not insert unsafe variables in this way, see below
// at "Dynamic values in the injected code"
`;

var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.remove();

Note: template literals are only supported in Chrome 41 and above. If you want the extension to work in Chrome 40-, use:

var actualCode = ['/* Code here. Example: */' + 'alert(0);',
                  '// Beware! This array have to be joined',
                  '// using a newline. Otherwise, missing semicolons',
                  '// or single-line comments (//) will mess up your',
                  '// code ----->'].join('\n');

Method 2b: Using a function

For a big chunk of code, quoting the string is not feasible. Instead of using an array, a function can be used, and stringified:

var actualCode = '(' + function() {
    // All code is executed in a local scope.
    // For example, the following does NOT overwrite the global `alert` method
    var alert = null;
    // To overwrite a global variable, prefix `window`:
    window.alert = null;
} + ')();';
var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.remove();

This method works, because the + operator on strings and a function converts all objects to a string. If you intend on using the code more than once, it's wise to create a function to avoid code repetition. An implementation might look like:

function injectScript(func) {
    var actualCode = '(' + func + ')();'
    ...
}
injectScript(function() {
   alert("Injected script");
});

Note: Since the function is serialized, the original scope, and all bound properties are lost!

var scriptToInject = function() {
    console.log(typeof scriptToInject);
};
injectScript(scriptToInject);
// Console output:  "undefined"

Method 3: Using an inline event

Sometimes, you want to run some code immediately, e.g. to run some code before the <head> element is created. This can be done by inserting a <script> tag with textContent (see method 2/2b).

An alternative, but not recommended is to use inline events. It is not recommended because if the page defines a Content Security policy that forbids inline scripts, then inline event listeners are blocked. Inline scripts injected by the extension, on the other hand, still run. If you still want to use inline events, this is how:

var actualCode = '// Some code example \n' + 
                 'console.log(document.documentElement.outerHTML);';

document.documentElement.setAttribute('onreset', actualCode);
document.documentElement.dispatchEvent(new CustomEvent('reset'));
document.documentElement.removeAttribute('onreset');

Note: This method assumes that there are no other global event listeners that handle the reset event. If there is, you can also pick one of the other global events. Just open the JavaScript console (F12), type document.documentElement.on, and pick on of the available events.

Dynamic values in the injected code

Occasionally, you need to pass an arbitrary variable to the injected function. For example:

var GREETING = "Hi, I'm ";
var NAME = "Rob";
var scriptToInject = function() {
    alert(GREETING + NAME);
};

To inject this code, you need to pass the variables as arguments to the anonymous function. Be sure to implement it correctly! The following will not work:

var scriptToInject = function (GREETING, NAME) { ... };
var actualCode = '(' + scriptToInject + ')(' + GREETING + ',' + NAME ')';
// The previous will work for numbers and booleans, but not strings.
// To see why, have a look at the resulting string:
var actualCode = "(function(GREETING, NAME) {...})(Hi I'm,Rob)";
//                                                 ^^^^^^ ^^^ No string literals!

The solution is to use JSON.stringify before passing the argument. Example:

var actualCode = '(' + function(greeting, name) { ...
} + ')(' + JSON.stringify(GREETING) + ',' + JSON.stringify(NAME) + ')';

If you have many variables, it's worthwhile to use JSON.stringify once, to improve readability, as follows:

...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]) + ')';
52 upvote
  flag
This answer should be part of official docs. Official docs should ship with recommended way --> 3 ways to do the same thing... Wrong? – Michal Stefanow
1 upvote
  flag
Wow, Kudos, @Rob! – Yossale
1 upvote
  flag
Usually method 1 is better wherever possible, due to Chrome's CSP (content security policy) restrictions for some extensions. – Qantas 94 Heavy
5 upvote
  flag
@Qantas94Heavy The extension's CSP does not affect content scripts. Only the page's CSP is relevant. Method 1 can be blocked by using a script-src directive that excludes the extension's origin, method 2 can be blocked by using a CSP that excludes "unsafe-inline"`. – Rob W
upvote
  flag
@RobW: I see - thanks for the clarification. – Qantas 94 Heavy
upvote
  flag
@RobW Thanks Rob. You made my day. – Jagdeep Singh
1 upvote
  flag
Someone asked why I remove the script tag using script.parentNode.removeChild(script);. My reason for doing it is because I like to clean up my mess. When an inline script is inserted in the document, it's immediately executed and the <script> tag can safely be removed. – Rob W
6 upvote
  flag
Other method: use location.href = "javascript: alert('yeah')"; anywhere in your content script. It's easier for short snippets of code, and can also access the page's JS objects. – Métoule
2 upvote
  flag
@ChrisP Be careful with using javascript:. Code spanning over multiple lines might not work as expected. A line-comment (//) will truncate the remainder, so this will fail: location.href = 'javascript:// Do something <newline> alert(0);';. This can be circumvented by ensuring that you use multi-line comments. Another thing to be careful of is that the result of the expression should be void. javascript:window.x = 'some variable'; will cause the document to unload, and be replaced with the phrase 'some variable'. If used properly, it's indeed an attractive alternative to <script>. – Rob W
upvote
  flag
What if I want the injected code to run before any code on the target runs? Is that in any way possible? I tried placing the script tag at the top of the head element but it does not work since It is still injected after the document is loaded. – Jonathan Azulay
1 upvote
  flag
@MrAzulay I've edited the answer and added "Method 3" which can be used for your purposes. – Rob W
upvote
  flag
@RobW code injected using Method 2 with the option document_start runs before code in the page. It even runs synchronously. – rsanchez
upvote
  flag
@RobW What's the best way to access html that's packaged with the extension? Insert it into the page as well or is there a way for the injected script to query the content_page for the html? – ckarbass
upvote
  flag
What is a content_page? If I understand your q, then you're asking for the best method to run code in a page within your extension, right? If so, just load it via <script> tags. – Rob W
upvote
  flag
Typo my bad. Basically, you can't do this: chrome.extension.getURL('html/template.html') within the context of the injected page. I ended up appending the html to the dom within a hidden div to gain access to the html, but I assume you could also make an ajax request to retrieve the html. – ckarbass
1 upvote
  flag
This is a great answer but I have to say you that you need to add this another solution as the Method 2c: to get the function code simply write the function and apply this: code = WrittenFunction.toString().match(/{([\s\S]*)}/)[1]; – jscripter
upvote
  flag
@Buba I recommend against that, because closure is a nice way to have (local) variables without polluting the global namespace. – Rob W
upvote
  flag
code = "("+WrittenFunction.toString()+")()" – jscripter
upvote
  flag
Thanks for the detailed answer. This has helped clear up a lot of questions I've had about getting scripts to load in the context of the page. – Justin Ryder
upvote
  flag
Method 1 is not working for me. I am using a content script that injects a script tag in the page but the script in that tag is executing in the context of the VM (as if it was part of the content script) and, obviously, I don't have access to the variables of existing scripts (ie, I cannot access window.originalGlobalVariable). – f.ardelian
upvote
  flag
@f.ardelian Method 1 is still working. "[VM]" has no special meaning. If the variables are not available, then you're either injecting the script too early, or the variables are really not globally accessible. – Rob W
upvote
  flag
@RobW: I am positive the variable is there but I cannot access it from the injected script. pastebin.com/JYDHQ8p6 – f.ardelian
upvote
  flag
@f.ardelian Could you share a link to the website where this problem shows up? Are you sure that you're using the latest version of your extension? Visit chrome://extensions, and click on Reload at your extension. Then close the website's tab and re-open it. – Rob W
upvote
  flag
@RobW: Unfortunately, I cannot share it. The only thing that I find "special" about this extension is that it is unpacked. – f.ardelian
1 upvote
  flag
@f.ardelian Can't help you any further then. Make sure that you've checked that you've really reloaded the extension and tab process because sometimes Chrome keeps using and old version of the content script (this is especially painful during development). – Rob W
upvote
  flag
I got a string is not a function error using Method2b. script.textContent = actualCode; this line is error point to, but it clearly not the line which causes the error. Any idea? – castiel
upvote
  flag
@castiel Open the developer tools, go to Sources and click on the tiny button in the upper-right corner to "Break on all errors". Then refresh the page and voila, you will see the exact line where the bug occurs. If the line is off, then you can inspect the scope variables to see whether everything looks ok. – Rob W
upvote
  flag
@RobW I am using chrome and didn't find that "Break on all erros" button. But I fix this error by using setTimeout to delay the execution of appendChild and removeChild. and everything works fine now. – castiel
upvote
  flag
@RobW, is there a way to achieve the result of Method 3 (i.e. run code before head element is created) in a sandboxed iframe that does not 'allow-scripts'? Using Method 3 results in “Blocked script execution in 'about:blank' because the document's frame is sandboxed and the 'allow-scripts' permission is not set.” being raised on the setAttribute line. Thanks. – Edward Grech
1 upvote
  flag
@EdwardGrech That's not possible. – Rob W
upvote
  flag
Thanks for the confirmation @RobW. My Chrome extension uses "Method 3" to patch HTMLCanvasElement.prototype.toDataURL() before it can be accessed by anyone else. Hopefully I'll be able to figure out a way to patch it from the sandboxed iframe's parent. Thanks. – Edward Grech
1 upvote
  flag
@sdgluck Thanks for your edit! – Rob W
upvote
  flag
How to can I access page variable with this way? – Lai32290
2 upvote
  flag
@Lai32290 Using events or HTML attributes, for example using document.currentScript.dataset.foo = 'only strings can be shared'; in the injected script, and then reading it back using script.dataset.foo (assuming that script is a reference to the <script> tag that you injected). – Rob W
upvote
  flag
@RobW is working! thank you so much! – Lai32290
upvote
  flag
@RobW Am I right if I suppose that site using a very strict Content-Security-Policy HTTP response header as does for example twitter.com will break all ways to "inject" code in the page? Why is Chrome not "relaxing" the policy for "extensions"? – manuell
upvote
  flag
@manuell Inline scripts do bypass the page's CSP (implemented in crbug.com/181602), external scripts are blocked though (this is an open bug - crbug.com/392338). – Rob W
upvote
  flag
@RobW Thanks for your clear response. I will now try to understand why I can inject scripts using method 1 and 2b, EXCEPT in twitter.com – manuell
upvote
  flag
@RobW FYI, I found why I thought that your code was not working in twitter.com It works well, but twitter is just breaking console.log. See //allinonescript.com/questions/35311671/… – manuell
upvote
  flag
@RobW "external scripts are blocked though" Not true (anymore?), I just tried it (on Twitter, which sets a CSP). location.href="javascript:code..." does get blocked. – Miscreant
upvote
  flag
@Miscreant Thanks for correcting me. External scripts are not blocked (unless it's http on a https page), but external scripts with a CORS attribute are disabled (you would set crossorigin if e.g. you want to have meaningful stack traces). – Rob W
upvote
  flag
upvote
  flag
@RobW Can you help me on this //allinonescript.com/questions/40045344/… – uncivilized
upvote
  flag
for followers, if you run into messages like "unable to load chrome-extension://" when trying to inject javascript that itself loads another javascript file, or "Refused to execute inline script (or javascript URL) because it violates the following Content Security Policy directive: “script-src ‘self’ blob: filesystem: chrome-extension-resource:”..." (and 2-3 all don't work), for me this meant "you are running this from popup.js and it needs to be run from contentscript.js instead" FWIW. And thanks! – rogerdpack
upvote
  flag
I have issue with 2b when the code in the function uses a webpack module, anyone managed to find a way to do it when using webpack? – SuperUberDuper
upvote
  flag
Any submodules of the webpack one cannot be found. – SuperUberDuper
upvote
  flag
@SuperUberDuper These methods should only be used if you really need to run code in the context of the page. You mentioning webpack modules suggests that you are trying to run more than a small snippet in the context of the page, possibly a complete application. That should not be done, because your application code can interfere with the functionality in a page, and vice versa. If you do really have a legitimate reason for wanting to run a webpack project in a page, build a bundle and run that code instead. – Rob W

The only thing missing hidden from Row W's excellent answer is how to call from the injected script to the content script and vice versa (especially if you have objects that can't be stringified).

In either the injected or your content script add an event listener:

document.addEventListener('yourCustomEvent', function (e)
{
  var data=e.detail;
  console.log("received "+data);
});

On the other side (content or injected script) call the event:

var data="anything";

// updated: this works with Chrome 30:
var evt=document.createEvent("CustomEvent");
evt.initCustomEvent("yourCustomEvent", true, true, data);
document.dispatchEvent(evt);

// the following stopped working in Chrome 30 (Windows), detail was 
// not received in the listener:
// document.dispatchEvent(new CustomEvent('yourCustomEvent', { detail: data }));
upvote
  flag
I've actually linked to the code and explanation at the second line of my answer, to //allinonescript.com/questions/9602022/…. – Rob W
upvote
  flag
@rob oh, I've updated my answer ;) – laktak
1 upvote
  flag
Do you have a reference for your updated method (e.g. a bug report or a test case?) The CustomEvent constructor supersedes the deprecated document.createEvent API. – Rob W
upvote
  flag
For me 'dispatchEvent(new CustomEvent...' worked. I have Chrome 33. Also it didn't work before because I wrote the addEventListener after injecting the js code. – jscripter
upvote
  flag
Be extra careful about what you pass in as your 2nd parameter to the CustomEvent constructor. I experienced 2 very confusing setbacks: 1. simply putting single quotes around 'detail' perplexingly made the value null when received by my Content Script's listener. 2. More importantly, for some reason I had to JSON.parse(JSON.stringify(myData)) or else it too would become null. Given this, it appears to me that the following Chromium developer's claim--that the "structured clone" algorithm is used automatically--isn't true. bugs.chromium.org/p/chromium/issues/detail?id=260378#c18 – jdunk

I've also faced the problem of ordering of loaded scripts, which was solved through sequential loading of scripts. The loading is based on Rob W's answer.

function scriptFromFile(file) {
    var script = document.createElement("script");
    script.src = chrome.extension.getURL(file);
    return script;
}

function scriptFromSource(source) {
    var script = document.createElement("script");
    script.textContent = source;
    return script;
}

function inject(scripts) {
    if (scripts.length === 0)
        return;
    var otherScripts = scripts.slice(1);
    var script = scripts[0];
    var onload = function() {
        script.parentNode.removeChild(script);
        inject(otherScripts);
    };
    if (script.src != "") {
        script.onload = onload;
        document.head.appendChild(script);
    } else {
        document.head.appendChild(script);
        onload();
    }
}

The example of usage would be:

var formulaImageUrl = chrome.extension.getURL("formula.png");
var codeImageUrl = chrome.extension.getURL("code.png");

inject([
    scriptFromSource("var formulaImageUrl = '" + formulaImageUrl + "';"),
    scriptFromSource("var codeImageUrl = '" + codeImageUrl + "';"),
    scriptFromFile("EqEditor/eq_editor-lite-17.js"),
    scriptFromFile("EqEditor/eq_config.js"),
    scriptFromFile("highlight/highlight.pack.js"),
    scriptFromFile("injected.js")
]);

Actually, I'm kinda new to JS, so feel free to ping me to the better ways.

1 upvote
  flag
This way of inserting scripts is not nice, because you're polluting the namespace of the web page. If the web page uses a variable called formulaImageUrl or codeImageUrl, then you're effectively destroying the functionality of the page. If you want to pass a variable to the web page, I suggest to attach the data to the script element (e.g. script.dataset.formulaImageUrl = formulaImageUrl;) and use e.g. (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })(); in the script to access the data. – Rob W
upvote
  flag
@RobW thank you for your note, although it's more about the sample. Can you please clarify, why I should use IIFE instead of just getting dataset? – Dmitry Ginzburg
3 upvote
  flag
document.currentScript only points to the script tag while it is executing. If you ever want to access the script tag and/or its attributes/properties (e.g. dataset), then you need to store it in a variable. We need an IIFE to get a closure to store this variable without polluting the global namespace. – Rob W
upvote
  flag
@RobW excellent! But can't we just use some variable name, which would hardly intersect with the existing. Is it just non-idiomatic or we can have some other problems with it? – Dmitry Ginzburg
1 upvote
  flag
You could, but the cost of using an IIFE is negligible, so I don't see a reason to prefer namespace pollution over an IIFE. I value the certainly that I won't break the web page of others in some way, and the ability to use short variable names. Another advantage of using an IIFE is that you can exit the script earlier if wanted (return;). – Rob W
upvote
  flag
@RobW maybe it would be easier then to wrap the whole injected.js into IIFE, right? Anyway, thanks for the professional advice=) – Dmitry Ginzburg
upvote
  flag
I was referring to injected.js all the time, not the content script that injects injected.js. In a content script, an IIFE is not that important because you have full control over the (global) namespace. – Rob W

in Content script , i add script tag to the head which binds a 'onmessage' handler, inside the handler i use , eval to execute code. In booth content script i use onmessage handler as well , so i get two way communication. Chrome Docs

//Content Script

var pmsgUrl = chrome.extension.getURL('pmListener.js');
$("head").first().append("<script src='"+pmsgUrl+"' type='text/javascript'></script>");


//Listening to messages from DOM
window.addEventListener("message", function(event) {
  console.log('CS :: message in from DOM', event);
  if(event.data.hasOwnProperty('cmdClient')) {
    var obj = JSON.parse(event.data.cmdClient);
    DoSomthingInContentScript(obj);
 }
});

pmListener.js is a post message url listener

//pmListener.js

//Listen to messages from Content Script and Execute Them
window.addEventListener("message", function (msg) {
  console.log("im in REAL DOM");
  if (msg.data.cmnd) {
    eval(msg.data.cmnd);
  }
});

console.log("injected To Real Dom");

This way , I can have 2 way communication between CS to Real Dom. Its very usefull for example if you need to listen webscoket events , or to any in memory variables or events.

Not the answer you're looking for? Browse other questions tagged or ask your own question.