I'm trying to make a Chrome Extension that will take some of the contents of a page (The inner HTML of a <span> with id="productTitile") I then would need to take that value and put it into a field in my popup.html.
I have tried this:
document.getElementById('input_87').value = chrome.tabs.executeScript({
code: 'document.getElementById("productTitle").innerHTML'
});
But it just returns undefined into the field. I then ran document.getElementById("productTitle").innerHTML in the console in the parent page, and it gave me the expected value, but when I ran the whole code in console of the popup extension, it again returned undefined.
What am I doing wrong?
First off, as Haibara Ai says, chrome.tabs.executeScript is asynchronous - it doesn't return anything (and doesn't do anything immediately).
A good source on this in general is this question: How do I return the response from an asynchronous call? (spoiler: you can't)
If you look at the docs (which, by the way, should be your first unconditional reflex), you'll see that it has a callback and if you read the above question you'll understand it's your only option. However, there are 2 additional complications:
The callback gets an array of results. That happens because executeScript can, optionally (with allFrames: true or frameId specified), run in subframes. So you'll need to use the first element of the results array:
chrome.tabs.executeScript({
code: 'document.getElementById("productTitle").innerHTML'
}, function(results) {
document.getElementById('input_87').value = results[0];
});
The call to executeScript can fail - for example, when the page is not scriptable regardless of permissions, such as the Chrome Web Store. It's wise to check that you actually got the result before using it:
chrome.tabs.executeScript({
code: 'document.getElementById("productTitle").innerHTML'
}, function(results) {
if (chrome.runtime.lastError) {
// Couldn't execute the script at all
} else if (typeof results[0] === "undefined") {
// Couldn't find what we wanted
} else {
// Everything is fine
document.getElementById('input_87').value = results[0];
}
});
Related
After struggling for hours with a weird bug in my actual Chrome extension, I finally managed to nail down the following MCVE:
background.js
chrome.runtime.onMessage.addListener(function(request) {
if (typeof request.hello !== "undefined") {
console.log("I got it!");
}
});
options.js
// the following gives an error
// chrome.runtime.sendMessage({ hello: true }, function(response) {
// if (chrome.runtime.lastError) {
// console.log(chrome.runtime.lastError);
// }
// });
// the following does not
chrome.runtime.sendMessage({ hello: true });
As you can probably tell from the comments above, when I add an extra callback function to check for a runtime last error, it gives an error:
The message port closed before a response was received.
However, when I do not add the callback function, no error is generated!
As as as I can tell from the docs, this is the correct format for my callback function:
If you specify the responseCallback parameter, it should be a function that looks like this: function(any response) {...};
Thus, I am failing to understand this inconsistent behaviour. I checked similar questions, and adding return true to the background.js listener simply delays the occurrence of the error in the first case.
Here's a zip of the above files to test locally. I am using Chrome 75.
Update: I do not intend to send any response from my background js file. It is just a one time message from my options page to the background page.
It doesn't, well at least not in my Chrome 75.
The Error Message you see is because you don't answer from your background script, but it's not because you checked for runtime.lastError.
If you want to avoid this error, you need to answer from you background script;
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if(sender.tab && // check it's actually a tab that talked to us
typeof request.hello !== "undefined"
) {
sendResponse({received: true}); // or whatever you like
}
});
I recently updated Chrome to version 55.0.2883.75.
I am using a self developed Chrome Plugin to parse my HTML files wherein I use chrome.tabs.executescript to get data from background HTML page.
So when I execute chrome.extension.onRequest, I save the background page's parsed data to a global variable and access it in the callback function of chrome.tabs.executescript and process it.
This was working fine till I update to Version 55.0.2883.75.
How can I access the global variables in the new version ??
My Code Below :
Step 1 :
chrome.extension.onRequest.addListener(
function (request, sender, sendResponse) {
parser = new DOMParser();
htmlDoc = parser.parseFromString(request.content, "text/html");
//outputJson is a global variable which is Populated here
outputJson = parseMyPage(outputJson, htmlDoc);
});
Step 2:
chrome.tabs.getSelected(null, function (tab) {
// Now inject a script onto the page
chrome.tabs.executeScript(tab.id,{
code: "chrome.extension.sendRequest({content: document.body.innerHTML}, function(response) { console.log('success'); });"
}, function () {
//my code to access global variables
if (outputJson && null != outputJson) {
// other stuff
}
});
});
The way your code is designed, you are relying on the order in which two asynchronous blocks of code are executed: the extension.onRequest1 event and the callback for tabs.executeScript(). Your code requires that the extension.onRequest1 event fires before the tabs.executeScript() callback is executed. There is no guarantee that this will be the order in which these occur. If this is a released extension, it is quite possible that this was failing on users' machines, depending on their configuration. It is also possible that the code in Chrome, prior to Chrome 55, resulted in the event and callback always happening in the order you required.
The solution is to to rewrite this to not require any particular order for the execution of these asynchronous code blocks. Fortunately, there is a way to do that and reduce complexity at the same time.
You can transfer the information you desire from the content script to your background script directly into the callback of the tabs.executeScript(), without the need to explicitly pass a message. The value of the executed script is passed to the callback in an array containing one entry per frame in which the script was injected. This can very conveniently be used to pass data from a content script to the tabs.executeScript() callback. Obviously, you can only send back a single value per frame this way.
The following code should do what you desire. I hand edited this code from your code in this Question and my answer here. While the code in that answer is fully tested, the fact that I edited this only within this answer means that some errors may have crept in:
chrome.tabs.getSelected(null, function (tab) {
// Now inject a script onto the page
chrome.tabs.executeScript(tab.id,{
code: "document.body.innerHTML;"
}, function (results) {
parser = new DOMParser();
htmlDoc = parser.parseFromString(results[0], "text/html");
//outputJson is a global variable which is Populated here
outputJson = parseMyPage(outputJson, htmlDoc);
//my code to access global variables
if (outputJson && null != outputJson) {
// other stuff
}
});
});
extension.sendRequest() and extension.onRequest have been deprecated since Chrome 33. You should replace these anywhere you are using them with runtime.sendmessage() and runtime.onMessage.
I would like to get elements from chrome tab with a certain URL, it does not have to be active. so far I have:
Test()
function Test() {
chrome.tabs.query({url: "https://www.somewebsite.com/*"}, function(results) {
chrome.tabs.executeScript(results[0].id,{code: 'El = document.getElementsByClassName("someclass")'});
console.log(El);
})
}
Maybe it has to be done through a content script file?
I have this code placed in my background.js file. Given the proper URL and Class this function will not return the Element. Why?
Thanks for any suggestions!
The background script is executed only once when chrome launches. Instead of writing this inside a function which you call in the same file, you have to put it in some kind of event listener. This code should stay in the background file, but has to be triggered by something, not just executed on extension load.
EDIT: Oh, I see, you're trying to pass the element to the background script. You can't.
Background scripts have access to the whole chrome APIs but not to the page content. The script you write in executeScript runs on the tab you specified, so the variables are available to other code within that tab, not within your background script. Extension code that can run on the page and edit its content is either sent using executeScript or put in content scripts.
If you want to share information between various layers of an extension, you need to use messages. They work like events and listeners. Read the docs here. You'll be able to pass data like numbers and strings, but not the actual HTMLElement. So any code that manipulates the DOM has to run on the tab itself.
If your code is small and simple, you could just write it in the executeScript call instead.
You are correct, there does not appear to be a way to manipulate elements in another tab from the background.js;
Test();
function Test() {
chrome.tabs.query({
url: "https://www.google.com/*"
}, function(results) {
if (results.length < 1) { //if there is no tab open with google.com
console.log("Tab not found, creating new tab");
chrome.tabs.create({
"url": "https://www.google.com/",
"selected": true
}, null);
chrome.tabs.query({
url: "https://www.google.com/*"
}, function(results) {
if (results.length >= 1) {
console.log("Found google.com in the new tab");
}
chrome.tabs.executeScript(results[0].id, {
code: "foo = document.getElementsByTagName('input'); console.log(foo,' This is sent from the active tab');
chrome.storage.local.set({
'foo': foo[0]
});
"},function(foo) {chrome.storage.local.get("
foo ", function(foo) {console.log(foo , 'This has retrieved foo right after saving it to storage.');});});
});
}
});
}
search()
function search(result) {
chrome.storage.local.get("foo", function(result) {
console.log(result, 'This has retrieved foo from storage in a separate function within background.js');});
}
the first console log will show the html collection object for the inputs from google.com inside the google.com tab console, the other two will show blank objects in the background.js console. Thanks, I will find another solution.
I wrote a script that's running from ebay listing iframe. It's working fine, it runs on $(document).ready(), sends an AJAX request to a remote server, gets some info, manipulate the DOM on 'success' callback, everything working perfect...
However, I added a piece of code, which should get the document.referrer, and extract some keywords from it, if they exist. Specifically, if a user searches ebay for a product, and clicks on my product from the results, the function extracts the keywords he entered.
Now, the problem is, that function is not running on page load at all. It seems like it blocks the script when it comes to that part. This is the function:
function getKeywords(){
var index = window.parent.document.referrer.indexOf('_nkw');
if (index >= 0){
var currentIndex = index + 5;
var keywords = '';
while (window.parent.document.referrer[currentIndex] !== '&'){
keywords += window.parent.document.referrer[currentIndex++];
}
keywords = keywords.split('+');
return keywords;
}
}
And I tried calling two logs right after it:
console.log('referrer: ' + window.parent.document.referrer);
console.log(getKeywords());
None of them is working. It's like when it comes to that 'window.parent.document.referrer' part, it stops completely.
But, when I put this all in a console, and run it, it works perfectly. It logs the right referrer, and the right keywords.
Does anybody know what might be the issue here?
The reason it is working on the console is because your window object is the outer window reference and not your iframe. Besides that, on the console:
window.parent === window
// ==> true
So, on in fact you are running window.document.referrer and not your frame's window.parent.document.referrer.
If you want to access your frame's window object you should something like
var myFrame = document.getElementsByClassName('my-frame-class')[0];
myFrame.contentWindow === window
// ==> false
myFrame.contentWindow.parent.window === window
// ==> true
This might help you debug your problem, but I guess the browser is just preventing an inner iframe from accessing the parent's window object.
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.