I am creating a Chrome Extension. When I try to get the tabId and tabIndex, they are both showing as "undefined".
here is the background.js:
chrome.extension.onRequest.addListener(
function (request, sender)
{
if (request.command == "selected-tab")
{
chrome.tabs.getSelected(null,
function()
{
// both show as undefined
alert('sender.tabId: ' + sender.tabId);
alert('sender.tabIndex' + sender.tabIndex);
});
}
}
);
Here is the content-script.js:
chrome.extension.sendRequest({ command: "selected-tab", urltext: urlText });
Here is the manifest.json:
{
"manifest_version": 2,
"name": "test2",
"description": "Test2 desc",
"version": "1.0",
"permissions": [
"tabs", "http://*/*", "https://*/*","contextMenus"
],
"background": {
"scripts": ["jquery-1.11.0.min.js", "background.js"]
},
"content_scripts": [
{
"matches": ["http://*/*"],
"js": ["jquery-1.11.0.min.js", "content-script.js"]
}
]
}
How can I get the tabId and tabIndex of the current tab inside background.js?
Thanks Before Hand
Update #1
Tried this inside background.js and it still did not show the tab id on the alert:
chrome.tabs.getCurrent(function (tab) {
alert(tab.id);
});
The chrome.extension.onRequest event supports the concept of a response back to the sender, but if the receiver doesn't reply (or indicate that it intends to) there is some code that tries to garbage collect the JS context setup inside your event handler. Because you're starting another asynchronous operation via chrome.tabs.getSelected inside the context of your handler, it might be that the garbage collection kicks in before your callback for getSelected fires.
See the documentaiton on the callback parameter to runtime.onMessage:
"Function to call (at most once) when you have a response. The
argument should be any JSON-ifiable object. If you have more than one
onMessage listener in the same document, then only one may send a
response. This function becomes invalid when the event listener
returns, unless you return true from the event listener to indicate
you wish to send a response asynchronously (this will keep the message
channel open to the other end until sendResponse is called)."
One easy fix to try would be to manually call sendResponse inside your getSelected callback:
chrome.extension.onRequest.addListener(
function (request, sender)
{
if (request.command == "selected-tab")
{
chrome.tabs.getSelected(null,
function()
{
// both show as undefined
alert('sender.tabId: ' + sender.tabId);
alert('sender.tabIndex' + sender.tabIndex);
sendResponse(); // context can now be GC'd
});
}
return true; // indicates we plan to call sendResponse
}
);
Related
Forgive me for any glaring mistakes as I am new to chrome extensions, but this error with Chrome's message passing API has been discussed here, here, and here in the past and the common response is along the lines of 'disable existing Chrome extensions, one of them is causing the error'. Is this the best that can be accomplished? Are we supposed to just roll over and accept the fact that our extensions will conflict with others? Returning true or returning a Promise for the listener callback function and using sendResponse does not solve the problem for me.
Currently, I can only get the new value stored in chrome.storage.local (no errors) by disabling all other chrome extensions, removing the extension and loading back up the unpacked extension. The code interestingly only seems to work on developer.chrome.com, it doesn't work at all on the other "matches" URLs in manifest.json.
I think that there is some significance in the await and async operators in solving this issue but I am unsure how to properly implement it.
manifest.json:
{
"manifest_version": 2,
"name": "my extension",
"version": "1.0",
"description": "its my extension",
"permissions": [
"declarativeContent",
"storage",
"activeTab"
],
"content_scripts": [
{
"matches": [
"*://developer.chrome.com/*",
"*://bbc.co.uk/*",
"*://theguardian.com/*",
"*://dailymail.co.uk/*"
],
"js": ["content.js"]
}
],
"background": {
"scripts": ["background.js"],
"persistent": false
},
"content_security_policy": "script-src 'self' https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js; object-src 'self'",
"page_action": {
"default_popup": "popup.html"
},
"icons": {
"16": "images/icon16.png",
"32": "images/icon32.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
}
}
popup.html:
<!DOCTYPE html>
<html>
<head>
<title>my extension</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="popup.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<h1>my extension</h1>
<h2>Article: <span id="article-headline"></span></h2>
<button id="detect-article">Detect Article</button>
</body>
</html>
popup.js:
$(document).ready(function() {
$("#detect-article").click(function() {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs){
chrome.tabs.sendMessage(tabs[0].id, {request: "Requesting headline"}, function(response) {
console.log("Requesting headline")
});
});
});
})
function getHeadline(changes) {
let changedValues = Object.keys(changes);
//console.log(changedValues);
for (var item of changedValues) {
console.log("new value: " + changes[item].newValue);
$("#article-headline").text(changes[item].newValue)
}
}
chrome.storage.onChanged.addListener(getHeadline);
content.js:
function handleRequest(message, sender, sendResponse) {
console.log("Request recieved");
let headlineList = document.getElementsByTagName("h1");
chrome.storage.local.set({headline: headlineList[0].innerText}, function() {
console.log("'" + headlineList[0].innerText + "' stored in local storage");
});
return true;
}
chrome.runtime.onMessage.addListener(handleRequest);
background.js:
chrome.runtime.onInstalled.addListener(function() {
chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
chrome.declarativeContent.onPageChanged.addRules([{
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostContains: 'developer.chrome.com' },
}),
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostContains: 'bbc.co.uk' },
}),
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostContains: 'theguardian.com' },
}),
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostContains: 'dailymail.co.uk' },
}),
],
actions: [new chrome.declarativeContent.ShowPageAction()]
}]);
});
});
Many thanks for taking the time to look/re-look at this issue, solutions pertaining to the aforementioned 'disable existing extensions' are not what I am looking for.
When you specify a callback for sendMessage you're telling the API that you NEED a response so when your content script doesn't respond using sendResponse the API thinks something terrible happened and reports it as such!
Reminder: when editing content scripts make sure to reload both the extension on chrome://extensions page and the tabs that should have this content script.
If you need a response from asynchronously running code such as chrome API callback:
Keep return true
Call sendResponse(someImportantData) inside the callback
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
chrome.storage.local.set({foo: 'bar'}, () => {
sendResponse('whatever');
});
return true;
});
Same for Promise, but don't use async for the onMessage listener, more info.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
fetch(message.url).then(r => r.text())
.then(t => sendResponse({ok: t}))
.catch(e => sendResponse({err: e.message}));
return true;
});
If you need a response and it can be sent immediately:
Replace return true with sendResponse
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse('whatever');
});
If you don't need any response:
Remove the callback in sendMessage
chrome.tabs.sendMessage(tabs[0].id, {request: "Requesting headline"});
Remove return true - all it does currently is telling the API to keep the messaging port open indefinitely, which will never be used by you, so it's just a memory leak source.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// do something
// don't return true
// ManifestV2: don't call sendResponse
// ManifestV3 bug: uncomment the next line
// sendResponse();
});
For ManifestV3 in Chrome 99, 100, 101 you need a dummy sendResponse() call.
Forgive me for any glaring mistakes as I am new to chrome extensions, but this error with Chrome's message passing API has been discussed here, here, and here in the past and the common response is along the lines of 'disable existing Chrome extensions, one of them is causing the error'. Is this the best that can be accomplished? Are we supposed to just roll over and accept the fact that our extensions will conflict with others? Returning true or returning a Promise for the listener callback function and using sendResponse does not solve the problem for me.
Currently, I can only get the new value stored in chrome.storage.local (no errors) by disabling all other chrome extensions, removing the extension and loading back up the unpacked extension. The code interestingly only seems to work on developer.chrome.com, it doesn't work at all on the other "matches" URLs in manifest.json.
I think that there is some significance in the await and async operators in solving this issue but I am unsure how to properly implement it.
manifest.json:
{
"manifest_version": 2,
"name": "my extension",
"version": "1.0",
"description": "its my extension",
"permissions": [
"declarativeContent",
"storage",
"activeTab"
],
"content_scripts": [
{
"matches": [
"*://developer.chrome.com/*",
"*://bbc.co.uk/*",
"*://theguardian.com/*",
"*://dailymail.co.uk/*"
],
"js": ["content.js"]
}
],
"background": {
"scripts": ["background.js"],
"persistent": false
},
"content_security_policy": "script-src 'self' https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js; object-src 'self'",
"page_action": {
"default_popup": "popup.html"
},
"icons": {
"16": "images/icon16.png",
"32": "images/icon32.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
}
}
popup.html:
<!DOCTYPE html>
<html>
<head>
<title>my extension</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="popup.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<h1>my extension</h1>
<h2>Article: <span id="article-headline"></span></h2>
<button id="detect-article">Detect Article</button>
</body>
</html>
popup.js:
$(document).ready(function() {
$("#detect-article").click(function() {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs){
chrome.tabs.sendMessage(tabs[0].id, {request: "Requesting headline"}, function(response) {
console.log("Requesting headline")
});
});
});
})
function getHeadline(changes) {
let changedValues = Object.keys(changes);
//console.log(changedValues);
for (var item of changedValues) {
console.log("new value: " + changes[item].newValue);
$("#article-headline").text(changes[item].newValue)
}
}
chrome.storage.onChanged.addListener(getHeadline);
content.js:
function handleRequest(message, sender, sendResponse) {
console.log("Request recieved");
let headlineList = document.getElementsByTagName("h1");
chrome.storage.local.set({headline: headlineList[0].innerText}, function() {
console.log("'" + headlineList[0].innerText + "' stored in local storage");
});
return true;
}
chrome.runtime.onMessage.addListener(handleRequest);
background.js:
chrome.runtime.onInstalled.addListener(function() {
chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
chrome.declarativeContent.onPageChanged.addRules([{
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostContains: 'developer.chrome.com' },
}),
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostContains: 'bbc.co.uk' },
}),
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostContains: 'theguardian.com' },
}),
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostContains: 'dailymail.co.uk' },
}),
],
actions: [new chrome.declarativeContent.ShowPageAction()]
}]);
});
});
Many thanks for taking the time to look/re-look at this issue, solutions pertaining to the aforementioned 'disable existing extensions' are not what I am looking for.
When you specify a callback for sendMessage you're telling the API that you NEED a response so when your content script doesn't respond using sendResponse the API thinks something terrible happened and reports it as such!
Reminder: when editing content scripts make sure to reload both the extension on chrome://extensions page and the tabs that should have this content script.
If you need a response from asynchronously running code such as chrome API callback:
Keep return true
Call sendResponse(someImportantData) inside the callback
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
chrome.storage.local.set({foo: 'bar'}, () => {
sendResponse('whatever');
});
return true;
});
Same for Promise, but don't use async for the onMessage listener, more info.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
fetch(message.url).then(r => r.text())
.then(t => sendResponse({ok: t}))
.catch(e => sendResponse({err: e.message}));
return true;
});
If you need a response and it can be sent immediately:
Replace return true with sendResponse
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse('whatever');
});
If you don't need any response:
Remove the callback in sendMessage
chrome.tabs.sendMessage(tabs[0].id, {request: "Requesting headline"});
Remove return true - all it does currently is telling the API to keep the messaging port open indefinitely, which will never be used by you, so it's just a memory leak source.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// do something
// don't return true
// ManifestV2: don't call sendResponse
// ManifestV3 bug: uncomment the next line
// sendResponse();
});
For ManifestV3 in Chrome 99, 100, 101 you need a dummy sendResponse() call.
I want to listen to all the requests going from a page(including from the iframes that it might have) and get metrics similar to resource timing api for all of the requests. I want to be able to do this from a background script.
I tried injecting a script tag directly which will listen to dom change events and then use that data. but see 2 problem there
1. it doesn't get all the dom change events of iframes.
2. I'm unable to locate the additional calls that are made from the iframes.
{
"manifest_version": 2,
"name": "somwnamw",
"version": "1.0",
"author": "Me",
"description": "Some desc",
"icons": {
"48": "icons/border-48.png"
},
"content_scripts": [
{
"matches": ["*://*.mydomain.com/*"],
"js": ["trackIt.js"],
"run_at": "document_start"
}
],
"background": {
"scripts": ["background.js"]
},
"permissions": [
"webRequest",
"<all_urls>"
],
"web_accessible_resources": ["pa.js"]
}
My content script has something like this
s.addEventListener('load', function(w) {
});
(document.body || document.documentElement).appendChild(s);
And the script I'm injecting into the dom looks like this
var observeDOM = (function(){
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver,
eventListenerSupported = window.addEventListener;
return function(obj, callback){
if( MutationObserver ){
// define a new observer
var obs = new MutationObserver(function(mutations, observer){
if( mutations[0].addedNodes.length || mutations[0].removedNodes.length )
callback();
});
// have the observer observe foo for changes in children
obs.observe( obj, { childList:true, subtree:true });
}
else if( eventListenerSupported ){
obj.addEventListener('DOMNodeInserted', callback, false);
obj.addEventListener('DOMNodeRemoved', callback, false);
obj.addEventListener('DOMAttrModified', callback, false);
obj.addEventListener('DOMElementNameChanged', callback, false);
obj.addEventListener('DOMNodeInsertedIntoDocument', callback, false);
obj.addEventListener('DOMNodeRemovedFromDocument', callback, false);
obj.addEventListener('DOMSubtreeModified', callback, false);
}
};
})();
// Observe a specific DOM element:
observeDOM( document.documentElement,function(data){
let performanceData = performance.getEntries();
console.log(performanceData);
aggregateDataForFPTI(performanceData.slice(perfDataLength, performanceData.length));
perfDataLength = performanceData.length;
});
But my above code doesn't detect dom changes and I'm also unable to get the calls made from the iframes. I do see those requests in chrome developer tools though. here my iframes are from the same domain.
So I want to instead switch to background script and then listen to all the calls happening from the page and get the resource's timings.
when i change the window.location.href,the executeScript didn't work.why is this?
manifest.json is
{
"name": "Page Redder",
"description": "Make the current page red",
"version": "2.0",
"permissions": [
"activeTab","*://*/*"
],
"browser_action": {
"default_title": "Make this page red"
},
"background": {
"scripts": ["jquery-1.11.1.js","background.js"]
},
"manifest_version": 2
}
background.js is
chrome.browserAction.onClicked.addListener(function(tab) {
chrome.tabs.executeScript(tab.id,{code:'window.location.href="http://www.google.com"'},function(){
chrome.tabs.executeScript(tab.id, {file:"test.js"}, function() {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError.message);
}
});
});
});
test.js is
alert("hello work")
The problem is that the file in the second executeScript is being injected after the code of the first executeScript has been executed, but before the window.location.href call has finished. See for yourself: add a breakpoint on the line calling the second chrome.tabs.executeScript, click your browser action and then wait for the page to load before resuming - the popup will work.
One way to solve this is to add a tabs.onUpdated listener. Then, when you click your browser action store what the tabId was. Inside the tabs.onUpdated listener you can execute test.js if the updated tabId matches the tabId set in your browser action. Quick example:
var activeTabId;
chrome.browserAction.onClicked.addListener(function(tab) {
chrome.tabs.executeScript(tab.id, {code: 'window.location.href="http://www.google.com"'}, function (){
activeTabId = tab.id;
});
});
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if(tabId === activeTabId) {
activeTabId = null; //To prevent executing the script multiple times
chrome.tabs.executeScript(tabId, {file:"test.js"});
}
});
I'm getting the last visited url instead of the current one. (when i go to site.com and after that to site2.com, the url I get is 'site.com' and after I refresh site2.com I'm getting the right one.
Based on the answers here:
Google Chrome Extension get page information
Display current URL in a chrome extension
I've come up with this code:
manifest.json
{
"manifest_version": 2,
"browser_action": {
"default_popup": "action.html"
},
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"],
"run_at": "document_end"
}
],
"background": {
"scripts": ["background.js"]
},
"permissions": [
"tabs",
"unlimitedStorage",
]
}
content.js
chrome.extension.sendRequest({method: "getUrl"}, function(response) {
console.log(response.data);
});
background.js
chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
if(request.method == "getUrl") {
chrome.tabs.query({ currentWindow: true, active: true }, function (tabs) {
currentUrl = tabs[0].url;
});
sendResponse({data: currentUrl});
}
else
sendResponse({}); // snub them.
});
I've also tried to put this code below directly in content.js and I'm getting an error and in background.js and the url is set to chrome://extensions/.
chrome.tabs.query({ currentWindow: true, active: true }, function (tabs) {
currentUrl = tabs[0].url;
});
what is the right way to do this ?
First of all:
chrome.extension.sendRequest/onRequest are deprecated. Please, use chrome.runtime.sendMessage/onMessage instead.
Also, whenever possible, prefer event pages over background pages. Most of the time it takes next to nothing to convert a background page to an event page, but can save consideably on resources.
The problem:
chrome.tabs.query is asynchronous. It means that, when called, it initializes the request and then completes its execution so the next part of your script gets executed. Once the request has completed, the registered callback function is executed. So, this is what happens in your case:
You visit http://www.site1.com.
chrome.tabs.query({...}, callback) gets executed (notice the callback has not been executed yet).
sendResponse({data: currentUrl}) gets executed (note that at this point currentUrl is not defined).
chrome.tabs.query's request completes and the callback function is executed, setting currentUrl = "http://www.site1.com" (but only after it is too late).
You visit http://www.site2.com.
chrome.tabs.query({...}, callback) gets executed (notice the callback has not been executed yet).
sendResponse({data: currentUrl}) gets executed (note that at this point currentUrl still equals http://www.site1.com from step 4).
chrome.tabs.query's request completes and the callback function is executed, setting currentUrl = "http://www.site2.com" (again only after it is too late).
The solutions:
(A)
Move the logic inside the callback.
(Note: Whenever the callback (i.e. sendResponse) is going be called asynchronously, it is necessary for the onMessage listener to return true.)
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if(request.method && (request.method == "getURL")) {
if (sendResponse) {
chrome.tabs.query({
currentWindow: true,
active: true
}, function(tabs) {
if (tabs.length > 0) {
sendResponse({ data: tabs[0].url });
}
});
/* Since 'sendResponse' is to going be called
* asynchronously, it is necessary to return 'true' */
return true;
}
} else {
// Do not talk to strangers !!!
}
});
(B)
Even simpler in your case, the sender parameter of the onMessage listener, contains the field tab which contains the URL info. E.g.:
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if(request.method && (request.method == "getURL") && sendResponse) {
sendResponse({ data: sender.tab.url });
}
});
(C)
If you only need the URL in the context of the content script, why don't you use location.href from within the content script ?