161

I'm trying to access the activeTab DOM content from my popup. Here is my manifest:

{
  "manifest_version": 2,

  "name": "Test",
  "description": "Test script",
  "version": "0.1",

  "permissions": [
    "activeTab",
    "https://api.domain.com/"
  ],

  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },
  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",

  "browser_action": {
    "default_icon": "icon.png",
    "default_title": "Chrome Extension test",
    "default_popup": "index.html"
  }
}

I'm really confused whether background scripts (event pages with persistence: false) or content_scripts are the way to go. I've read all the documentation and other SO posts and it still makes no sense to me.

Can someone explain why I might use one over the other.

Here is the background.js that I've been trying:

chrome.extension.onMessage.addListener(
  function(request, sender, sendResponse) {
    // LOG THE CONTENTS HERE
    console.log(request.content);
  }
);

And I'm just executing this from the popup console:

chrome.tabs.getSelected(null, function(tab) {
  chrome.tabs.sendMessage(tab.id, { }, function(response) {
    console.log(response);
  });
});

I'm getting:

Port: Could not establish connection. Receiving end does not exist. 

UPDATE:

{
  "manifest_version": 2,

  "name": "test",
  "description": "test",
  "version": "0.1",

  "permissions": [
    "tabs",
    "activeTab",
    "https://api.domain.com/"
  ],

  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"]
    }
  ],

  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",

  "browser_action": {
    "default_icon": "icon.png",
    "default_title": "Test",
    "default_popup": "index.html"
  }
}

content.js

chrome.extension.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.text && (request.text == "getDOM")) {
      sendResponse({ dom: document.body.innerHTML });
    }
  }
);

popup.html

chrome.tabs.getSelected(null, function(tab) {
  chrome.tabs.sendMessage(tab.id, { action: "getDOM" }, function(response) {
    console.log(response);
  });
});

When I run it, I still get the same error:

undefined
Port: Could not establish connection. Receiving end does not exist. lastError:30
undefined
Simon Dragsbæk
  • 2,367
  • 3
  • 30
  • 53
brandonhilkert
  • 4,205
  • 5
  • 25
  • 38

5 Answers5

226

The terms "background page", "popup", "content script" are still confusing you; I strongly suggest a more in-depth look at the Google Chrome Extensions Documentation.

Regarding your question if content scripts or background pages are the way to go:

Content scripts: Definitely
Content scripts are the only component of an extension that has access to the web-page's DOM.

Background page / Popup: Maybe (probably max. 1 of the two)
You may need to have the content script pass the DOM content to either a background page or the popup for further processing.


Let me repeat that I strongly recommend a more careful study of the available documentation!
That said, here is a sample extension that retrieves the DOM content on StackOverflow pages and sends it to the background page, which in turn prints it in the console:

background.js:

// Regex-pattern to check URLs against. 
// It matches URLs like: http[s]://[...]stackoverflow.com[...]
var urlRegex = /^https?:\/\/(?:[^./?#]+\.)?stackoverflow\.com/;

// A function to use as callback
function doStuffWithDom(domContent) {
    console.log('I received the following DOM content:\n' + domContent);
}

// When the browser-action button is clicked...
chrome.browserAction.onClicked.addListener(function (tab) {
    // ...check the URL of the active tab against our pattern and...
    if (urlRegex.test(tab.url)) {
        // ...if it matches, send a message specifying a callback too
        chrome.tabs.sendMessage(tab.id, {text: 'report_back'}, doStuffWithDom);
    }
});

content.js:

// Listen for messages
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
    // If the received message has the expected format...
    if (msg.text === 'report_back') {
        // Call the specified callback, passing
        // the web-page's DOM content as argument
        sendResponse(document.all[0].outerHTML);
    }
});

manifest.json:

{
  "manifest_version": 2,
  "name": "Test Extension",
  "version": "0.0",
  ...

  "background": {
    "persistent": false,
    "scripts": ["background.js"]
  },
  "content_scripts": [{
    "matches": ["*://*.stackoverflow.com/*"],
    "js": ["content.js"]
  }],
  "browser_action": {
    "default_title": "Test Extension"
  },

  "permissions": ["activeTab"]
}
Piper
  • 1,266
  • 3
  • 15
  • 26
