passing data from executed script to background in chrome extension - javascript

I have this idea for passing data form an injected script (getDOM.js) to my background.js
bacgkround.js
chrome.contextMenus.onClicked.addListener(function(info, tab){
chrome.tabs.executeScript(tab.id, {file: "getDOM.js"})
});
chrome.contextMenus.onClicked.addListener(function(info, tab){
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {greeting: "GetURL"}, function(response) {
alert(response.navURL);
});
});
});
getDOM.js
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.greeting === "GetURL")
sendResponse({navURL:'test'});
});
as you can see i used massage function to pass data, but there is a problem. i cant get data on right time, i will pass background.js previous data
content must be dynamic (not specific "test"), every time it will alert previous data. imagine getDOM.js will pass selected text, with this code it will pas previous selected text. how can i fix this ?
my example of dynamic data :
function getHTMLOfSelection () {
var range;
if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
return range.htmlText;
}
else if (window.getSelection) {
var selection = window.getSelection();
if (selection.rangeCount > 0) {
range = selection.getRangeAt(0);
var clonedSelection = range.cloneContents();
var div = document.createElement('div');
div.appendChild(clonedSelection);
return div.innerHTML;
}
else {
return '';
}
}
else {
return '';
}
}
var dom = getHTMLOfSelection();
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.greeting === "getDom")
sendResponse({DOM:dom});
});
it will pass selected dom to background.js

