In the past I have written a couple of small extensions for chrome but never got a chance to write an extension for firefox. In the last couple of weeks, I tried to write one. Since I use firefox personally, I wanted to create something that would be useful for me.
There were two specific usecases that I could think of to write an extension.
- A remote bookmark system: I don’t use firefox’s sync devices feature so I need something to synchronize the bookmarks among my own devices.
- An extension to create video library for technical talks (mostly from conferences like pycon, gophercon, cpp-con etc.) that are available on youtube.
Both of the applications are based on a simple principle,
- You have the extension installed on the browser.
- You have a web service running somewhere and it is accessible to the browser.
- Using the extension, you send the
URL
of the page to the web service. - The web service recieves the
URL
and depending upon your usecase, it takes an appropriate action. In the case of remote bookmark, it should dump it in adatabase/key-val
store and in the case of video library; it should download that video using a tool likeyoutube-dl
.
Here is how I created it.
As you can see that there are two parts to it,
- Firefox addon.
- A web service.
Firefox addon
Here is how I thought of the addon,
- It registers a new context menu on the page, so that when a user right-clicks on the page it will give user an option to send the current URL to the server.
- On the right-click, it reads the title of the page and the current URL and sends it to the server.
To implement this.
We need two specific things,
manifest.json
: Browser uses this file to get all the details required about the addon.background.js
: This could be any.js
file. This is where the actual logic of this addon resides.
manifest.json
In this case manifest.json
looks like
{
"manifest_version": 2,
"name": "remote-lib-bookmark",
"version": "1.0",
"description": "a simple firefox addon.",
"icons": {
"48": "icons/border-48.png"
},
"content_scripts": [
{
"matches": [
"*://*/*"
]
}
],
"background": {
"scripts": [
"background.js"
]
},
"permissions": [
"tabs",
"contextMenus"
]
}
In this json
file we are telling browser
Name of the addon
{ "name": "remote-lib-bookmark" }
Content scripts
{ "content_scripts": [ { "matches": [ "*://*/*" ] } ] }
This tells browser to use this plugin to all of the URLs. More about match patterns can be found on developer.mozilla.org docs
Background scripts
{ "background": { "scripts": [ "background.js" ] } }
This script will be running through out the addon life time. (typically addon starts on the browser starts and it stops with the browser exits).
Permissions
{ "permissions": [ "tabs", "contextMenus" ] }
In this case we need these two specific permissions, to use different APIs for
tabs
and thecontextMenus
.
background.js
The background.js
looks like
// connect to the websocket server
var ws_con = new WebSocket("ws://localhost:8888/ws");
//console.log(ws_con);
// set the call-back for onopen
ws_con.opopen = function(event){
ws_con.send("hello from firefox addon");
}
// set the callback for onmessage
ws_con.onmessage = function (event){
console.log(event.data);
}
// set the callback for onerror
ws_con.onerror = function(error){
console.log(error);
}
// log the message that the addon has started.
console.log("remote-lib-bookmark addon started");
// create the context menu
browser.contextMenus.create({
id: "trial-context",
title: "remote-bookmark",
contexts: ["page", "browser_action", "page_action"]
}, function(e){console.log("menu created");});
// add the onClicked listener to the context menu.
browser.contextMenus.onClicked.addListener(function(info, tab) {
// We receive the tab and the info about the clicked context here.
console.log(info);
console.log(tab);
// check which context menu has been clicked.
switch (info.menuItemId) {
case "trial-context":
console.log(info.pageUrl);
// get the page url and the title and send it to the we service (over web socket).
var bookmark_data = {"title": tab.title, "url": info.pageUrl};
ws_con.send(JSON.stringify(bookmark_data));
break;
}
});
The web service
Since this is a web service running seperately, I chose to write it in python. I used tornado for writing it. Also since I wanted to try out leveldb this time, I used plyvel here.
The code for this looks like
main.py
import tornado.ioloop
import tornado.web
from tornado import websocket
import level_db_connect as ldb
import json
# handler for endpoint `/`
class MainHandler(tornado.web.RequestHandler):
def get(self):
bookamrks = ldb.get_all_bookmarks()
self.write(json.dumps(bookamrks))
# websocket handler for `/ws` endpoint
class EchoWebSocket(tornado.websocket.WebSocketHandler):
def open(self):
print("WebSocket opened")
def on_message(self, message):
print("received: ", message)
self.write_message(u"You said: " + message)
bookmark_data = json.loads(message)
ldb.add_bookmark(bookmark_data)
def on_close(self):
print("WebSocket closed")
def check_origin(self, origin):
return True
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
(r"/ws", EchoWebSocket),
])
if __name__ == "__main__":
ldb.init_db()
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
The web service code is fairly simple, it is a modified version of the sample code of web socket demo on tornado’s docuentation
level_db_connect.py
import plyvel
config = {}
def init_db():
db = plyvel.DB('./db/', create_if_missing=True)
config["db"] = db
def add_bookmark(bookmark_data):
print("bookmark data", bookmark_data)
config["db"].put(
bytes(bookmark_data["title"], "utf-8"),
bytes(bookmark_data["url"], "utf-8"))
def get_all_bookmarks():
bookmarks = {}
for key, val in config["db"]:
bookmarks[key.decode("utf-8")] = val.decode("utf-8")
return bookmarks
At this point the service just receives the bookmark data over web socket
and serves the json with all of the book marks at the end point /
.
This is a very crude implementation of both the firefox addon and the web service
but I think it is good enough to give the reader a basic idea about the subject.
I also live streamed these coding sessions.
If you are interested, please have a look:
and