gkalpak
  • 47,844
  • 8
  • 105
  • 118
  • Thanks for the response! I need to digest this a little bit more, but I think it'll be really helpful. One thing I realized is that chrome.runtime goes to both popup and background. So in my case, I have a popup, so I wouldn't have a need for a background script right? Rather, i would have my content script do the work on the DOM and then sendResponse back to chrome.runtime? – brandonhilkert Nov 03 '13 at 22:19
  • 1
    Indeed, chrome.runtime sends to both bg page and popup. Depending on what you are trying to do, you might not need anything other than the content script at all. In any case, if you need to process something in the popup, keep in mind that JS executed in the popup is immediately canceled as soon as the popup is closed/hiden. (And yes, if you only need to process thins in the popup, you don't necessarily need to have a bg page.) – gkalpak Nov 03 '13 at 22:28
  • I added an updated version of with your suggestions, but I'm still getting a similar error. Am i doing something wrong? – brandonhilkert Nov 04 '13 at 01:12
  • Nevermind. It appears I failed to reload the window i was testing in before I saw the changes. – brandonhilkert Nov 04 '13 at 01:56
  • What's the difference between `chrome.tabs.sendMessage` and `chrome.runtime.sendMessage`? – solvingPuzzles Sep 24 '14 at 19:49
  • 8
    @solvingPuzzles: `chrome.runtime.sendMessage` sends messages to the BackgroundPage and to Popups. `chrome.tabs.sendMessage` sends messages to ContentScripts. – gkalpak Sep 24 '14 at 20:41
  • 48
    Downvoted since this answer does not explain how to obtain the ACTUAL DOM from the current tab. – John Paul Barbagallo Oct 30 '14 at 19:22
  • 3
    @JohnPaulBarbagallo: The question was about getting the DOM content, not about accessing/manipulating the actual DOM. I think my answer does that (and others seem to think the same way). If you have a better solution, post it as an answer. If you have a different requirement, post it as a new question. In any case, thx for the feedback :) – gkalpak Oct 31 '14 at 07:35
  • Where does the content print out? – zoltar Oct 22 '15 at 16:27
  • 2
    @zoltar: It is printed in the background-page's console. – gkalpak Oct 23 '15 at 06:54
  • Thank you, I actually figured it out on my own but a good thing to have here for anyone searching! (Background page is clickable through settings > extensions in Chrome.) – zoltar Oct 24 '15 at 00:09
  • When I tried this I did not get the original page's DOM; what was printed was the DOM of the extension which is not useful. – Graham Wheeler Nov 10 '15 at 05:53
  • @GrahamWheeler: Make sure you used it correctly. It worked for me (and judging by the upvotes, it seems to have worked for a bunch of people as well). If you find that it is still not working for you, I suggest opening a new question (possibly pointing back to this answer) including your code and describing the actual (vs expected) behaviour. – gkalpak Nov 10 '15 at 07:05
  • So in the manifest file, AND in the background.js file you are maintaining a regex to match the StackOverflow url. That doesn't seem right. Why would you want to have to maintain two different regex values? – DanGordon Jun 29 '16 at 18:37
  • @DanGordon: I am always open to suggestions/improvements :) The regex is necessary in `manifest.json` in order to know where to inject scripts. You might or might not need it in `background.js`, depending on your business logic. – gkalpak Jun 29 '16 at 20:59
  • 1
    If your user interaction *begins* with the user clicking a `browserAction` button, then the content script should be injected with [`chrome.tabs.executeScript()`](https://developer.chrome.com/extensions/tabs#method-executeScript) instead of a *manifest.json* `content_script` entry. That way your content script does not burden the browser by being injected into every page just to wait to be used. Using `chrome.tabs.executeScript()`, the script can begin functioning when it is injected with [the data, if any is needed, that has been passed to it](https://stackoverflow.com/a/40815514/3773011). – Makyen Aug 22 '17 at 03:20
  • 2
    I have copy/paster this answer but cannot get any console.log form the content script. help please! – ClementWalter Sep 03 '18 at 17:50
  • @ClementWalter, the same:D – Arthur Apr 09 '20 at 09:58
  • 1
    Thanks piperchester and @gkalpak. Your answer solved my question -> https://stackoverflow.com/questions/62774734/safari-web-extension-injecting-script-only-when-extension-button-is-clicked. The issue was mainly related to message sending, – Paresh Thakor Jul 09 '20 at 10:36
  • this is a great answer: https://stackoverflow.com/a/1964445/5195852 – ducci Aug 13 '20 at 09:41
  • document.all[0].outerHTML get popup.html, the the current tab innerHTML. How to get the current tab html ? – assayag.org Nov 02 '20 at 13:32
  • It should be noted that `chrome.browserAction.onClicked` [*does not fire*](https://developer.chrome.com/docs/extensions/reference/browserAction/) if your extension has a pop-up. – DragonBobZ Dec 23 '20 at 00:16
104

Update for manifest v3

chrome.tabs.executeScript doesn't work in manifest v3, as noted in the comments of this answer. Instead, use chrome.scripting. You can specify a separate script to run instead of a function, or specify a function (without having to stringify it!).

Remember that your manifest.json will need to include

...
"manifest_version": 3,
"permissions": ["scripting"],
...

You don't have to use the message passing to obtain or modify DOM. I used chrome.tabs.executeScriptinstead. In my example I am using only activeTab permission, therefore the script is executed only on the active tab.

part of manifest.json

"browser_action": {
    "default_title": "Test",
    "default_popup": "index.html"
},
"permissions": [
    "activeTab",
    "<all_urls>"
]

index.html

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <button id="test">TEST!</button>
    <script src="test.js"></script>
  </body>
</html>

test.js

document.getElementById("test").addEventListener('click', () => {
    console.log("Popup DOM fully loaded and parsed");

    function modifyDOM() {
        //You can play with your DOM here or check URL against your regex
        console.log('Tab script:');
        console.log(document.body);
        return document.body.innerHTML;
    }

    //We have permission to access the activeTab, so we can call chrome.tabs.executeScript:
    chrome.tabs.executeScript({
        code: '(' + modifyDOM + ')();' //argument here is a string but function.toString() returns function's code
    }, (results) => {
        //Here we have just the innerHTML and not DOM structure
        console.log('Popup script:')
        console.log(results[0]);
    });
});
Eric
  • 21
  • 4
Oskar
  • 2,548
  • 1
  • 20
  • 22
15

For those who tried gkalpak answer and it did not work,

be aware that chrome will add the content script to a needed page only when your extension enabled during chrome launch and also a good idea restart browser after making these changes

bxN5
  • 1,430
  • 2
  • 16
  • 27
  • Google specifies in their docs that content scripts need to have the page in question refreshed as well as the extension in order to see changes. – Joe Moore Jan 15 '23 at 19:15
3

I was not able to make the above work. See below the simplest setup that worked for me with V3.

manifest.json

{
  "name": "DOM Reader",
  "version": "1.0",
  "manifest_version": 3,
  "description": "Reads the content of a page.",
  "permissions": [
    "scripting",
    "activeTab"
  ],
  "action": {
    "default_popup": "index.html"
  }
}

index.html

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <button id="read-content">Read content</button>
    <script src="contentScript.js"></script>
  </body>
</html>

contentScript.js

document.getElementById('read-content').addEventListener('click', () => {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
        const tab = tabs[0];

        function printTitle() {
            const title = document.title;
            console.log(title);
        };

        chrome.scripting.executeScript({
            target: { tabId: tab.id },
            func: printTitle,
            //        files: ['contentScript.js'],  // To call external file instead
        }).then(() => console.log('Injected a function!'));
    });
});
kahlo
  • 2,314
  • 3
  • 28
  • 37
  • The output code in `executeScript()` will run in the context (i.e. print to the console) of the target tab, not the addon popup/action. The next step that is missing in this basic example would be to pass the "output" from the `executeScript()` func to the extension's action and display it there. – wesinat0r Jul 12 '23 at 01:18
