615

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?

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
André Alves
  • 6,535
  • 3
  • 17
  • 23
  • 21
    try to remove the quotes around your function name: `player.addEventListener("onStateChange", state);` – Eduardo Mar 01 '12 at 17:13
  • 5
    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](https://developer.chrome.com/extensions/match_patterns) – Nilay Vishwakarma Jan 31 '17 at 19:34
  • 2
    Also see https://bugs.chromium.org/p/chromium/issues/detail?id=478183 – Pacerier Jul 15 '17 at 19:21

6 Answers6

1234

Underlying cause:
Content scripts are executed in an "isolated world" environment.

Solution:
Inject the code into the page using DOM - that code will be able to access functions/variables of the page context ("main world") or expose functions/variables to the page context (in your case it's the state() method).

  • Note in case communication with the page script is needed:
    Use DOM CustomEvent handler. Examples: one, two, and three.

  • Note in case chrome API is needed in the page script:
    Since chrome.* APIs can't be used in the page script, you have to use them in the content script and send the results to the page script via DOM messaging (see the note above).

Safety warning:
A page may redefine or augment/hook a built-in prototype so your exposed code may fail if the page did it in an incompatible fashion. If you want to make sure your exposed code runs in a safe environment then you should either a) declare your content script with "run_at": "document_start" and use Methods 2-3 not 1, or b) extract the original native built-ins via an empty iframe, example. Note that with document_start you may need to use DOMContentLoaded event inside the exposed code to wait for DOM.

Table of contents

  • Method 1: Inject another file - ManifestV3 compatible
  • Method 2: Inject embedded code - MV2
  • Method 2b: Using a function - MV2
  • Method 3: Using an inline event - ManifestV3 compatible
  • Method 4: Using executeScript's world - ManifestV3 only
  • Method 5: Using world in manifest.json - ManifestV3 only, Chrome 111+
  • Dynamic values in the injected code

Method 1: Inject another file (ManifestV3/MV2)

Particularly good when you have lots of code. Put the code in a file within your extension, say script.js. Then load it in your content script like this:

var s = document.createElement('script');
s.src = chrome.runtime.getURL('script.js');
s.onload = function() { this.remove(); };
// see also "Dynamic values in the injected code" section in this answer
(document.head || document.documentElement).appendChild(s);

The js file must be exposed in web_accessible_resources:

  • manifest.json example for ManifestV2

    "web_accessible_resources": ["script.js"],
    
  • manifest.json example for ManifestV3

    "web_accessible_resources": [{
      "resources": ["script.js"],
      "matches": ["<all_urls>"]
    }]
    

If not, the following error will appear 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 (MV2)

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 (MV2)

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 (ManifestV3/MV2)

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.

Method 4: Using chrome.scripting API world (ManifestV3 only)

  • Chrome 95 or newer, chrome.scripting.executeScript with world: 'MAIN'
  • Chrome 102 or newer, chrome.scripting.registerContentScripts with world: 'MAIN', also allows runAt: 'document_start' to guarantee early execution of the page script.

Unlike the other methods, this one is for the background script or the popup script, not for the content script. See the documentation and examples.

Method 5: Using world in manifest.json (ManifestV3 only)

In Chrome 111 or newer you can add "world": "MAIN" to content_scripts declaration in manifest.json to override the default value which is ISOLATED. The scripts run in the listed order.

  "content_scripts": [{
    "js": ["content.js"],
    "matches": ["<all_urls>"],
    "run_at": "document_start"
  }, {
    "world": "MAIN",
    "js": ["page.js"],
    "matches": ["<all_urls>"],
    "run_at": "document_start"
  }],

Dynamic values in the injected code (MV2)

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]).slice(1, -1) + ')';

