Content Script and Background Communication - javascript

I am looking to create an extension for a particular site to provide additional formatting and sharing options that they don't currently have.
I am having issues getting things to communicate properly and there doesn't seem to be a clearly laid out example.
Manifest:
{
"name": "Test",
"description": "Testing.",
"version": "1.0",
"background_page": "background.html",
"permissions": [
"tabs", "http://www.sitedomain.com/*"
],
"content_scripts": [
{
"matches": ["*://*.sitedomain.com/*"],
"js": ["jquery.min.js", "test.js"],
"css": ["test.css"]
}
]
}
Content Script:
$(document).ready(function () {
alert('test js fired');
$("#ColumnContainer div.item").each(function () {
$(this).css("background-color", "skyBlue");
var itemId = $(this).children("a.itemImage").attr("href");
$(this).children(".details").append("Goto Item");
});
});
chrome.extension.onRequest.addListener(function (request, sender, sendResponse) {
alert('listener request');
alert(request);
});
JavaScript of Background HTML:
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
if (changeInfo.status == "complete") {
if (tab.url.indexOf("sitedomain.com") > -1) {
chrome.tabs.executeScript(null, {file: "test.js"});
}
}
});
chrome.tabs.sendRequest(tabId, request, responseCallback);
function responseCallback() {
alert('response callback');
}
function gotoItem(itemId) {
alert('goto Item - ' + itemId);
}
The above code does append the link and change the styling on the client page when the sitedomain.com is loaded. However, I haven't had any luck getting the gotoItem method to fire, Chrome Dev Tools shows undefined. I have tried various combinations, but just can't quite grasp the listeners and requests yet.
I would really like to see a clean sample that just shows how to call a method from each site.

I see two issues with your code. 1) the gotoItem function is defined in the background page and content_scripts can't access functions there. 2) content_scripts and javascript on pages they are injected into can not interact so your onclick can't be part of the links html.
To fix #1 is as simple as moving the gotoItem function to be in the content_script.
To fix #2 something like the following should work.
$("#ColumnContainer div.item").each(function(){
$(this).css("background-color","skyBlue");
var itemId = $(this).children("a.itemImage").attr("href");
var $link = $('Goto Item');
$link.click(function() {
gotoItem(itemId);
}
$(this).children(".details").append($link);
});
You may have to modify how itemId gets passed.

Related

Why webNavigation Listener failed for some websites?

I am trying to use chrome extension to get some data from web of science. In one step, I want to create a new tab and wait until it loaded. So I add a webNavigation Listener after creating the tab. I found the listener works well
only for some websites. If I use the target url (web of science) as the code below, I won't get the alert window. But if I use the target "https://stackoverflow.com/questions/ask", it gives the alert successfully. Why this happens? Could anyone advice the reason to me? Really thankful to it.
background.js
chrome.browserAction.onClicked.addListener(function(tab) {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
var activeTab = tabs[0];
tabId = activeTab.id;
chrome.tabs.sendMessage(tabId, {"message": "clicked_browser_action"});
});
});
var link = 'https://apps.webofknowledge.com/OneClickSearch.do?product=UA&search_mode=OneClickSearch&excludeEventConfig=ExcludeIfFromFullRecPage&SID=7ENVgUT3nRKp41VVlhe&field=AU&value=Leroux,%20E.'; // failed in this url
//var link = 'https://stackoverflow.com/questions/ask'; //success in this url
function listener1(){
chrome.webNavigation.onCompleted.removeListener(listener1);
chrome.tabs.sendMessage(tabId, {"message": "to content"});
alert('listener succeed');
}
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.joke == 'content initial'){
chrome.tabs.create({ url: link });
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
var activeTab = tabs[0];
tabId = activeTab.id;
});
//alert(link);
chrome.webNavigation.onCompleted.addListener(listener1, {url: [{urlMatches : link}]});
}
}
)
content.js
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if( request.message === "clicked_browser_action" ) {
console.log('content initial');
chrome.runtime.sendMessage({joke: 'content initial'}, function(response) {
});
}
}
)
manifest.json
{
"manifest_version": 2,
"name": "citation",
"version": "1",
"background": {
"scripts": ["background.js"],
"persistent": false
},
"browser_action": {},
"content_scripts": [{
"matches": ["<all_urls>"],
"run_at": "document_idle",
"js": ["content.js"]
}],
"permissions": [
"downloads",
"webNavigation",
"tabs",
"<all_urls>"
]
}
The main problem is that urlMatches is a regular expression in RE2 syntax as you can see in the documentation so various special symbols in the URL like ? are interpreted differently. Solution: use urlEquals or other literal string comparisons.
There are other problems:
The API is asynchronous so the tabs are created and queried later in the future in no predictable sequence. Solution: use the callback of create().
All tabs are reported in webNavigation listener, not just the active one, so theoretically there's a problem of two identical URLs being reported in different tabs. Also the API filtering parameter cannot handle URLs with #hash part Solution: remember the tab id you want to monitor in a variable and compare it in the listener, and explicitly strip #hash part in the filter.
The site may redirect the final URL of the page so it may not get reported due to your filter. Solution: specify only the host name in the filter.
The tab that sends you messages or performs navigation may be inactive. Solution: use the tab id in the listener's parameters.
chrome.browserAction.onClicked.addListener(tab => {
chrome.tabs.sendMessage(tab.id, {message: 'clicked_browser_action'});
});
var link = '...............';
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.joke === 'content initial') {
chrome.tabs.create({url: link}, tab => {
chrome.webNavigation.onCompleted.addListener(function onCompleted(info) {
if (info.tabId === tab.id && info.frameId === 0) {
chrome.webNavigation.onCompleted.removeListener(onCompleted);
chrome.tabs.sendMessage(tab.id, {message: 'to content'});
console.log('listener succeeded');
}
}, {
url: [{urlPrefix: new URL(link).origin + '/'}],
});
});
}
});
Notes:
Avoid declaring content_scripts in manifest.json for all URLs if you only need processing on demand. Use programmatic injection in such cases.
Instead of alert() use the proper debugging in devtools like breakpoints or console.log() of the background page (more info).

