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.

Leave a Comment