Dynamic values in the injected code (ManifestV3)

  • Use method 1 and add the following line:

    s.dataset.params = JSON.stringify({foo: 'bar'});
    

    Then the injected script.js can read it:

    (() => {
      const params = JSON.parse(document.currentScript.dataset.params);
      console.log('injected params', params);
    })();
    

    To hide the params from the page scripts you can put the script element inside a closed ShadowDOM.

  • Method 4 executeScript has args parameter, registerContentScripts currently doesn't (hopefully it'll be added in the future).

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • 136
    This answer should be part of official docs. Official docs should ship with recommended way --> 3 ways to do the same thing... Wrong? – Mars Robertson Jun 09 '13 at 11:17
  • 4
    Usually method 1 is better wherever possible, due to Chrome's CSP (content security policy) restrictions for some extensions. – Qantas 94 Heavy Aug 02 '13 at 02:33
  • 15
    @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 Aug 02 '13 at 07:24
  • 7
    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 ` – Rob W Aug 29 '13 at 13:35
  • 9
    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 Sep 26 '13 at 21:08
  • 3
    @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 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 ` – Rob W Sep 27 '13 at 07:42
  • 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 Oct 02 '13 at 23:50
  • 1
    @MrAzulay I've edited the answer and added "Method 3" which can be used for your purposes. – Rob W Oct 03 '13 at 08:29
  • @RobW code injected using Method 2 with the option `document_start` runs before code in the page. It even runs synchronously. – rsanchez Dec 05 '13 at 16:28
  • @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 Feb 11 '14 at 00:29
  • 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 – Rob W Feb 11 '14 at 07:05
  • 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 Feb 13 '14 at 03:14
  • 1
    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 Mar 26 '14 at 07:30
  • @Buba I recommend against that, because closure is a nice way to have (local) variables without polluting the global namespace. – Rob W Mar 26 '14 at 08:08
  • 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 Aug 10 '14 at 15:49
  • 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 Oct 12 '14 at 14:39
  • @f.ardelian Method 1 is still working. ["\[VM\]" has no special meaning](https://stackoverflow.com/questions/17367560/chrome-development-tool-vm-file-from-javascript). 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 Oct 12 '14 at 14:42
  • @RobW: I am positive the variable is there but I cannot access it from the injected script. http://pastebin.com/JYDHQ8p6 – f.ardelian Oct 12 '14 at 14:58
  • @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 Oct 12 '14 at 15:00
  • @RobW: Unfortunately, I cannot share it. The only thing that I find "special" about this extension is that it is unpacked. – f.ardelian Oct 12 '14 at 15:06
  • 1
    @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 Oct 12 '14 at 15:07
  • 1
    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 Oct 22 '14 at 13:06
  • @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 Oct 22 '14 at 13:09
  • @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 Oct 22 '14 at 13:36
  • @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. – dwardu Sep 07 '15 at 00:12
  • 1
    @EdwardGrech That's not possible. – Rob W Sep 07 '15 at 11:32
  • 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. – dwardu Sep 07 '15 at 12:04
  • How to can I access page variable with this way? – Lai32290 Dec 10 '15 at 01:15
  • 2
    @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 ` – Rob W Dec 10 '15 at 10:29
  • @RobW Am I right if I suppose that site using a very strict `Content-Security-Policy HTTP response header` as does for example https://twitter.com/ will break all ways to "inject" code in the page? Why is Chrome not "relaxing" the policy for "extensions"? – manuell Feb 09 '16 at 16:02
  • @manuell Inline scripts do bypass the page's CSP (implemented in https://crbug.com/181602), external scripts are blocked though (this is an open bug - https://crbug.com/392338). – Rob W Feb 09 '16 at 16:05
  • @RobW Thanks for your clear response. I will now try to understand why I can inject scripts using method 1 and 2b, EXCEPT in https://twitter.com/ – manuell Feb 09 '16 at 16:21
  • @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 http://stackoverflow.com/questions/35311671/is-there-a-way-to-work-around-sites-breaking-console-log – manuell Feb 11 '16 at 16:02
  • @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 Apr 22 '16 at 22:58
  • @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 Apr 22 '16 at 23:06
  • this conflicts with https://developer.chrome.com/extensions/content_scripts#registration – noɥʇʎԀʎzɐɹƆ Sep 17 '16 at 15:42
  • @RobW Can you help me on this http://stackoverflow.com/questions/40045344/getting-an-oauth-access-token-to-work-for-chrome-extension – uncivilized Oct 14 '16 at 17:35
  • 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 Oct 18 '16 at 22:47
  • 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 Aug 15 '17 at 19:55
  • Any submodules of the webpack one cannot be found. – SuperUberDuper Aug 15 '17 at 20:00
  • @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 Aug 15 '17 at 20:17
  • You can use `document.head.appendChild(script);` for method 1 (Chrome will insert the `` if the page originally didn't have one). – thdoan Aug 12 '18 at 05:37
  • Method 3 (inline event) was broken in Chrome 71, but fixed in 72, see https://crbug.com/912069. Looks like there's still a chance the fix may still land in 71 too. – wOxxOm Dec 13 '18 at 18:02
  • Is there any way to send messages back to the background script using the first method? – Rod Lima Mar 07 '19 at 23:14
  • 1
    @Rodrigo Yes. Either by communicating with the content script first (e.g. via CustomEvents) or via `externally_connectable` - https://developer.chrome.com/extensions/manifest/externally_connectable Be careful though: if you allow the script in the web page's context to send a message, then any website where your script runs can also forge that message. If your message results in powerful behavior, then you need to perform validation where needed. – Rob W Mar 08 '19 at 05:12
  • 1
    I am trying to inject a script that has been uglified by webpack first and I am getting the following error: `Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'strict-dynamic' 'unsafe-inline' https: 'report-sample' 'nonce-`. Any hints? – Dimitris Karagiannis Jul 04 '19 at 12:51
  • To clarify, this happens when the code has been extracted with `eval-source-map` – Dimitris Karagiannis Jul 04 '19 at 13:17
  • I also get the 'string is not a function error' on setting `textContent`. In console it works somehow. `setTimeout` didn't help. – Cubimon Nov 05 '19 at 21:09
  • How can I add some Javascript code at the very beginning, so the original code will be executed after the injection? Example: I want to modify `Date.prototype.toString = function() { return this.toISOString() }`, because it is desired such new toString() function being used. – Ωmega Jan 26 '21 at 14:32
  • 1
    why do we need to remove the following tag? `s.onload = function() { this.remove(); };` – Anatoly Apr 07 '21 at 20:53
  • Method 2c: For those who have a lot of code to insert and think 2b is not practical. Write a "build.py" Python script (or any other language you like) that will convert your code into a javascript string, then build a new javascript file containing that string and the code responsible to insert it into the page. – FourchetteVerte Apr 20 '21 at 15:06
  • I got an error using solution 1 because the format for "web_accessible_resources" changed, see https://developer.chrome.com/docs/extensions/mv3/manifest/web_accessible_resources/ . One should do something like: ```"web_accessible_resources": [ {"matches": ["http://mobile.twitter.com/*"], "resources":["script.js"] } ]``` – Maarten Derickx Apr 27 '21 at 14:10
  • @Rob W After 2 days of hunting, option 1 was the best solution I saw. Thank you so much!! – Reena Verma Apr 05 '22 at 18:07
  • is Method 4 not going to work for content script? – user924 Jul 10 '22 at 09:11
  • 1
    Method 3 is needed for custom js from users, but some websites have this error: "Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'report-sample' 'nonce-yyyy' 'unsafe-inline' 'strict-dynamic' https: http: 'unsafe-eval'". Note that 'unsafe-inline' is ignored if either a hash or nonce value is present in the source list." – Jeff Baker Jul 30 '22 at 07:03
  • I'm sure any of these 50+ comments have mentioned it somewhere, but you are a god among men. Thank you! – Vidal Tan Aug 16 '22 at 17:32
  • @wOxxOm I'm trying to the first step you explained in MV3, and I'm getting this error: `Failed to load resource: net::ERR_FILE_NOT_FOUND script.js:1` even though the files exists. Any idea what I'm doing wrong? – Sijaan Hallak Aug 18 '22 at 22:11
  • You need to reload the extension after editing manifest.json. If this doesn't help, please open a new question with [MCVE](/help/mcve). – wOxxOm Aug 19 '22 at 04:45
  • @RobW Method 1 unfortunately doesn't work for me with local files. I have described the issue in the following link, could you take a moment and maybe comment on this if you know something that may fix this? https://stackoverflow.com/q/73627752/2875404 – user2875404 Sep 06 '22 at 22:59
  • Does Method 4 have any concrete examples? You are expected to provide a tabId, which comes from... where? – Graham Leggett Sep 16 '22 at 13:25
  • 1
    Can **Method 4** (`chrome.scripting.registerContentScripts` with `world: 'MAIN'`) be **blocked with page's CSP**? – nikitakot Jan 16 '23 at 11:01
  • Hi ! I just wanted to thank you for this detailled explaination :) – YanouHD Feb 19 '23 at 23:40
