Chrome Extension: Get Page Variables in Content Script

Chrome Extension: Get Page Variables in Content Script

If you really need to, you can insert a <script> element into the page's DOM; the code inside your <script> element will be executed and that code will have access to JavaScript variables at the scope of the window. You can then communicate them back to the content script using data- attributes and firing custom events.

Sound awkward? Why yes, it is, and intentionally so for all the reasons in the documentation that serg has cited. But if you really, really need to do it, it can be done. See here and here for more info. And good luck!

How to access page variables from Chrome extension background script

You can create a new background script with the following code:

chrome.browserAction.onClicked.addListener( function() {
chrome.tabs.executeScript( { file: 'inject.js' } );
});

Your inject.js should stay as part of the extension, but you don't need to mention it in the manifest. This way, it will be run as a content script each time you press the extension icon. You didn't include the part of the manifest where you define the browserAction, but you just need to not specify a default_popup.

chrome extension manifest v3, accessing window variable

Maybe I have not understand well your problem but:

1. If the page you want to access to is a page extension, you don't have to inject any content script to retreive any variable.

2. If you want to get a content script variable you have created\set in a tab, you have to send a message to content script in that tab asking to return its value along that message.

3. If you want to get a variable of the main page (out of your possible content script) you don' have any hope due to isolated world rules.

Accessing all the window variables of the current tab in chrome extension

What you are asking for is to inject a script an get the value of a variable defined in the original page script. There are two answers to this problem:

1. Official Google answer

This is not possible. Content script injected in a page is sandboxed and can't access to original page javascript scope. This means that you can't access to variable, function and objects defined in the original page's javascript. And your variable, function and objects will not be accessible from the original page.

Only the DOM of the page is shared. That allow you to modify the content of the page. But you can't, for example, delete an existing event handler.

This is for evident security and safety reason. If you override without knowing it a function of the original page, it would break it.

Take a look here for more information

2. Unofficial and dirty answer

There is a way to bypass the chrome sand box restriction. This come with the shared DOM that allow you to add a <script src="..."><\script> to the page. The script will be loaded and executed in the original page's javascript VM so you will have access to the global javascript scope.

But this way, will not have access to the Chrome extension API, because you are running code in the original page. So the communication with the background page or the injected Content Script will be difficult.

A common way to do this is to add a hidden <div> to the page and put in it the result you want to send to your Content Script. You put the result as text (with JSON.stringify for example) and then read the result with your Content Script.

It's really dirty and it have to be use only in last try.

Access variables and functions defined in page context using a content script

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

  • Method 1 can set the URL of the script element in the content script:

    s.src = chrome.runtime.getURL('script.js?') + new URLSearchParams({foo: 1});

    Then script.js can read it:

    const params = new URLSearchParams(document.currentScript.src.split('?')[1]);
    console.log(params.get('foo'));
  • Method 4 executeScript has args parameter, registerContentScripts currently doesn't (hopefully it'll be added in the future).

Access global js variables from js injected by a chrome extension

Since content scripts run in an "isolated world" the JS variables of the page cannot be directly accessed from an extension, you need to run code in page's main world.

WARNING! DOM element cannot be extracted as an element so just send its innerHTML or another attribute. Only JSON-compatible data types can be extracted (string, number, boolean, null, and arrays/objects of these types), no circular references.

1. ManifestV3 in modern Chrome 95 or newer

This is the entire code in your extension popup/background script:

async function getPageVar(name, tabId) {
const [{result}] = await chrome.scripting.executeScript({
func: name => window[name],
args: [name],
target: {
tabId: tabId ??
(await chrome.tabs.query({active: true, currentWindow: true}))[0].id
},
world: 'MAIN',
});
return result;
}

Usage:

(async () => {
const v = await getPageVar('foo');
console.log(v);
})();

See also how to open correct devtools console.

2. ManifestV3 in old Chrome and ManifestV2

