Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Access global js variables from js injected by a chrome extension

I am trying to create an extension that will have a side panel. This side panel will have buttons that will perform actions based on the host page state.

I followed this example to inject the side panel and I am able to wire up a button onClick listener. However, I am unable to access the global js variable. In developer console, in the scope of the host page I am able to see the variable (name of variable - config) that I am after. but when I which to the context of the sidepanel (popup.html) I get the following error -
VM523:1 Uncaught ReferenceError: config is not defined. It seems like popup.html also runs in a separate thread.

How can I access the global js variable for the onClick handler of my button?

My code:

manifest.json

{
    "manifest_version": 2,

    "name": "Hello World",
    "description": "This extension to test html injection",
    "version": "1.0",
    "content_scripts": [{
        "run_at": "document_end",
        "matches": [
            "https://*/*",
            "http://*/*"
        ],
        "js": ["content-script.js"]
    }],
    "browser_action": {
        "default_icon": "icon.png"
    },
    "background": {
        "scripts":["background.js"]
    },
    "permissions": [
        "activeTab"
    ],
    "web_accessible_resources": [
        "popup.html",
        "popup.js"
    ]
}

background.js

chrome.browserAction.onClicked.addListener(function(){
    chrome.tabs.query({active: true, currentWindow: true}, function(tabs){
        chrome.tabs.sendMessage(tabs[0].id,"toggle");
    })
});

content-script.js

chrome.runtime.onMessage.addListener(function(msg, sender){
    if(msg == "toggle"){
        toggle();
    }
})

var iframe = document.createElement('iframe'); 
iframe.style.background = "green";
iframe.style.height = "100%";
iframe.style.width = "0px";
iframe.style.position = "fixed";
iframe.style.top = "0px";
iframe.style.right = "0px";
iframe.style.zIndex = "9000000000000000000";
iframe.frameBorder = "none"; 
iframe.src = chrome.extension.getURL("popup.html")

document.body.appendChild(iframe);

function toggle(){
    if(iframe.style.width == "0px"){
        iframe.style.width="400px";
    }
    else{
        iframe.style.width="0px";
    }
}

popup.html

<head>
<script src="popup.js"> </script>
</head>
<body>
<h1>Hello World</h1>
<button name="toggle" id="toggle" >on</button>
</body>

popup.js

document.addEventListener('DOMContentLoaded', function() {
  document.getElementById("toggle").addEventListener("click", handler);
});

function handler() {
  console.log("Hello");
  console.log(config);
}
like image 904
Leon Avatar asked Oct 22 '17 01:10

Leon


People also ask

How do you access variables in JavaScript?

In JavaScript, variables can be accessed from another file using the <script> tags or the import or export statement. The script tag is mainly used when we want to access variable of a JavaScript file in an HTML file. This works well for client-side scripting as well as for server-side scripting.

Can a function access a global variable in JavaScript?

A JavaScript global variable is declared outside the function or declared with window object. It can be accessed from any function.


Video Answer


1 Answers

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.
like image 67
wOxxOm Avatar answered Jan 04 '23 03:01

wOxxOm