Chrome Extension "Receiving end does not exist." Error

I'm working on a Chrome Extension but lately I've noticed I've been getting the following error (pointing to the first line of popup.html):
Unchecked runtime.lastError: Could not establish connection. Receiving
end does not exist.
I've found a similar question here. But the error there is caused by the background property which I haven't declared on my manifest.
I'm using chrome.extension.onMessage.addListener on the contents.js script to listen for events and chrome.tabs.sendMessage on the popup.js script to send the events. Most of the time everything works fine, but sometimes I get the above error and none of the requests do anything.
The manifest.json is of the following format:
{
"manifest_version": 2,
"name": "APP_NAME",
"description": "APP_DESCRIPTION",
"version": "APP_VERSION",
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
},
"permissions": [
"activeTab",
"storage",
"clipboardRead",
"clipboardWrite"
],
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"content.js"
],
"css": [
"content.css"
]
}
]
}
Message Listener Example:
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
if (request.action === "this") console.log({
dom: doThis()
});
if (request.action === "that") sendResponse({
dom: doThat()
});
else if (request.action === "other") doOther();
else sendResponse({});
});
Message Sender Example:
function getSelectedTab() {
return new Promise(function(resolve) {
chrome.tabs.getSelected(null, resolve);
});
}
function sendRequest(data) {
data = data || {
action: undefined
};
return new Promise(function(resolve) {
getSelectedTab().then(function(tab) {
chrome.tabs.sendMessage(tab.id, data, resolve);
});
});
}
Send Request Invocation Example:
document.querySelector("#this").addEventListener("click", function() {
sendRequest({
action: "this"
}).then(function(res) {
console.log(res);
});
});
document.querySelector("#that").addEventListener("hover", function() {
sendRequest({
action: "that"
});
});
addEventListener("blur", function() {
sendRequest({
action: "other"
});
});
I'm not sure if my answer good for given case, but if you reading it, you faced this kind of problem, and probably my answer will help you.
I spent a lot of time, trying to understand why it sometimes throws this error, while I'm working at dev version, and doesn't do it for released version of my extension. Then I understood, that after every code save, it updates at chrome, and creates new content version of script. So if you don't reload page, where you used previous version of your code to create context.js and trying it again with updated version, it throws this error.
I kinda wasted about one full day to figure it out, it's simply, but there a lot of answers in stackoverflow about this case, so you used to try them, and not think with your brain. Don't be like me:)

