Firefox/Chrome addon socketio - firefox

I'm trying to port chrome extension to firefox. My Chrome extension uses backgroung page to init one socketio connection on plugin start. Then in content page I send events to background page to emit events through socketio. So i have 1 connection and it is used by many tabs.
Is there any possibily to create something like this in FF? May be it's possible to create one shared worker for addon and communicate with worker from tabs? Any suggestions?
Thanks in advance.

You can implement something a lot like a background-page in Firefox using the SDK's PageWorker api. Here's a really simple example:
index.js
var data = require("sdk/self").data;
var page = require("sdk/page-worker");
var re = new RegExp("^https://www.google.*");
let { ActionButton } = require("sdk/ui/button/action");
let button = ActionButton({
id: "my-button-id",
label: "Button Label",
icon: {
"16": "chrome://mozapps/skin/extensions/extensionGeneric.png",
"32": "chrome://mozapps/skin/extensions/extensionGeneric.png"
},
onClick: function(state) {
main();
}
});
var worker;
function main() {
let contentURL = 'https://www.google.ca/';
if (worker) {
worker.contentURL = contentURL;
}
worker = page.Page({
include: re,
contentURL: contentURL,
contentScriptWhen: "ready",
contentScriptFile: data.url('worker.js')
});
worker.port.on('fromWorker', (m) => {
console.log("got message", m);
worker.port.emit('toWorker', true)
});
}
worker.js
console.log("worker> attached...");
self.port.on('toWorker', function(message) {
console.log("worker>", message);
});
self.port.emit("fromWorker", "message from content worker: "+ [document.location, document.title].join(', '));
If you want to interact with scripts loaded into the page, you'll need to proxy through the content script into the page via postMessage, see these docs for more help.

Related

Websockets+Single instance+crosstab communication best way?

I'm utilizing websockets for passing json messages but I don't want multiple ws connections if multiple tabs are open.
To reduce the connections I want to implement a single ws connection object that can send/receive messages to and from all tabs to my website. The object should forward the json to all tabs and each tab will process the message.
I've been looking at web/shared/service workers and I'm not sure the 2018 path to solve the issue and browser support seems to be a concern as well.
Looks like shared workers are not supported in Safari in support of service workers. Chrome/ff/opera seem to support shared workers.
In short it's a little confusing, bit of a mess and I want to know the best path forward with the best support.
If you know of a good resource, example code to implement ws with your suggested method please provide it as well.
After further research I've decided to implement web workers.
At this point I'm having success and I wanted to add an important piece that I got stuck on for future readers.
In my worker.js file I put this at the top to kick things off. The importScripts function threw an error if I didn't do it otherwise.
Also for the sake of helping, this is my skeleton code in my worker.js file that works. Message processing from the html pages are separated from the ws messages received from the server. You can start, stop the worker from the html page.
All tabs will get the messages from the worker, each page needs to process the messages as needed.
I'm also using robust-websockets so it auto reconnects from this github as this code works with web workers and is maintained. There is another project by the same name that isn't as updated by the time of this post. The reconnecting-websockets does not support web workers and you will get an error. - https://github.com/nathanboktae/robust-websocket
html
<script>
document.addEventListener('DOMContentLoaded', init);
function init(){
worker = new Worker('js/ws_worker.js');
worker.addEventListener('message', workerMessaged);
worker.postMessage({ "args": '<username_admin>', "cmd": 'start', "url": '127.0.0.1:8000' });
worker.postMessage({ "message": 'Initialize new worker'});
console.log('Message posted to worker, start');
}
// Received a json message from the Worker, process it.
function workerMessaged(ev){
console.log('Message received from worker');
console.log(ev.data);
worker.postMessage({ "cmd": 'message', "message": 'Sending reply over ws'});
}
worker.js
// proper initialization
if( 'function' === typeof importScripts) {
importScripts('robust-websocket.js');
var WebSocket;
self.addEventListener("message", function(e) {
var args = e.data.args;
var cmd = e.data.cmd;
var roomName = e.data.args;
var url = e.data.url;
var message = e.data;
// Code to process ws messages from the server
WebSocket.onmessage = function(event) {
console.log(" WebSocket message received: " + event.data, event);
self.postMessage(event.data);
};
WebSocket.onerror = function(event) {
console.log(" WebSocket message received: " + event.data, event);
self.postMessage(event.data);
};
if (cmd === 'start') {
WebSocket = new RobustWebSocket(
'ws://' + url +
'/ws/' + roomName + '/');
console.log('Connected via websockets');
/* Send initial message to open the connection and finalize the ws object*/
WebSocket.onopen = function() {
var obj = { "message": "hello" };
var json = JSON.stringify(obj);
WebSocket.send(json);
};
} else if (cmd === 'stop') {
WebSocket.onclose = function() {
console.log('Closing WebSocket');
WebSocket.close();
console.log('Closed WebSocket');
console.log('Terminating Worker');
self.close(); // Terminates the worker.
};
} else if (cmd === 'message') {
WebSocket.onopen = function() {
var json = JSON.stringify(message);
WebSocket.send(json);
};
console.log('message sent over websocket');
console.log('message');
} else {
console.log('logging error ' + e.data);
console.log(e.data);
self.postMessage('Unknown command: ');
}
}, false);
};