0

Here is a manifest v3 version that passes the DOM content to the extension's popup context (the action page), using messaging (https://developer.chrome.com/docs/extensions/mv3/messaging/)

manifest.json

{
  "name": "DOM Reader",
  "version": "1.0",
  "manifest_version": 3,
  "description": "Reads the content of a page.",
  "permissions": [
    "scripting",
    "activeTab"
  ],
  "action": {
    "default_popup": "index.html"
  }
}

index.html

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <button id="read-content">Read content</button>
    <script src="contentScript.js"></script>
    <div id='result-div' style="width:500px">
        <code id='result'></code>
    </div>
  </body>
</html>

contentScript.js

document.getElementById('read-content').addEventListener('click', () => {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
        const tab = tabs[0];
        
        function printTitle() {
            //console.log("inside printTitle func");
            const title = document.title;
            var resultStr = "doc title: " + document.title;
            console.log(resultStr);
            
            // https://developer.chrome.com/docs/extensions/mv3/messaging/
            (async () => {
                const response = await chrome.runtime.sendMessage({info: resultStr});
                // do something with response here, not outside the function
                console.log(response);
            })();
            
            //return resultStr;
        };

        chrome.scripting.executeScript({
            target: { tabId: tab.id },
            func: printTitle,
            //        files: ['contentScript.js'],  // To call external file instead
        }).then(() => console.log('Injected a function!'));
    });
});

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "from a content script: " + sender.tab.url :
                "from the extension");
    var resp = request.info;
    if (resp) {
        document.getElementById("result").innerText = resp;
        sendResponse({farewell: "thanks for sending! goodbye"});
    }
  }
);

chrome.runtime.sendMessage() is used within the executeScript function to pass the content to the action popup.

wesinat0r
  • 121
  • 8