Communicating between Chrome DevTools and content script in extension - javascript

(I have already read this and it didn't work, and I've done a lot of searching and experimentation to no avail.)
I am writing a Chrome extension (BigConsole) with the goal of building a better Console tab for the Chrome developer tools. This means I would like to execute user-input code in the context of the page with access to the DOM and other variables on the page. To do this, the communication is structured as follows:
devtools creates a panel where the user writes code
When the user wants to execute code from the panel, the panel sends a message to a background script with the code
The background script receives the message/code from panel and passes it on to the content script which is injected into the page
The content script receives the message/code from the background script and injects a script element into the page which then runs the code
The result of the script on the page is then posted back to the content script with window.postMessage
The content script listens for the message/result from the page and passes it on to the background script
The background script receives the message/result from the content script and passes it on to the panel
The panel receives the message/result from the background script and inserts it into the log of results
Whew.
Right now, when the user tries to run the code, nothing happens. I put a bunch of console.log()s into the code but nothing appears in the console. My main question is, what have I done wrong here with the message passing that results in nothing happening? Alternatively, I would love to be told that I am making this way too complicated and there is a better way of doing things. Simplified code below...
panel.js:
window.onload = function() {
var port = chrome.runtime.connect({name: "Eval in context"});
// Add the eval'd response to the console when the background page sends it back
port.onMessage.addListener(function (msg) {
addToConsole(msg, false);
});
document.getElementById('run').addEventListener('click', function() {
var s = document.getElementById('console').value;
try {
// Ask the background page to ask the content script to inject a script
// into the DOM that can finally eval `s` in the right context.
port.postMessage(s);
// Outputting `s` to the log in the panel works here,
// but console.log() does nothing, and I can't observe any
// results of port.postMessage
}
catch(e) {}
});
};
background.js:
chrome.runtime.onConnect.addListener(function (port) {
// Listen for message from the panel and pass it on to the content
port.onMessage.addListener(function (message) {
// Request a tab for sending needed information
chrome.tabs.query({'active': true,'currentWindow': true}, function (tabs) {
// Send message to content script
if (tab) {
chrome.tabs.sendMessage(tabs[0].id, message);
}
});
});
// Post back to Devtools from content
chrome.runtime.onMessage.addListener(function (message, sender) {
port.postMessage(message);
});
});
content.js:
// Listen for the content to eval from the panel via the background page
chrome.runtime.onMessage.addListener(function (message, sender) {
executeScriptInPageContext(message);
});
function executeScriptInPageContext(m) { alert(m); }

As pointed out by Alex, here's a typo in your code which prevents it from working.
Drop your current code and use chrome.devtools.inspectedWindow.eval to directly run the code and parse the results. This simplifies your complicated logic to:
devtools creates a panel where the user writes code
devtools runs code
devtools handles result
PS. There is a way to manipulate the existing console, but I recommend against using it, unless it's for personal use. Two different ways to do this are shown in this answer.

Related

Can't access document element in chrome extension

I am currently making a chrome extension using the following Chrome tutorial:
https://developer.chrome.com/docs/extensions/mv3/getstarted/
I ran into some issues with my own code initially so now I'm just using their code as a basis for my own thing.
In popup.js, the tutorial has a function that changes the background color on click.
function setPageBackgroundColor() {
chrome.storage.sync.get("color", ({ color }) => {
document.body.style.backgroundColor = color;
});
I replaced the line in the code instead to what I wanted it to do.
document.getElementById(xyz).miscfunction(arg1, arg2);
The getting started extension works as intended (obviously). When I run my line of code in the console in Chrome, it works exactly as intended. However, when I run the line using the extension, it gives me the following error:
Uncaught TypeError: document.getElementById(...).miscfunction is not a function
I have tested and found out that getElementById is indeed getting the element I want because when I try to run this on a tab that does not have said element, it gives me an error saying that the element is null. It does not do so for the tab that actually does have the element.
What is preventing my extension from being able to access the miscfunction? When I was last trying to debug this months ago, I recall having some issues with injection and the content security policy but I do not remember what I did to get to that point. Any help would be appreciated.
EDIT:
I have since updated the popup to call a script js file instead of calling a function, and then put the function in a separate file.
To access the document element of the page you are viewing, you need to paste the required code in the content script.
You first need to add content script and it's scope in manifest.json
"content_scripts" : [{
"matches" : ["https://<website-you-are-working-on>"]
"js" : ["script.js"] //file containing the code you need to execute
}]
The pop up script now needs to send message to content script to do what you want to do.
popup.js
chrome.tabs.query({currentWindow: true, active: true}, function (tabs){
const activeTab = tabs[0];
chrome.tabs.sendMessage(activeTab.id, {
"action" : "change-background"
"payload": {
//required info
}
})});
Now listen for the message in the content script and do the required task.
script.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if(message.action == "change-background"){
//your code here
}
})

Call content script on URL change