Have chrome extension display on certain page using page action

I'm trying to make a chrome extension for the Pinterest.
I followed the examples I found from the Chrome extension sample (the one with displaying icon in the omnibox when there is a 'g' in the url) and changed the file a bit to make it display the icon when the site has "pinterest.com" in it. Here is the code:
manifest.json:
"permissions": [
"tabs",
"http://*.pinterest.com/"
]
background.js, I copied most of the code from the example online:
function showPinterestAction(tabId, ChangeInfo, tab) {
if(tab.url.indexOf('pinterest.com') > -1){
chrome.pageAction.show(tabId);
}
/* This doesn't work. tab.url return undefine to me :( */
};
chrome.tabs.onUpdated.addListener(function(tabId, change, tab) {
if (change.status == "complete") {
showPinterestAction(tabId);
}
});
chrome.tabs.onActivated.addListener(function(tabId, info) {
selectedId = tabId;
showPinterestAction(tabId);
});
// Ensure the current selected tab is set up.
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
alert(tabs[0].id);
showPinterestAction(tabs[0].id);
});
It is not displaying the icon at the right page. If I try to alert(tab.url) it gives me undefined. Can someone please tell me what's wrong with my code?
Well, you're only ever calling showPinterestAction with one parameter, tabId.
No surprises, therefore, that tab parameter is simply undefined. The signature of showPinterestAction follows the tab update callback, but you're not using it like one.
You can modify showPinterestAction to pull the data it needs:
function showPinterestAction(tabId) {
chrome.tabs.get(tabId, function(tab){
if(tab.url.indexOf('pinterest.com') > -1){
chrome.pageAction.show(tabId);
}
});
};
You also probably want to make your match pattern more general: "*://*.pinterest.com/*" should cover your use case.
Alternatively, instead of latching on to multiple tabs events, you can use declarativeContent API - it was created for this.
var rule = {
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostSuffix: 'pinterest.com' }
})
],
actions: [ new chrome.declarativeContent.ShowPageAction() ]
};
chrome.runtime.onInstalled.addListener(function(details) {
chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
chrome.declarativeContent.onPageChanged.addRules([rule]);
});
});
In this case you will not need "heavy" permissions like "tabs" or host permissions. Your manifest only needs
"permissions": [
"declarativeContent",
"activeTab"
]
for this to work.

Message callback returns a value infrequently - Chrome Extension