111

The only thing missing hidden from Rob W's excellent answer is how to communicate between the injected page script and the content script.

On the receiving side (either your content script or the injected page script) add an event listener:

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

On the initiator side (content script or injected page script) send the event:

var data = {
  allowedTypes: 'those supported by structured cloning, see the list below',
  inShort: 'no DOM elements or classes/functions',
};

document.dispatchEvent(new CustomEvent('yourCustomEvent', { detail: data }));

Notes:

  • DOM messaging uses structured cloning algorithm, which can transfer only some types of data in addition to primitive values. It can't send class instances or functions or DOM elements.
  • In Firefox, to send an object (i.e. not a primitive value) from the content script to the page context you have to explicitly clone it into the target using cloneInto (a built-in function), otherwise it'll fail with a security violation error.

    document.dispatchEvent(new CustomEvent('yourCustomEvent', {
      detail: cloneInto(data, document.defaultView),
    }));
    
wOxxOm
  • 65,848
  • 11
  • 132
  • 136
laktak
  • 57,064
  • 17
  • 134
  • 164
  • 1
    I've actually linked to the code and explanation at the second line of my answer, to http://stackoverflow.com/questions/9602022/chrome-extension-retrieving-gmails-original-message. – Rob W Oct 11 '13 at 09:00
  • 1
    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 Nov 06 '13 at 16:23
  • 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 Mar 10 '14 at 09:15
  • 1
    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. https://bugs.chromium.org/p/chromium/issues/detail?id=260378#c18 – jdunk Mar 28 '17 at 08:21
  • 4
    I think the official way is to use window.postMessage: https://developer.chrome.com/extensions/content_scripts#host-page-communication – Enrique Dec 15 '18 at 13:08
  • 2
    how to send response back from content script to initiator script – Vinay Sep 16 '19 at 07:41
  • Using dispatchEvent at the injected script and addEventListener at the content script, it works. However, the other way round doesn't work (ie dispatchEvent at content script and addEventListener at injected script), any ideas why? – KaiJun Dec 30 '20 at 13:37
  • doesn't work on YouTube site ! – user924 Jul 10 '22 at 09:54
  • @Enrique I wouldn't take it as a recommendation. I prefer `CustomEvent`s because I think it's harder to cause side effects with it (e.g. if there is already an `onmessage` listener). – WofWca May 23 '23 at 11:19