how to append iframe to hosted page using Firefox SDK addon?

Assume frame.html inside data folder in Firefox SDK addon,
how to append an iframe and define frame.html as its source?
Additional info:
Because of CPS, its not possible to use inline source, so I can't use data.load('frame.html'), I need to use URL of the file:
lib/main.js
tabs.activeTab.attach({
contentScriptFile: [self.data.url("js/appender.js") ],
attachTo: 'top',
contentScriptOptions: {
scriptUrl: self.data.url("js/sandbox.js"),
imageUrl: self.data.url("image.jpg")
frameUrl: self.data.url("sandbox.html")
}
});
data/appender.js
var scriptTag = document.createElement("script");
scriptTag.src = self.options.scriptUrl;
document.body.appendChild(scriptTag); // worked fine
var imageTag = document.createElement("image");
imageTag.src = self.options.imageUrl;
document.body.appendChild(imageTag); // worked fine
var iframeTag = document.createElement("iframe");
iframeTag.src = self.options.frameUrl;
document.body.appendChild(iframeTag); // the html tag of iframe is empty
It's blank because of the firefox security policy which doens't allow content scripts to load resources URLs as iframes. The solution could be to set it directly from the background script, for this you'll need to use low level sdk.
var { viewFor } = require("sdk/view/core");
var tab_utils = require("sdk/tabs/utils");
var iframe = viewFor(tab).linkedBrowser._contentWindow.document.querySelector('iframe[src="' + self.data.url("sandbox.html") + '"]')
iframe.contentWindow.location.href = iframe.getAttribute("src")
so the completely working code would look like:
data/appender.js
var iframeTag = document.createElement("iframe");
iframeTag.src = self.options.frameUrl;
document.body.appendChild(iframeTag); // the html tag of iframe is empty
// note that the iframe is attached after the content ready event, so you have to inform the background when it can start working with the iframe
self.port.emit("attached", true);
main.js
tabs = require("sdk/tabs")
self = require("sdk/self")
var { viewFor } = require("sdk/view/core");
tab_utils = require("sdk/tabs/utils");
tabs.on("ready", function(tab) {
var worker = tab.attach({
contentScriptFile: [self.data.url("js/appender.js") ],
attachTo: 'top',
contentScriptOptions: {
frameUrl: self.data.url("sandbox.html")
}
});
worker.port.on('attached', function() {
reinitIframe(tab)
});
function reinitIframe(tab) {
var iframe = viewFor(tab).linkedBrowser._contentWindow.document.querySelector('iframe[src="' + self.data.url("sandbox.html") + '"]')
iframe.contentWindow.location.href = iframe.getAttribute("src")
}
})
You'll probably need a cross-process wrapper in order to make it work in future version of Firefox with electrolysis
Found a better way. You add a blank <iframe> and attach an event listener for the load event. Then you can easily append whatever elements you want to the iframe as usual.
Include this in your content-script:
var iframe = document.createElement('iframe'),
div = document.createElement('div');
div.textContent('Hello World');
document.body.appendChild(iframe);
iframe.addEventListener('load', function(){
this.contentWindow.document.body.appendChild(div)
})

Firefox Addon - Accessing pre-loaded content script from ActionButton