Lots of problems here. Let's see.
It does not make sense to execute getHTMLOfSelection() at the point when the script is injected. You probably should put that inside the message handler: to get the selection when asked to.
A much bigger problem is the fact that every time you inject a script. This, together with 1, leads to all kinds of fun. Let's look in more detail!
The user triggers the context menu.
Your first contextMenu.onClicked handler runs. The script injection is scheduled. It's asynchronous, so you don't know when it will finish unless you use a callback. Which you don't.
Your second contextMenu.onClicked handler runs. It sends a message to a script which potentially haven't finished executing. If you're lucky, you get a response.
User triggers the context menu again on the same page.
Your first contextMenu.onClicked handler runs, again. The script is going to be injected again, creating a second listener for the message that will compete with the first. It's again asynchronous, so maybe by the time your message arrives dom is up to date. Maybe not.
Your second contextMenu.onClicked handler runs, again. This time there sure is a message listener (maybe two!) that returns maybe up to date data.
And so on. You see the problem?
Furthermore, you can't pass a DOM object with sendResponse. The object needs to be JSON-serializable, and DOM objects contain circular references, which is a no-no. You need to extract the data you need on the content script side, and pass only that.
So, let's try to tackle those problems.
There are 2 ways of dealing with this. I'll present both, pick the one you prefer. Both will take care of problems 1 and 2.
First way is to ensure your message handler is only added once by including some kind of guard variable:
// getDOM.js
if(!getDOM_ready) { // This will be undefined the first time around
getDOM_ready = true;
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
// If only you knew how I hate this "greeting" example copied over
if (request.command === "GetURL") {
var dom = getHTMLOfSelection();
sendResponse({DOM: dom});
}
}
);
}
function getHTMLOfSelection() {
/* ... */
}
Then on the background side, we need to be sure we send a message only after the script finishes executing:
chrome.contextMenus.onClicked.addListener(function(info, tab){
chrome.tabs.executeScript(tab.id, {file: "getDOM.js"}, function() {
// This only executes after the content script runs
// Oh, and you most certainly don't need to query for tabs,
// you already have a tab and its id in `onClicked`
chrome.tabs.sendMessage(tab.id, {command: "GetURL"}, function(response) {
alert(response.navURL);
});
});
The second way is to drop Messaging altogether. executeScript actually returns the last value the content script evaluated. That makes the content script trivial and does not leave a message listener behind:
// getDOM.js
function getHTMLOfSelection() {
/* ... */
}
getHTMLOfSelection(); // Yes, that's it
On the background side, you need to adapt the listener:
chrome.contextMenus.onClicked.addListener(function(info, tab){
chrome.tabs.executeScript(tab.id, {file: "getDOM.js"}, function(results) {
// results is an array, because it can be executed in more than one frame
alert(results[0]);
});
The code is much simpler here, AND it does not leave an active event listener behind.
As for problem 3, you need to extract the info you need (say, a link) from the selection and pass only that instead of the object.
Finally, this is not a complete solution. Problem is, your selection can be inside an iframe, and this solution only injects code into the top frame. Solution to that is left as an exercise to the reader; using all_frames: true in the content script options will inject into all frames, and one of them will have a non-empty selection. You just need to see which.

Related

Scrape values generated by script from active tab chrome extension content script

I'm trying to scrape values from a page that have been generated by a JS script. When I inspect the page, i see the values there, but my selectors return null/undefined.
The purpose of the extension is to allow people, on click of a button, to scrape their personalised data from a page that requires login WITHOUT having to provide any login details to the extension.
In chrome-console, the static "title" values return, so i'm pretty sure my selectors are fine and it's just that accessing the document doesn't count for the executed scripts.
From reading, I might need to use something like pupeteer or selenium, but it seems they fire up their own browser instance (bad, as I'd need to take user login details to mock the sign in process) or i'd need to modify how the chrome browser starts with --remote-debugging-port=A_PORT_NUMBER which i want to avoid.
From chrome console and my extension, I can retrieve the values highlighted green, (so it is not an issue with iframes as some posts suggest) and can't retrieve values highlighted red.
HTML structure in image
From popup.html
document.addEventListener("DOMContentLoaded", function () {
...
document.querySelector('button[id="scrape"]').addEventListener("click", function onclick() {
chrome.tabs.query({ currentWindow: true, active: true },
function (activeTab) {
chrome.tabs.sendMessage(activeTab[0].id, { action: "putSource_scrapeSalePage", index: activeTab[0].index })
}
)
})
...
}, false)
From content.js
//Need to import pupeteer/selenium here? How else to use it for active tab?
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse) {
...
else if (request.action === "putSource_scrapeSalePage") {
let htmlvar = $(document)
console.log(htmlvar);
let test = $('td[desc= "transactionType"]').text().trim() //returns fine
let tableData1raw = $('table.tableDataOne tbody tr').find("tbody").find("tr")
let tableData1raw_almost = $(tableData1raw).each(function (i, element) {
console.log(element)
const $element = $(element).find("td")
console.log($element)
...
The Question:
If there is no better way to do this, how can I do this from content-script with something like pupeteer?
In the end I was able to use the Value i know i COULD get ("transaction type" title) and use it to traverse to it's sibling element (+) and retrieve whatever Div was there, instead of trying to target the Div class directly.
$('td[desc= "transactionType"] + td').find("div").text();

How can all the individual executions of a content.js on different frames of the same page communicate with each other?

So, I'm building an extension that autofills different types of forms. As it's not apparent from the url which form is used on a particular website, I need to match all the urls in my manifest. I'm trying to detect the form by the 'src'-attribute in the web page.
Some of the fields of a certain form are not in the first frame. So "all_frames" has to be true in my manifest. That means content.js fires once for each frame or iFrame.
**content.js:**
async function checkForPaymentType(value, attribute = 'src') {
return document.querySelectorAll(`[${attribute}*="${value}"]`);
}
let hasThisForm;
document.addEventListener('DOMContentLoaded', () => {
checkForPaymentType('formJs.js').then((value) => {
if(value.length) {
hasThisForm = true;
}
if(hasThisForm)
fillForm();
});
});
The problem now is, that that only the first frame has the "src='formJs.js" attribute in one of its elements. So it only fills out those fields in the first frame.
My solution idea
What I am trying to do is some sort of global boolean variable ('hasThisForm') that can only be set true. So once the first frame detected that there is this form on the website the other frames fire fillForm() as well.
Problems
1.I'm not able to set a variable that can be read from all of the executions.
2.I need the other executions of content.js to wait for the first one.
Another solution would be to have some sort of window.querySelectorAll, so every execution of content.js searches in the whole page and not just in its frame.
Thanks in advance:)
So I figured it out.
As pointed out in the comment from #wOxxOm or in this question (How to get all frames to access one single global variable) you need to manage everything via the background page.
You want to set up a listener in every Frame and send a message only from the top frame (to the background page which sends it back to the whole tab).
After hours of trying I noticed that the listeners weren't even ready when the message from the topFrame was sent. I put in a sleeper before sending the message which is not the ideal way I guess. But there is no "listenerIsSet-Event".
This is my code:
content.js
document.addEventListener('DOMContentLoaded', () => {
chrome.runtime.onMessage.addListener(
function (msgFromTopFrame) {
console.log(msgFromTopFrame)
});
if (window === top) {
Sleep(1000).then(() => {
const msgToOtherFrames = {'greeting': 'hello'};
chrome.runtime.sendMessage(msgToOtherFrames);
});
}
});
background.js
chrome.runtime.onMessage.addListener((msg, sender) => {
if(('greeting' in msg)) {
chrome.tabs.sendMessage(sender.tab.id, msg);
}
});
You probably want to execute some code depending on the value received. You can write it only once in the listener. It will execute in all frames including the top frame as the background.js sends it to all frames in the tab.
Note:
There may be some errors with the dicts/keys in the messages "sent around" in this code. Just log the message in all the listeners to get the right expressions.

embed _generated_background_page to options page

I made Chrome extension which working fine, but need to be modified this way:
console.log of background.js should be monitored in real time by user constantly, while options page opened, as an element of this page. At the worst case, it may be duplicate of log, but generated in background.js.
How can I access and display this data, where background script process external data once a second?
You can't embed the background page in another page.
To do what you are looking to do, you can override console.log to do extra stuff:
// background.js
(function() {
if(window.console && console.log) {
var old = console.log;
console.log = function() {
// Do extra stuff here
chrome.runtime.sendMessage({
type: "backgroundLogEvent",
content: arguments
});
// Call normal console.log
old.apply(this, arguments);
}
}
})();
And in the options page, you can receive those messages:
// options.js
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if(message.type == "backgroundLogEvent") {
// Do something with message.arguments
// I.e. log them here:
message.arguments.unshift('From background:');
console.log.apply(console, message.arguments);
}
});
This will transmit all console.log messages from background in realtime.
Exercise for the reader: implement a buffer that shows a short history before opening the options page.

Chrome Extension: Message Passing (Sending the DOM to popup.js) returns 'null'

I would like to use a Chrome Extension to download the current page's DOM. I'm not sure why, but when my download occurs, the result is just a text file with either 'null' or 'undefined', rather than the DOM. I've tried to assimilate the knowledge from here and here, but I can't seem to get the message from content.js through to popup.js.
Additionally, I'm not sure why this actually works. When I read the docs, it seems like I need to send the message from popup.js to content.js by selecting the active tab:
chrome.tabs.query({currentWindow: true, active: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {message: 'getMessageFromContent'}, function(response) {
//Code to handle response from content.js
}
});
My current code:
content.js
var page_html = DOMtoString(document);
chrome.runtime.sendMessage({method: 'downloadPageDOM', pageDOM: thisPage});
function DOMtoString(document_root) { ... }
background.js
chrome.tabs.query({currentWindow: true, active: true}, function(tab) {
var page_html;
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.message == 'downloadPageDOM')
page_html = request.pageDOM;
else if (request.message == 'getPageDOM')
sendResponse(page_html);
});
});
popup.js
document.addEventListener('DOMContentLoaded', function() {
var download_button = document.getElementById('download_button');
download_button.addEventListener('click', function() {
chrome.runtime.sendMessage({message:'getPageDOM'}, function(response) {
download(response, "download.html", "text/html");
});
});
});
function download(data, fileName, mimeType) { ... }
I feel like I'm missing a crucial understanding of how message passing works. If anyone could take a second to help me understand why the file that downloads just has 'null', I would sincerely appreciate it.
You're over-complicating this, which leads to a lot of logical errors.
You've set up the background page to act like a message proxy, and the content script itself triggers updating your page_html variable. Then the popup pulls that data with another message.
Note that page_html will not contain the current tab's data in any case: you're overwriting this data with the last loaded tab.
What you can do is completely cut out the middleman (i.e. background.js). I guess you got confused by the fact that sending a message TO a popup is a generally a bad idea (no guarantee it's open), but the other way around is usually safe (and you can make it always safe).
Solution 1 (bad, but here for educational purposes)
The logic of your app is: once the user clicks the button, make the snapshot at that moment. So, instead of making your content script do its work immediately, add a message listener:
// content.js
chrome.runtime.onMessage(function(message, sender, sendResponse) {
else if (request.message == 'getPageDOM')
sendResponse(DOMtoString(document));
});
function DOMtoString(document_root) { ... }
And in your popup, request it:
// popup.js
// (Inside the click listener)
chrome.tabs.query({currentWindow: true, active: true}, function(tabs) {
// Note that sending a message to a content script is different
chrome.tabs.sendMessage(tabs[0].id, {message:'getPageDOM'}, function(response) {
download(response, "download.html", "text/html");
});
});
However, this solution is not 100% robust. It will fail if the content script is not injected into the page (and this can happen). But it's possible to fix this.
Solution 2
Let's not assume the content script is injected. In fact, most of the time you don't NEED to inject it automatically, only when the user clicks your button.
So, remove the content script from the manifest, make sure you have host permissions ("<all_urls>" works well, though consider activeTab permission), and the use programmatic injection.
There is a little-used form of programmatic injection that collects the value of the last executed statement. We're going to use that.
// content.js
DOMtoString(document); // This will be the last executed statement
function DOMtoString(document_root) { ... }
In the popup, execute script, collect results:
// popup.js
// (Inside the click listener)
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.executeScript(tabs[0].id, {file: "content.js"}, function(data) {
// Data is an array of values, in case it was executed in multiple tabs/frames
download(data[0], "download.html", "text/html");
});
});
NOTE: All of the above assumes that your function DOMtoString actually works.

Chrome extension persistent popup best practices

I've understood from the docs that closing chrome extension popups when losing focus has been a design choice.
I'm working on an extension where the user chooses to save elements from a webpage. As he interacts with the main webpage I would like the popup to get updated but that's obviously not possible.
What's the proper way of handling this situation? (this is my first chrome extension)
You can have a content script detect the "save" action. Let's suppose it's a specific DOM element you know for sure it's going to be in the specific main, or that you create by yourself.
content.js
//content script
document.onreadystatechange = function () {
if (document.readyState == "complete") {
// Grab the UI frmo the mainpage you want to append the save functionality
var someElementsYouWantToAppendASaveButtonTo = document.getElementsByTagName("...");
var len = someElementsYouWantToAppendASaveButtonTo.length;
for (var i = 0; i < len; i++) {
// Create a UI save button to provide a functionality
var theSaveButton = document.createElement("button");
theSaveButton.value = "Save to Chrome Extension";
// Send data to extension when clicked
theSaveButton.addEventListener("click", function() {
var dataToSentToExtension = {...} // Retrieve from the clicked element, or whatever you want to save
chrome.extension.sendMessage(dataToSentToExtension, function(response) {
if(response.success) console.log("Saved successfully");
else console.log("There was an error while saving")
});
}, false);
someElementsYouWantToAppendASaveButtonTo[i].appendChild(theSaveButton)
}
}
}
Then, on the background, you detect the response and set up the popup as you wish.
background.js
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
if(request.dataToSave) {
chrome.storage.local.set(dataToSave, function() {...});
// You can then set upn the proper popup for the next click or even switch to it
switch(request.popupToDisplay) {
case "awesomeDisplay":
chrome.browserAction.setPopup({...})
break;
}
var responseFromExtension = {success: true}
} else {
var responseFromExtension = {error: true}
}
});
It seems you are looking to modify\update your popup.html page in accord to changes in a web page. If so, use content scripts and establish connection for single message communication with background page(Since focus is lost every time) and update popup.html indirectly.
References:
Content Scripts
Background Page
Message Passing
Reference for communication between popup and background page apart from these,
there are bunch of questions on these topics, they will get you started..

Categories

Resources