Build a Firefox extension step-by-step
Lately I’ve been using the reading list on dev.to. It’s a nice tool, but I’m used to saving articles for later in Pocket.
In this article, we’re going to create a Firefox extension to automatically add a post to your Dev.to reading list and your Pocket account at the same time. Here is what it looks like (the extension file is available at the end of this article): The extension expects you to be already connected to a Pocket account in your browser (so we don’t have to handle API authentification).
What is a browser extension?
- manifest.json (configuration file)
- background.js (our background script)
- devtopocket.js (content script executed on the dev.to page)
- images/
Content and background scripts
We have two scripts in our project: one that handles background work (sending the Ajax request) and another one (a content script) that registers a click event on the «Reading list» Dev.to button:
Content script
The content script (devtopocket.js) registers the click and sends the request to our background script.
document.getElementById("reaction-butt-readinglist").addEventListener("click", function() if(window.confirm("Do you want to save this article in Pocket?")) sendBackgroundToPocket(); > >);
The sendBackgroundToPocket method needs to communicate with the background script and ask it to send the Ajax request.
browser.runtime gives us a two way communication channel between our extension scripts. browser.runtime.sendMessage sends a message on that channel and returns a Promise that waits for a response on the other side. Once we get the answer (meaning the Ajax request has completed), a message is displayed to the user (cf the above gif):
function sendBackgroundToPocket() browser.runtime.sendMessage("url": window.location.href>).then(function() document.getElementById("article-reaction-actions").insertAdjacentHTML("afterend", "This article has been saved to Pocket!") setTimeout(function() document.getElementById("devtopocket_notification").remove() >, 2000) >); >
Background script
A background script is used to write time consuming operations that do not depend on a specific web page being opened.
These scripts are loaded with the extension, and are executed until the extension is disabled or uninstalled.
Our background script (background.js) has two roles:
In the extension configuration (manifest.json below), we’re going to say «load devtopocket.js on pages matching an url pattern» and it works when we browse straight to an article page.
The «issue» with dev.to website is that it uses HTML5 History api to browse pages (as does every single page webapp). Firefox doesn’t listen for url changes if the page isn’t fully reloaded and therefore doesn’t execute our content script. That’s why we’re going to need a background script to listen for url changes via History API, and manually execute the frontend script when needed.
We listen to url changes by using the webNavigation API:
browser.webNavigation.onHistoryStateUpdated.addListener(function(details) browser.tabs.executeScript(null,file:"devtopocket.js">); >, url: [originAndPathMatches: "^.+://dev.to/.+/.+$">] >);
restricts the listener to a specific target url pattern (the same as the one we’re also going to define in our manifest.json ).
The browser.tabs.executeScript method loads a content script in the current tab.
The background scripts expects a message from our content script (when the «Reading list» button is clicked):
function handleMessage(message, sender, sendResponse) if(message.url) sendToPocket(message.url, sendResponse) return true; > > browser.runtime.onMessage.addListener(handleMessage)
The sendToPocket method is called upon message receiving.
To save our url in Pocket, we’re going to call the existing save page provided by Pocket (https://getpocket.com/save). A classic Ajax request will do the trick:
function sendToPocket(url, sendResponse) var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) sendResponse(); > >; xhr.open("GET", "https://getpocket.com/save?url="+url, true); xhr.send(); >
You might see coming the Cross Origin Request problem, we’ll address it later with the extension permissions.
The manifest
manifest.json is our extension configuration file. It’s like a package.json in a javascript webapp or an AndroidManifest.xml in an Android app. You define the version and name of your project, permissions that you need and JavaScript source files that compose your extension.
First we write the app definition:
"manifest_version": 2, "name": "DevToPocket", "version": "1.0.0", "description": "Send your DEV.to reading list to Pocket", "icons": "48": "icons/devtopocket-48.png" >, . >
Supply at least a 48×48 icon, if you supply more sizes Firefox will try to use the best icon size depending on your screen resolution. We’re going to use this icon:
Then we define our permissions:
. "permissions": [ "storage", "cookies", "webNavigation", "tabs", "*://dev.to/*/*", "*://getpocket.com/*" ] >
You can find the permissions list in the Mozilla documentation.
URLs in the permissions gives our extension extended privileges. In our case, it gives us access to getpocket.com from dev.to without cross-origin restrictions, we can inject a script in dev.to via tabs.executeScript and we have access to getpocket.com cookies so the Ajax request is authentificated. The full host permissions list is available here.
The full manifest.json file:
"manifest_version": 2, "name": "DevToPocket", "version": "1.0.0", "description": "Send your DEV.to reading list to Pocket", "icons": "48": "icons/devtopocket-48.png" >, "content_scripts": [ "matches": ["*://dev.to/*/*"], "js": ["devtopocket.js"] > ], "background": "scripts": ["background.js"] >, "permissions": [ "storage", "cookies", "webNavigation", "tabs", "*://dev.to/*/*", "*://getpocket.com/*" ] >
Run the extension
In order to run your extension, we are going to use the web-ext command line: https://github.com/mozilla/web-ext
This is a command line tool to help build, run, and test WebExtensions.
npm install --global web-ext
Then in your terminal, run the following command in your project folder:
It’s going to launch a browser with your extension temporarily loaded. The extension is automatically reloaded when you make some changes.
Sign the extension
To install your extension in someone else’s browser, you’ll need to package and sign the extension.
First create a developer account on the Mozilla Developer Hub then retrieve your API credentials here: https://addons.mozilla.org/en-US/developers/addon/api/key/
Run the web-ext sign command:
web-ext sign --api-key=user:XXX --api-secret=YYY
Your extension file will be available afterwards in web-ext-artifacts/devtopocket-X.X.X-an+fx.xpi. Open the file in Firefox to install it.
The complete source code is available on GitHub: https://github.com/scleriot/devtopocket
You can download and install the latest release: https://github.com/scleriot/devtopocket/releases/latest
This extension also works with Firefox for Android!
Controlling a Firefox Extension via Javascript
Is it possible, using javascript, to control an overlay firefox extension? I’ve extracted the contents of the extension and have identified what functions/methods I need to run, but they are not accessible within the scope of the console. Thanks in advance for any ideas.
You can have the extension listen for an Event on say window , then dispatchEvent from the JavaScript on the page (this requires modifying the extension’s code).
I heard you can get the top level wrapper of any addon and use any function from there. I asked on a forum where this was mentioend before will update you with what I hear back.
2 Answers 2
Yes it possible to interact with other add-ons, given the right circumstances.
My test case here will be com.googlecode.sqlitemanager.openInOwnWindow() , which is part of the SqliteManager addon.
- In newer builds (I’m using Nightly), there is the Browser Toolbox. With it is is as simple as opening a toolbox and executing com.googlecode.sqlitemanager.openInOwnWindow() in the Console.
- You may instead use the Browser Console (or any chrome enabled WebDev Console for that matter, e.g. the Console of «about:newtab»). But you need some boilerplate code to first find the browser window. So here is the code you can execute there: var bwin = Services.wm.getMostRecentWindow(«navigator:browser»); bwin.com.googlecode.sqlitemanager.openInOwnWindow()
- Again, enable chrome debugging. Then open a Scratchpad and switch to Chrome in the Environment menu. Now executing com.googlecode.sqlitemanager.openInOwnWindow() in our Scratchpad will work.
- You may of course write your own overlay add-on.
- As a last resort, patch the add-on itself.
- Bootstrapped/SDK add-ons: you can load XPIProvider.jsm (which changed location recently) and get to the bootstrapped scope (run environment of bootstrap.js ) via XPIProvider.bootstrapScopes[addonID] , and take it from there (use whatever is in the bootstrap scope, e.g. the SDK loader).
Now about the right circumstances: If and how you can interact with a certain add-on depends on the add-on. Add-ons may have global symbols in their overlay and hence browser window, such as in the example I used. Or may use (to some extend) JS code modules. Or have their own custom loader stuff (e.g. AdBlock Plus has their own require() -like stuff and SDK add-ons have their own loader, which isn’t exactly easy to infiltate).
Since your question is rather unspecific, I’ll leave it at this.
Edit by question asker: This is correct, however I figured I’d add an example of the code I ended up using in the end, which was in fact taken directly from mozilla’s developer network website:
var myExtension = < myListener: function(evt) < IprPreferences.setFreshIpStatus(true); // replace with whatever you want to 'fire' in the extension >> document.addEventListener("MyExtensionEvent", function(e) < myExtension.myListener(e); >, false, true); // The last value is a Mozilla-specific value to indicate untrusted content is allowed to trigger the event.
var element = document.createElement("MyExtensionDataElement"); element.setAttribute("attribute1", "foobar"); element.setAttribute("attribute2", "hello world"); document.documentElement.appendChild(element); var evt = document.createEvent("Events"); evt.initEvent("MyExtensionEvent", true, false); element.dispatchEvent(evt);