I'm new to Chrome extensions. I know there's lots of similar questions and info out there, but none of it seems to address this problem.
I need my content script to execute on every page that matches *://*.youtube.com/watch?v=*.
I've tried using the above value and *://*.youtube.com/* as the match property, but neither works supposedly due to the way YouTube handles requests. I've also tried using the onhashchange event, but of course YouTube doesn't use anchors in their URLs. I've read about webRequest, but I don't need the function to be called when somebody is scrolling through the comments and the page loads more comments.
All I need is a way to call my content script when the URL changes. How exactly can I accomplish this?
Additionally, I cannot load the content script at document_start because the extension scrapes the HTML and parses it.
I had the same issue and this is what I did. I added a background script to listen to all changes
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.url) {
chrome.tabs.sendMessage(tabId, {
message: 'hello',
url: changeInfo.url
});
}
});
Then in my content script, I listen to it and reload the URL
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.message === 'hello') {
const { url } = request;
chrome.location.replace(url);
}
});

chrome extension - passing data from background to custom html page

Creating browser extension where I have to open new tab from background.js and pass JSON data to this new tab. In new tab I am manipulating/rendering DOM using passed JSON data.
Below is part of my background.js where I create new tab with custom URL and send JSON data object
....
var analyticsUrl = chrome.extension.getURL("analytics.html");
chrome.tabs.create({ url: analyticsUrl, selected: true }, sendDataToAnalytics);
function sendDataToAnalytics(tab)
{
console.log(JSON.stringify(txnDataJSON));
chrome.tabs.sendMessage(tab.id, {"action" : "renderChartsTxns", "txns" : JSON.stringify(txnDataJSON)});
}
....
My custom analytics.html page has
<script src="analytics.js" type="text/javascript"></script>
And analytics.js looks like below
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if(request.action == "renderChartsTxns")
{
// JSON parsing and analytics.html DOM processing
});
Problem is, my analytics.js listener is never receiving any messages. I've confirmed that background.js is sending JSON message as expected (using background page debugging)
BTW, analytics.js/html are not registered as part of manifest.json file but these files are part of extension package.
I did this setup today morning and everything was working properly for few hours (was able to receive JSON data in analytics.js), not sure what changed later and I lost message receiving in analytics.js (for debugging I tried clearing browser cache, uninstall and reinstalled chrome and much more but no luck yet!)
The callback of chrome.tabs.create returns as soon as the tab is created, not after it fully loads.
As such, you have a race condition: your message is potentially sent before the listener is initialized. Since it's a race condition, it can sometimes work.
The correct logic here would be to send a message requesting data from the newly opened tab, and use sendResponse to pass that data from the background.
// analytics.js
chrome.runtime.sendMessage({"action" : "getTxns"}, function(txns) {
// Process the data
});
// background.js
// Register this before opening the tab
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if(request.action == "getTxns") {
sendResponse(txnDataJSON); // No need to serialize yourself!
}
});

How should I handle Chrome tabs that don't yet have a content script loaded?

I'm trying to extend some of the handling of messaging between my background process and content scripts. In the normal course of things my background process sends a message via postMessage() and the content script replies via another channel with a response. However I would like to now extend the background process to fall-back to something else if the content script can't find a valid thing on the page. It's when looking at this I discovered a problem when sending a message to blank or system pages. As the tabs don't have content scripts loaded there is nothing to receive the posted message. This generates warnings in the console logs but otherwise no ill effects. However:
// Called when the user clicks on the browser action.
//
// When clicked we send a message to the current active tab's
// content script. It will then use heuristics to decide which text
// area to spawn an edit request for.
chrome.browserAction.onClicked.addListener(function(tab) {
var find_msg = {
msg: "find_edit"
};
try {
// sometimes there is no tab to talk to
var tab_port = chrome.tabs.connect(tab.id);
tab_port.postMessage(find_msg);
updateUserFeedback("sent request to content script", "green");
} catch (err) {
if (settings.get("enable_foreground")) {
handleForegroundMessage(msg);
} else {
updateUserFeedback("no text area listener on this page", "red");
}
}
});
Doesn't work. I would expect the connect or the postMessage to throw an error I can trap, however the console log is filled with error messages including:
Port: Could not establish connection. Receiving end does not exist.
But I do not end up in the catch statement.
In the end I couldn't do it with connect, I had to use the one shot sendMessage() which has a call-back function when the response comes in. That can then be interrogated for success and the state of lastError. The code now looks like this:
// Called when the user clicks on the browser action.
//
// When clicked we send a message to the current active tab's
// content script. It will then use heuristics to decide which text
// area to spawn an edit request for.
chrome.browserAction.onClicked.addListener(function(tab) {
var find_msg = {
msg: "find_edit"
};
// sometimes there is no content script to talk to which we need to detect
console.log("sending find_edit message");
chrome.tabs.sendMessage(tab.id, find_msg, function(response) {
console.log("sendMessage: "+response);
if (chrome.runtime.lastError && settings.get("enable_foreground")) {
handleForegroundMessage();
} else {
updateUserFeedback("sent request to content script", "green");
}
});
});

Sending a message from background.js to a content script

I have a background.js with following snippet:
chrome.browserAction.onClicked.addListener(toggleStatus);
Now inside toggleStatus I'd like to send a message to my content script, that something has happened. Is that possible?
Or am I completely missing the point, is this the right way to go?
Behind this is that I'd like to activate/deactive my extension, without the need of reloading the page.
After some more trial & error, I finally found a solution which satisfied me enough.
First add an event listener to the content script:
var extensionPort = chrome.extension.connect();
extensionPort.onMessage(function(msg) {
// handle the received message
});
To send messages to every connected port of the extension add this to the background.js:
chrome.extension.onConnect.addListener(function(port) {
port.postMessage({ someMessage: 'take me to the foobar' });
});

Categories