ChatGPT解决这个技术问题 Extra ChatGPT

Use a content script to access the page context variables and functions

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?

try to remove the quotes around your function name: player.addEventListener("onStateChange", state);
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

w
wOxxOm

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

Dynamic values in the injected code

Method 1: Inject another file - ManifestV3 compatible

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();
};
(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": [""] }]

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 compatible

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.

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]) + ')';

This answer should be part of official docs. Official docs should ship with recommended way --> 3 ways to do the same thing... Wrong?
Usually method 1 is better wherever possible, due to Chrome's CSP (content security policy) restrictions for some extensions.
@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"`.
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.
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.
w
wOxxOm

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), }));


I've actually linked to the code and explanation at the second line of my answer, to stackoverflow.com/questions/9602022/….
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.
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.
I think the official way is to use window.postMessage: developer.chrome.com/extensions/…
how to send response back from content script to initiator script
B
Brad Parks

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.


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.
@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?
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.
@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?
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;).
d
doron aviguy

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.


A
Arik

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'



T
Tarmo Saluste

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+")";


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.
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.
Great workaround that does not require passing messages back and forth.