I am attaching content script to every tab on my firefox add-on.
And if user clicks on ActionButton (top right icon on browser) I am trying to access content script handler function, but it does not work.
I am using walkthroughs from Content Scripts but still could not make it work.
Am I doing something wrong?
Please see code below the TODO marked areas are not working;
// main.js
tabs.on('ready', function (tab) {
var worker = tab.attach({
include: "*",
//contentScript: 'window.alert("Page matches ruleset");',
contentStyleFile: [data.url("scripts/myTestContent1.js")]
});
worker.port.on('listener1', function (params) {
// I will listen some messages coming from myTestContent1.js
});
console.log("main.js: tab is ready!");
});
// browser icon typically at top right
var button = buttons.ActionButton({
id: "myActionButton",
label: "My pretty add-on",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick
});
// when top right browser icon button is clicked
function handleClick(state) {
var myWorker = tabs.activeTab.attach({
//contentScriptFile: // I do not want to attach anything, just get Active tab!
});
// TODO this code is not handled by active tab's content script
// should be handled by myTestContent1.js but does not work
myWorker.port.emit("initialize", "Message from the add-on");
}
myWorker.port.emit is not calling handler function on my content script.
// myTestContent1.js
// TODO this code is not calling :(
self.port.on("initialize", function () {
alert('self.port.on("initialize")');
});
I fixed the problem by keep tracking tab-worker pairs, so
var TabWorkerPair = function (tabid, worker) {
this.tabid = tabid;
this.worker = worker;
};
tabs.on('close', function (tab) {
for (var i = 0; i < TabWorkerPairList.length; i++) {
var pair = TabWorkerPairList[i];
if (pair.tabid == tab.id) {
// remove object from list
TabWorkerPairList.splice(i, 1);
break;
}
}
console.log("main.js > tabs.on('close') > TabWorkerPairList count: " + TabWorkerPairList.length);
});
tabs.on('ready', function (tab) {
var worker = tab.attach({
include: "*",
contentScriptFile: [data.url("scripts/myTestContent1.js")]
});
// queue workers for tab
var pair = new TabWorkerPair(tab.id, worker);
TabWorkerPairList.push(pair);
console.log("main.js: tab is ready!");
});
and finally I can now emit worker function;
// when top right browser icon button is clicked
function handleClick(state) {
for (var i = 0; i < TabWorkerPairList.length; i++) {
var pair = TabWorkerPairList[i];
if (pair.tabid == tabs.activeTab.id) {
pair.worker.port.emit("initialize", pair.tabid);
break;
}
}
}
main reason: a tab can have multiple workers. So you should manually access the worker you are interested in.

How to emit and listen event between pagemod and widget?