We'll extract the variable and send it into the content script via DOM messaging. Then the content script can relay the message to the extension script in iframe or popup/background pages.

  • ManifestV3 for Chrome 94 or older needs two separate files

    content script:

    const evtToPage = chrome.runtime.id;
    const evtFromPage = chrome.runtime.id + '-response';

    chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
    if (msg === 'getConfig') {
    // DOM messaging is synchronous so we don't need `return true` in onMessage
    addEventListener(evtFromPage, e => {
    sendResponse(JSON.parse(e.detail));
    }, {once: true});
    dispatchEvent(new Event(evtToPage));
    }
    });

    // Run the script in page context and pass event names
    const script = document.createElement('script');
    script.src = chrome.runtime.getURL('page-context.js');
    script.dataset.args = JSON.stringify({evtToPage, evtFromPage});
    document.documentElement.appendChild(script);

    page-context.js should be exposed in manifest.json's web_accessible_resources, example.

    // This script runs in page context and registers a listener.
    // Note that the page may override/hook things like addEventListener...
    (() => {
    const el = document.currentScript;
    const {evtToPage, evtFromPage} = JSON.parse(el.dataset.args);
    el.remove();
    addEventListener(evtToPage, () => {
    dispatchEvent(new CustomEvent(evtFromPage, {
    // stringifying strips nontranferable things like functions or DOM elements
    detail: JSON.stringify(window.config),
    }));
    });
    })();
  • ManifestV2 content script:

    const evtToPage = chrome.runtime.id;
    const evtFromPage = chrome.runtime.id + '-response';

    // this creates a script element with the function's code and passes event names
    const script = document.createElement('script');
    script.textContent = `(${inPageContext})("${evtToPage}", "${evtFromPage}")`;
    document.documentElement.appendChild(script);
    script.remove();

    // this function runs in page context and registers a listener
    function inPageContext(listenTo, respondWith) {
    addEventListener(listenTo, () => {
    dispatchEvent(new CustomEvent(respondWith, {
    detail: window.config,
    }));
    });
    }

    chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
    if (msg === 'getConfig') {
    // DOM messaging is synchronous so we don't need `return true` in onMessage
    addEventListener(evtFromPage, e => sendResponse(e.detail), {once: true});
    dispatchEvent(new Event(evtToPage));
    }
    });
  • usage example for extension iframe script in the same tab:

    function handler() {
    chrome.tabs.getCurrent(tab => {
    chrome.tabs.sendMessage(tab.id, 'getConfig', config => {
    console.log(config);
    // do something with config
    });
    });
    }
  • usage example for popup script or background script:

    function handler() {
    chrome.tabs.query({active: true, currentWindow: true}, tabs => {
    chrome.tabs.sendMessage(tabs[0].id, 'getConfig', config => {
    console.log(config);
    // do something with config
    });
    });
    }

So, basically:

  1. the iframe script gets its own tab id (or the popup/background script gets the active tab id) and sends a message to the content script
  2. the content script sends a DOM message to a previously inserted page script
  3. the page script listens to that DOM message and sends another DOM message back to the content script
  4. the content script sends it in a response back to the extension script.

Chrome Extension: How do I get a web page to access functions defined by my content script?

There are many ways to exhange information between the specific tab you're viewing and your background script. You can check those out on this answer.

The solution to your Chrome extension ID problem is using chrome.runtime.id to get the extension ID and inject it alongside your other code into the required page and then using it to send a message back.

Chrome Extension Pass Variable from popup.js to content script

You can use messaging to send the data to the injected script, but you need the tab Id.

popup.js

function FireInjectionScript() {
//Want to Pass this variable.
var updateTextTo = document.getElementById('mat_no').value.trim();
chrome.tabs.query({active: true, currentWindow: true}, tabs => {
chrome.tabs.executeScript(tabs[0].id,{file: 'InjectionScript.js'},()=>{
chrome.tabs.sendMessage(tabs[0].id,{myVar:updateTextTo});
});
});
}

document.getElementById('caseButton').addEventListener('click', FireInjectionScript);

InjectionScript.js

chrome.runtime.onMessage.addListener(message=>{
if (message.myVar) {
//here you can access message.myVar
}
});


Related Topics



Leave a reply



Submit