I'm building a chrome extension which communicates with a nodejs server through websockets. The point of it is to track browsing history with content. It all seems to work, but occasionally (30% of the time) the callback in a function passed to onMessage.addListener doesn't fire correctly. Let me show you the code:
background.js
var socket = io('http://localhost:3000/');
var tabLoad = function (tab) {
socket.emit('page load', tab);
};
var tabUpdate = function (tabid, changeinfo, tab) {
var url = tab.url;
if (url !== undefined && changeinfo.status == "complete") {
tab.user_agent = navigator.userAgent;
tab.description = '';
tab.content = '';
socket.emit('insert', tab);
}
};
socket.on('inserted', function(page){
socket.emit('event', 'Requesting page content\n');
//page = {tab: page, id: docs._id};
chrome.tabs.sendMessage(page.tab_id, {requested: "content", page: page}, function(data) {
socket.emit('content', data);
});
});
try {
chrome.tabs.onCreated.addListener(tabLoad);
chrome.tabs.onUpdated.addListener(tabUpdate);
} catch(e) {
alert('Error in background.js: ' + e.message);
}
content script - public.js
var messageHandler = function(request, sender, sendContent) {
if (request.requested == "content") {
var html = document.getElementsByTagName('html')[0].innerHTML;
var data = {
content: html,
page: request.page
};
sendContent(data);
return true;
}
};
chrome.extension.onMessage.addListener(messageHandler);
The problem is that sometimes data in sendContent is undefined, while sometimes it is alright. Any ideas how to debug this or what i'm doing wrong?
I've tried replacing document.getElementsByTagName('html')[0].innerHTML with a hardcoded 'test' string, but that didn't help.
Pages like youtube/wikipedia seem to never work, while facebook/google works.
Edit: The sendContent callback does fire 100% of the time it's just that the data passed to it is undefined.
Edit: Here's the manifest file
{
"manifest_version": 2,
"name": "Socket test",
"description": "sockets are cool",
"version": "1.0",
"permissions": [
"http://st-api.localhost/",
"http://localhost:3000/",
"tabs",
"background",
"history",
"idle",
"notifications"
],
"content_scripts": [{
"matches": ["*://*/"],
"js": ["public/public.js"]
//"run_at": "document_start"
}],
//"browser_action": {
// "default_icon": "logo.png",
// "default_popup": "index.html"
//},
"background": {
//"page" : "background.html",
"scripts": ["socket-io.js", "background.js"],
"persistent": true
}
}
First off, your understanding that sendContent is executed 100% of the time is wrong.
As established in the comments, the sendMessage callback also gets executed when there was an error; and this error is, in your case, "Receiving end does not exist"
The error lies in your manifest declaration of the content script. A match pattern "*://*/" will only match top-level pages on http and https URIs. I.e. http://example.com/ will match, while http://example.com/test will not.
The easiest fix is "*://*/*", but I would recommend the universal match pattern "<all_urls>".
With that fixed, there are still a couple of improvements to your code.
Replace chrome.extension.onMessage (which is deprecated) and use chrome.runtime.onMessage
Modify the sendMessage part to be more resilient, by checking for chrome.runtime.lastError. Despite the wide permission, Chrome still won't inject any content scripts into some pages (e.g. chrome:// pages, Chrome Web Store)
Make sure you use "run_at" : "document_start" in your content script, to make sure onUpdated with "complete" is not fired before your script is ready.

window.postMessage from web_accessible_resource to content script

i try to post a message from a web_accessible_resource to a content-script of my chrome extension.
My Setup:
parts of my manifest.json:
"content_scripts": [{
"matches": ["http://*/*"],
"js": ["content.js"]
}],
"web_accessible_resources": ["run.js"]
content.js
// this listener is never triggered ;-(
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
if (request.type === 'foo') {
// do whatever i want if request.type is foo
}
});
run.js
window.postMessage({type: 'foo'}, '*');
Things i also tried that worked:
Adding a listener directly in run.js:
window.addEventListener("message", function(msg) {
if (msg.data.type === 'foo') {
// that one works
}
});
Posting a Message from a background script:
chrome.tabs.getCurrent(function (tab) {
chrome.tabs.sendMessage(tab.id, {type: "foo"});
});
Question:
What do i have to do? do i need to set some authorisation or something for my content-script or why does this not work???
You can now enable the api to send messages directly from a webpage to your extension id
Enable in manifest
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}
Send message from webpage:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
function(response) {
if (!response.success)
handleError(url);
});
Receive in extension:
chrome.runtime.onMessageExternal.addListener(function(){});
See: https://developer.chrome.com/extensions/messaging#external-webpage
just when i asked my question, i had an idea which of course...
... WORKED.
of course i need to attach a window.addEventListener in my content.js:
window.addEventListener("message", function(e) {
if (e.data.type === 'closeSidebar') {
// that works now
}
});

Categories