I am using the addon-sdk. I have a widget and upon clicking the widget I want to do something with the website (be it modifying the page or reading the deep DOM).
So my thought after reading https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/page-mod#Communicating_With_Content_Scripts would be:
pagemod activated on matched url (in this case ANY)
click on widget which satisfies left-click event. It then emits widget-click.
Pagemod receives widget-click event and fires back an event called from-pagemod.
from-pagemod does something to the webpage.
I see the following output in stdout:
console.log: project: about to emit widget-click
console.log: project: after emitting widget-click
So pagemod didn't receive that event or it was never set up. I am not sure what is missing from this simple test case. Any help is appreciated.
Here is lib/main.js.
var widgets = require('sdk/widget');
var pageMod = require("sdk/page-mod");
var data = require("sdk/self").data;
var tabs = require("sdk/tabs");
exports.main = function() {
var widget = widgets.Widget({
label: "widget label",
id: "widget-id",
contentURL: data.url("off.png"),
contentScriptFile: [data.url("widget.js"), data.url("page.js")]
});
widget.port.on("left-click", function() {
console.log("about to emit widget-click");
widget.port.emit("widget-click", "foo");
console.log("after emitting widget-click");
});
var page = pageMod.PageMod({
include: "*",
contentScriptWhen: "end",
contentScriptFile: data.url("page.js"),
onAttach: function(worker) {
worker.port.on("widget-click", function(msg) {
console.log("on widget-click, ready to emit from-pagemod event");
worker.port.emit("from-pagemod", "foo");
});
}
});
};
Here is page.js
self.port.on("from-pagemod", function(msg) {
console.log("inside from-pagemod listener");
// read DOM or modify the DOM
});
Here is widget.js
this.addEventListener('click', function(event) {
if(event.button == 0 && event.shiftKey == false)
self.port.emit('left-click');
}, true);
Edit 2, Answering the actual question:
If you want to do something to the page on widget click when the content script is already attached, register your widget listener somewhere you have access to the page-worker. You could do this by putting your widget.port.on code inside the pageMod's onAttach(), but then it would only work for the most recently attached page. The best way to make it functional would be to store all workers then check if the current tab has a worker when the widget is clicked, like so:
main.js partial
var workers = [];
widget.port.on("left-click", function() {
console.log("about to emit widget-click");
var worker = getWorker(tabs.activeTab);
if (worker) {
worker.port.emit("widget-click", "foo");
console.log("after emitting widget-click");
}
});
var page = pageMod.PageMod({
include: "*", // TODO: make more specific
contentScriptWhen: "end",
contentScriptFile: data.url("page.js"),
onAttach: function(worker) {
workers.push(worker);
worker.on('detach', function() {
detachWorker(worker);
});
// could be written as
// worker.on('detach', detachWorker.bind(null, worker);
}
});
function detachWorker(worker) {
var index = workers.indexOf(worker);
if(index!==-1) workerArray.splice(index, 1);
}
function getWorker(workers, tab) {
for (var i = workers.length - 1; i >= 0; i--) {
if (workers[i].tab===tab) return worker;
}
}
page.js
self.port.on("widget-click", function(msg) {
console.log("inside widget-click listener");
// read DOM or modify the DOM
});
The reason that your solution wasn't working is that you assumed that events were somehow linked to the file rather than an object.
Old answer: You're making it way too complicated.
Just attach the content script on click (as opposed to adding a content script to every single page that does nothing unless it receives an event)
main.js
var widgets = require('sdk/widget');
var data = require("sdk/self").data;
var tabs = require("sdk/tabs");
exports.main = function() {
var widget = widgets.Widget({
label: "widget label",
id: "widget-id",
contentURL: data.url("off.png"),
contentScriptFile: data.url("widget.js")
});
widget.port.on("left-click", function() {
console.log("about to attach script");
var worker = tabs.activeTab.attach({
contentScriptFile: data.url("page.js");
});
worker.port.on('message from content script', function(someVariable){
//Now I can do something with someVariable in main.js
});
});
};
page.js
// read DOM or modify the DOM
//I'm done, I'll send info back to the main script. This is optional
self.port.emit('message from content script', someVariable);
Edit: Read Modifying the Page Hosted by a Tab for more info. Also, the Tutorials page is a good place to start when you're trying to do something you haven't done before with the SDK. It's a good way to step back and think of alternatives for trying to achieve your goal.
a port is a communication channel between a content script and an add-on component: your Widget may interact with its content via widget.js, and your pagemod with the matched webpage content via page.js. When you do widget.port.emit("widget-click", "foo"); this message can only be listened by widget.js using self.port.on('widget-click') not by the pagemod instance. Since you widget and pagemod are objects that share the main.js scope they can talk each other by just accessing its properties and methods.

How to create a button near the address bar?

I am designing a firefox extension, and I want to add a button near the address bar. And then I need to attach a bookmarklet to that button.
Someone can tell me what APIs do I have to use to create that button and to add the bookmarklet ?
Here's an example that uses Erik Vold's toolbarbutton library to add a button near the addressbar:
const data = require("self").data;
const tabs = require("tabs");
exports.main = function(options) {
var btn = require("toolbarbutton").ToolbarButton({
id: 'my-toolbar-button',
label: 'Add skull!',
image: data.url('skull-16.png'),
onCommand: function() {
if (typeof(tabs.activeTab._worker) == 'undefined') {
let worker = tabs.activeTab.attach({
contentScript: 'self.port.on("sayhello", function() { alert("Hello world!"); })'
});
tabs.activeTab._worker = worker;
}
tabs.activeTab._worker.port.emit("sayhello");
}
});
if (options.loadReason === "install") {
btn.moveTo({
toolbarID: "nav-bar",
forceMove: false // only move from palette
});
}
};
You can also see this as a runnable example on the Add-on Builder site:
https://builder.addons.mozilla.org/addon/1044724/latest/

Resources