Reputation: 15508
I'm currently trying to write a browser extension that catches all mailto links, instead of letting them being opened by the default mail application. The mechanism can be enabled and disabled using a simple toggle button.
When my extension loads everything seems to work, but I get a Error: Could not establish connection. Receiving end does not exist.
. Then when I use the toggle button to disable and enable it again I get the following:
23:52:15.247 function sendMsgToTabs(msg) background.js:16:9
23:52:15.256 this.sendMsgToTabs(...) is undefined background.js:17
23:52:15.302 Error: Could not establish connection. Receiving end does not exist. (unknown)
23:53:29.347 TypeError: this._recipeManager is null[Learn More] LoginManagerParent.jsm:77:9
Edit: And the disabling doesn't seem to work properly in the content script. The click handler is not properly removed for some reason...
So what could be causing all these errors? I checked this.sendMsgToTabs
in the debugger and it actually isn't undefined. I also don't have any unusual sites open, so I don't understand why there is a connection problem.
I've only tested my code in Firefox for now. But people say Chrome's API is essentially the same. Here is my code:
background.js
'use strict'
class MyWebExtensionBackend {
constructor() {
this.icons = {
enabled: '/icons/on.png',
disabled: '/icons/off.png'
}
this.isEnabled = true
browser.browserAction.onClicked.addListener(this.toggle.bind(this)) //toolbar button
browser.runtime.onMessage.addListener(this.msgListener.bind(this))
this.enable()
}
enable() {
browser.browserAction.setIcon({ path: this.icons.enabled })
this.isEnabled = true
console.log(this.sendMsgToTabs)
this.sendMsgToTabs({isEnabled: this.isEnabled}).catch(console.error)
}
disable() {
browser.browserAction.setIcon({ path: this.icons.disabled })
this.isEnabled = false
this.sendMsgToTabs({isEnabled: this.isEnabled}).catch(console.error)
}
toggle() {
if (this.isEnabled)
this.disable()
else
this.enable()
}
sendMsgToTabs(msg) {
return browser.tabs.query({}, tabs => {
let msgPromises = []
for (let tab of tabs) {
let msgPromise = browser.tabs.sendMessage(tab.id, msg)
msgPromises.push(msgPromise)
}
return Promise.all(msgPromises)
})
}
msgListener(msg, sender, sendResponse) {
console.log(msg.link)
/* browser.notifications.create({ // doesn't work
"type": "basic",
"iconUrl": browser.extension.getURL("icons/on.png"),
"title": 'url opened',
"message": msg.link
}); */
}
}
let myWebExtensionBackend = new MyWebExtensionBackend()
content-script.js
'use strict'
class MyWebExtensionFrontend {
constructor() {
this.isEnabled = false
browser.runtime.onMessage.addListener(this.msgListener.bind(this))
}
linkHandler(event) {
if (event.target.tagName !== 'A')
return
let link = event.target.href
if (link.startsWith('mailto:')) {
event.preventDefault() // doesn't appear to have an effect
console.log(link)
browser.runtime.sendMessage({'link': link}).catch(console.error)
//using a promise here felt kind of wrong
//because event handler functions can't really deal with that
//from what I can tell
return false // doesn't appear to have an effect
}
}
enable() {
console.log('enable frontend')
window.addEventListener('click', this.linkHandler.bind(this))
this.isEnabled = true
}
disable() {
console.log('disable frontend')
window.removeEventListener('click', this.linkHandler.bind(this))
this.isEnabled = false
}
msgListener(req) {
if (req.isEnabled)
this.enable()
else
this.disable()
return Promise.resolve({res: ''})
}
}
let myWebExtensionFrontend = new MyWebExtensionFrontend()
manifest.json
{
"description": "A basic toggle button",
"manifest_version": 2,
"name": "toggle-button",
"version": "1.0",
"homepage_url": "https://github.com/TODO",
"icons": {
"48": "icons/on.png"
},
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content-script.js"]
}
],
"browser_action": {
"default_icon": "icons/on.png"
}
}
Upvotes: 1
Views: 1942
Reputation: 349042
There are two issues with your code. One is specific to WebExtensions, another is specific to DOM and JavaScript.
In Firefox, WebExtensions are available at two namespaces, chrome.
and browser.
. chrome.
is designed to behave as close to Chrome's extension API, whereas browser.
has some enhancements. In particular, the asynchronous browser.
APIs may return a promise. The promise is only returned if the method is not called with a callback function. If it does receive a callback as parameter, the browser.
API behaves like chrome.
. Here are some examples:
// These have a callback and return undefined.
chrome.tabs.query({}, function(tabs) { /* ... */ });
browser.tabs.query({}, function(tabs) { /* ... */ });
chrome.tabs.query({}); // No callback, returns undefined (this is quite useless).
browser.tabs.query({}); // No callback, returns a Promise<Array>.
browser.tabs.query({}).then(function(tabs) { /* ... */});
Note that the error message that you received showed that the return value is undefined:
this.sendMsgToTabs(...) is undefined
If the method was undefined (as you thought), Firefox would print the following error:
this.sendMsgToTabs is not a function
The fix for your case is to use promises as follows:
sendMsgToTabs(msg) {
// Note: Changed ", tabs =>" to ").then(tabs =>"
return browser.tabs.query({}).then(tabs => {
let msgPromises = []
for (let tab of tabs) {
let msgPromise = browser.tabs.sendMessage(tab.id, msg)
msgPromises.push(msgPromise)
}
return Promise.all(msgPromises)
});
}
If there is any tab without a content script, the promise will be rejected though. If you don't care about the return value of the promise, add the following after let msgPromise
:
msgPromise = msgPromise.catch(() => {}); // Ignore errors.
Your code looks like this:
// your enable() function:
window.addEventListener('click', this.linkHandler.bind(this))
// your disable() function:
window.removeEventListener('click', this.linkHandler.bind(this))
The issue with this is that bind
returns a new function. So when the enable()
method is called, a new function is created and used as an event listener. When disable()
is called, a new function is created and passed to removeEventListener
. Since this new function is distinct from any other function (the previous parameter to addEventListener
in particular), the result is that the click handler is not removed.
There are three ways to register DOM events with the desired this
value:
At the construction of the class, replace the instance method with a bound function:
// In the constructor:
this.linkHandler = this.linkHandler.bind(this);
// In your enable() function:
window.addEventListener('click', this.linkHandler);
// In your disable() function:
window.removeEventListener('click', this.linkHandler);
At the call of enable()
create a bound function if needed:
// In your enable() function:
if (!this.linkHandlerBound) {
this.linkHandlerBound = this.linkHandler.bind(this);
}
window.addEventListener('click', this.linkHandlerBound);
// In your disable() function:
window.removeEventListener('click', this.linkHandlerBound);
// If wanted (but not needed), run: this.linkHandlerBound = null;
Pass an object with the handleEvent
method:
// A method of your class
handleEvent(event) {
if (event.type === 'click') {
this.linkHandler(event);
}
}
// In your enable() function:
window.addEventListener('click', this);
// In your disable() function:
window.removeEventListener('click', this);
Note, do not make the similar mistake of defining the object with the handleEvent
method as follows:
// Do NOT do this! You won't be able to remove the listener because
// you did not store a reference to the event handler object.
window.addEventListener('click', {
handleEvent: this.linkHandler.bind(this)
});
Upvotes: 2
Reputation: 8102
I just noticed that you haven't shared how you handle connections but you may want to check Long-lived connections to distinguish different types of connections and how to have an established connection.
As mentioned in the given link,
When establishing a connection, each end is given a
runtime.Port
object which is used for sending and receiving messages through that connection.
Here is a sample on how you open a channel from a content script, and send and listen for messages:
var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
if (msg.question == "Who's there?")
port.postMessage({answer: "Madame"});
else if (msg.question == "Madame who?")
port.postMessage({answer: "Madame... Bovary"});
});
Furthermore, you may want to also check the suggested solutions in this related SO post and see which works for you.
Upvotes: 0