9

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.

Brad Parks
  • 66,836
  • 64
  • 257
  • 336
Dmitry Ginzburg
  • 7,391
  • 2
  • 37
  • 48
  • 3
    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 May 17 '15 at 17:27
  • @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 May 17 '15 at 17:36
  • 4
    [`document.currentScript`](https://developer.mozilla.org/en-US/docs/Web/API/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 May 17 '15 at 17:38
  • @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 May 17 '15 at 17:51
  • 2
    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 May 17 '15 at 17:58
  • @RobW maybe it would be easier then to wrap the whole `injected.js` into IIFE, right? Anyway, thanks for the professional advice=) – Dmitry Ginzburg May 17 '15 at 18:04
  • 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 May 17 '15 at 18:38
9

You can use a utility function I've created for the purpose of running code in the page context and getting back the returned value.

This is done by serializing a function to a string and injecting it to the web page.

The utility is available here on GitHub.

Usage examples -



// Some code that exists only in the page context -
window.someProperty = 'property';
function someFunction(name = 'test') {
    return new Promise(res => setTimeout(()=>res('resolved ' + name), 1200));
}
/////////////////

// Content script examples -

await runInPageContext(() => someProperty); // returns 'property'

await runInPageContext(() => someFunction()); // returns 'resolved test'

await runInPageContext(async (name) => someFunction(name), 'with name' ); // 'resolved with name'

await runInPageContext(async (...args) => someFunction(...args), 'with spread operator and rest parameters' ); // returns 'resolved with spread operator and rest parameters'

await runInPageContext({
    func: (name) => someFunction(name),
    args: ['with params object'],
    doc: document,
    timeout: 10000
} ); // returns 'resolved with params object'


Arik
  • 5,266
  • 1
  • 27
  • 26
8

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.

doron aviguy
  • 2,554
  • 2
  • 22
  • 18
4

If you wish to inject pure function, instead of text, you can use this method:

function inject(){
    document.body.style.backgroundColor = 'blue';
}

// this includes the function as text and the barentheses make it run itself.
var actualCode = "("+inject+")()"; 

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

And you can pass parameters (unfortunatelly no objects and arrays can be stringifyed) to the functions. Add it into the baretheses, like so:

function inject(color){
    document.body.style.backgroundColor = color;
}

// this includes the function as text and the barentheses make it run itself.
var color = 'yellow';
var actualCode = "("+inject+")("+color+")"; 
Tarmo Saluste
  • 585
  • 6
  • 18
  • This is pretty cool...but the second version, with a variable for color, does not work for me...I get 'unrecognized' and the code throws an error...does not see it as a variable. – 11teenth Aug 14 '18 at 20:02
  • 2
    The first example works like a charm. Thank you very much for this answer. This works even when inline-script is restricted and you sir have my respect. – John Yepthomi Nov 03 '21 at 10:49
  • 1
    Great workaround that does not require passing messages back and forth. – forgetso Dec 30 '21 at 13:31
  • 1
    content-script.js: Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'report-sample' – stallingOne Oct 25 '22 at 12:44