A Practical Guide To Browser Extensions
Recently I've had a serious problem with wasting my time on watching Youtube, Netflix, HBOMax, sports, and other brainless entertainment. I love watching the stuff otherwise I wouldn't be doing it. After spending way too much time on it I decided that I need to do something about it. But before I do something about it I want to shoutout Lovecraft Country on HBO, because the show was great. If you like horror/spooky/mystery stuff check it out.
If you've been following Learning Computations you'll know that I installed Arch Linux recently, and talked about all the stuff I learned in the process. While configuring Arch it really inspired me to make my own stuff after seeing just how many solutions there where for the same problem. It made me think why don't I create a tailor made solution for my own problem. So I did. I made a browser extension to fix my problem of not being able to stop myself from watching brainless entertainment.
Here's what we'll do:
- Define the web extension that will stop me from being a lazy piece of trash
- Look through extension docs and figure out what an extension is and what it's made of
- Build an extension
- Finish off with publishing an extension to Firefox and Chrome add on stores
Chapter 1: What's being built? What is an extension?
Alright let's start by defining what the web extension should do. The extension I want should let me
- Create categories and add time limits to those categories
- Add websites to categories and track my time on those websites
- Block me from all the websites in that category once I hit the limit
- Set a bed time. Once it's bed time all websites I visit are blocked
To keep this article focused I'm only going to implement the bed time feature. I want to focus on web extensions, and not logic specific to my application.
The first place to look was the docs. The your first extension tutorial in the Mozilla extensions docs seemed like a logical place to start. In this tutorial I built an extension that changed the border of pages belonging to the mozilla.org
domain. Lets briefly cover this tutorial.
In following this tutorial I created a directory with some files that looks like this:
-
borderify
manifest.json
borderify.js
icons/...
The first thing it asked me to do is create a
manifest.json
file and fill it out with the contents they provide. What ismanifest.json
? They don't say, but we'll answer this question in a bit.One key in the
manifest.json
iscontent_scripts
I'll let the tutorial explain this
The most interesting key here is content_scripts, which tells Firefox to load a script into Web pages whose URL matches a specific pattern.
In this case, we're asking Firefox to load a script called "borderify.js" into all HTTP or HTTPS pages served from "mozilla.org" or any of its subdomains.
- Once you link
borderify.js
by adding it tocontent_scripts
inmanifest.json
you add some JS toborderify.js
to make the border ofmozilla.org
domains red.
If you have some time I'd recommend doing the tutorial as it isn't too time consuming, and it'll make things more concrete. If you don't then don't worry we'll cover everything it does. The tutorial doesn't go into much detail, but it offers a starting point.
Great. I've done this tutorial, created these files, but I'm not really sure how all the pieces fit together, what is an extension made of exactly, and what else can extensions do? Let's try to figure these out so we have a better picture of what's going on.
Alright so what is an extension? The next place in the docs I checked out was What Are Extensions, and it was a bit more helpful.
An extension adds features and functions to a browser. It’s created using familiar web-based technologies—HTML, CSS, and JavaScript.
It can take advantage of the same web APIs as JavaScript on a web page, but an extension also has access to its own set of JavaScript APIs.
This means that you can do a lot more in an extension than you can with code in a web page.....
Extensions for Firefox are built using the WebExtensions APIs, a cross-browser system for developing extensions. To a large extent, the API is
compatible with the extension API supported by Google Chrome and Opera. Extensions written for these browsers will in most cases run in
Firefox or Microsoft Edge with just a few changes. The API is also fully compatible with multiprocess Firefox. - What Are Extensions
Ok now I'm getting somewhere. Web extensions aren't that different from normal JS, CSS, and HTML apps, but they have access to a special API. The Web Extensions API. The nice sounding part about this is it seems like the code I write will be compatible with other browsers! Which is great to hear I don't want to write different code for basically the same thing. There's some gotchas here, but we'll cover them later. I'm focused on building my extension for Firefox right now, but once I get to Chrome you'll see the mistakes I made.
Ok I have an idea about what a Web Extension is and the technology it uses, but still don't know how the tutorial app fully ties into this. Lets figure that out.
Chapter 2: What is an extension made of?
The your first extension tutorial mentions the Anatomy of an Extension article. Here we'll figure out what an extension is actually made of.
An extension consists of a collection of files, packaged for distribution and installation - Anatomy of an Extension
Alright then. An extension is just some files. Very cool I guess.
Here's the answer to "what is manifest.json
?":
This is the only file that must be present in every extension. It contains basic metadata
such as its name, version, and the permissions it requires. It also provides pointers to other files in the extension.
In other words manifest.json
is the glue that hold together my extension. It's the file that tells the browser "hey I'm an extension, and here's my name, version, permissions, and all the files I use to do what I need to do Mr. browser".
So all an extension is a manifest.json
+ other files (like the content_scripts key) which manifest.json
points to. This is exactly what the tutorial app is. Things are starting to make more sense.
Chapter 3: Lets build this shit
manifest.json
Now i've got an idea what an extension is, and what its made up of. Next on the agenda is figuring out what my extension needs. Based on Anatomy of an Extension this is what I'll add:
Icons - For the extension and any buttons it might define.
Obviously my extension has to look ver very cool so I'll need some icons
Sidebars, popups, and options pages - HTML documents that provide content for various user interface components.
I'll need a way to set a bed time so I'll use one of these to create an HTML form.
Content scripts - JavaScript included with your extension, that you will inject into web pages.
I'll need to block websites after the bed time I set, and changing the HTML of existing sites seems like an easy way to do this. The only question here is how will I get my bed time into the content script?
All of these things will be part of my manifest.json
, which will get setup as we go along. Remember manifest.json
is our glue. manifest.json
has a lot of keys we won't get to, but it's worth checking out the reference to see all the details: manifest.json reference
Oh also while digging around in the docs I found this about manifest.json
It is a JSON-formatted file, with one exception: it is allowed to contain "//"-style comments.
This is fucking cool. If you've worked with JSON you'll know it doesn't let you have comments. This seems like a massive technological advancement so I'll be using it, but this might be the time to ask yourself has technology gone too far? Anyways, this is very exciting.
Bad news is when I published to the Chrome web store I ran into issues with the comments I added into my manifest.json
. I didn't have these issues when I published to Firefox. If you want to comment your manifest.json
you'll need to remove them when you publish to Chrome.
Icons
First up is figuring out a way to add icons. To start I'm going to create my initial manifest.json
. Here's what I used to start out:
manifest.json
{
"author": "you already know it's ya boi",
"manifest_version": 2,
"name": "sleepy-time",
"version": "1.0",
"description": "get that good sleepy-time you need",
}
If your wondering about any of the keys then the manifest.json
reference above can give you more information.
To add Icons we simply need some images of the appropriate size, and to link to them in our manifest.json
. Here's what that looks like:
"icons": {
"48": "icons/trust-nobody-v2-48.jpg"
},
The 48 here is the size of the icon (48px X 48px) and icons/trust-nobody-v2-48.jpg
is the location of the icon relative to manifest.json
Sidebars, popups, and options pages
Next up is figuring out a way to set the bed time. A UI seems like a natural place to put this so lets see how I can add one. The docs say there are 3 options
- Sidebar - A pane that is displayed at the left-hand side of the browser window, next to the web page
- Popup - A dialog that you can display when the user clicks on a toolbar button or address bar button
- Option - A page that's shown when the user accesses your add-on's preferences in the browser's native add-ons manager
I'm going to go with a popup as I'm not too picky about how I set my bed time. Here's what the docs say about creating a popup:
The popup is specified as an HTML file, which can include CSS and JavaScript files, as a normal web page does. ....
The HTML file is included in the extension and specified as part of the browser_action or page_action key by "default_popup" in the manifest.json: - Popups
Looks like to get a popup I only need to add an HTML file, update manifest.json
with a browser_action
property, and then specify the HTML file in the default_popup
key under it. Here's what that looks like:
"browser_action": {
"default_popup": "popup.html"
}
Here's what my HTML looks like:
popup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="mypop.js"></script>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div>Hello popup</div>
<button id="my-button" onclick="logSome()">Click this for something</button>
</body>
</html>
I've also added a JS file that looks like this:
popup.js
function logSome() {
console.log('clicked a button. Nice!');
}
So I click my extension and the popup, well pops up. I click my log button and it doesn't log... I look in the console and I see
Content Security Policy: The page’s settings blocked the loading of a resource at inline (“script-src”).
Fuck. CSP. If your not familiar with CSP I'd recommend looking at this and this. Basically CSP stops you from doing things that you might normally e.g. onclick="logSome()"
in the good name of security. In this case the default CSP policy is blocking me from executing inline Javascript. In order to satisfy CSP I need to remove my inline Javascript and do everything in popup.js
and it'll work. That code looks like:
popup.js
function logSome() {
console.log('clicked a button. Nice!');
}
document.addEventListener('DOMContentLoaded', function () {
var clickyButton = document.querySelector('#my-button');
clickyButton.addEventListener('click', logSomething);
});
After these changes my log button works!
Storing Data
I've got my UI up, but I don't have any way to store the bed time value or get it so I can use it in my extension. To fix this we'll take our first look at using the Web Extensions API.
The Web Extensions API gives extensions superpowers. Basically it allows extensions to do things that normal web applications can't. In some cases it's necessary to ask for permission in order to use specific APIs. How do you ask for permissions you might ask? If you guessed manifest.json
you is right. We'll see how that works in a bit. Finally, all APIs are accessed through the browser
namespace and we'll see an example of this as well.
There's a lot of ways to store data, but I'm going to use the storage
API, which will let me store and retrieve data in my extension. So I go to the docs as one does. I find and look through the storage docs to understand how this API works, and there's a couple things that jump out at me.
- There's three types of storage, but I'm interested in one called
sync
.sync
will let me store and retrieve data across all the browsers that I'm logged into. I want this so I can set my bed time across different computers for example. The storage docs have more info on storage types if you'd like to check it out. -
sync
provides me with two methods to get and retrieve data:storage.sync.get
andstorage.sync.set
- > To use this API you need to include the "storage" permission in your manifest.json file. - storage docs
- > Note that the implementation of storage.sync in Firefox relies on the Add-on ID. If you use storage.sync, you must set an ID for your extension using the browser_specific_settings manifest.json key. - storage docs
Let's put all this together now. I'll start by requesting the storage permission, and setting an Add-on ID. Here's what that looks like:
manifest.json
"permissions":[
"storage"
],
"browser_specific_settings": {
"gecko": {
"id": "myID@ID.id"
}
},
browser specific settings docs - I didn't really touch on this, but here's more info if your interested.
permissions info - more info on permissions
Now I have the correct permissions and I've set an Add-on ID. Now I'm free to use the storage API. I'm going to replace the code I used for logging with the new storage code. Here's what that looks like:
mypop.js
function setBlockTime(blockTime) {
var blockTimeEle = document.querySelector('#block-time');
if (blockTime.blockTime) {
blockTimeEle.value = blockTime.blockTime;
}
}
document.addEventListener('DOMContentLoaded', function () {
// populate the form if a value exists in the store
browser.storage.sync.get('blockTime').then(setBlockTime);
var form = document.querySelector('#settings-form');
form.addEventListener('submit', (event) => {
event.preventDefault();
let timeToBlock = document.getElementById('block-time').value;
browser.storage.sync.set({
"blockTime": timeToBlock,
});
});
});
Here's what the update HTML looks like:
popup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="popup.js"></script>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div>Blacklist settings</div>
<form id="settings-form">
<label for="">Sleep Time</label>
<input id="block-time" name="" type="text" value=""/>
<button type="submit">set sleep time</button>
</form>
</body>
</html>
storage
is just one of many APIs available in the Web Extensions API. To see everything it offers you can look at the Javascript API listings in the Javascript APIs page. There's ways to get at tabs, windows, HTTP requests, and much more.
Alright I've got a way to store and retrieve data. To put the finishing touches on this now I just need to block pages I visit past my bed time.
Content Scripts
To finish off let's see how to add content scripts. Again I go to the one thing I consider holy the docs. In particular I go to the content scripts docs
Here's what they tell me about content scripts
A content script is a part of your extension that runs in the context of a particular web page...
Just like the scripts loaded by normal web pages, content scripts can read and modify the content of their pages using the standard DOM APIs...
Content scripts can only access a small subset of the WebExtension APIs, but they can communicate with background scripts using a messaging system, and thereby indirectly access the WebExtension APIs.
We aren't going to talk about background scripts here, but they're very useful for certain applications, and I suggest looking into them if your building an application of your own. Sadly content scrips aren't allowed full access to the Web Extensions API, but they are allowed to use storage
.
There are 3 ways that content scripts can be loaded.
- At install time, into pages that match URL patterns - Using the content_scripts key in your manifest.json, you can ask the browser to load a content script whenever the browser loads a page whose URL matches a given pattern.
- At runtime, into pages that match URL patterns - Using the contentScripts API...
- At runtime, into specific tabs - Using the tabs.executeScript() API...
I don't have a need for the second or third ways here so I'm going to focus on the first way. In this scheme I just need to update manifest.json
with a content script and a URL pattern. Here's what that looks like:
manifest.json
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["block-website.js"]
}
]
manifest.json - content scripts
The matches
key is what specifics the URL pattern. In my case I have a catchall. Here's more info on match patterns.
All that's left to do is read the bed time value, check it against the current time and then block the page if it's past bed time. Simple enough. Here's the code:
block-website.js
function getCurrentHours() {
let date = new Date();
return date.getHours();
}
function blockPage(blockTime){
if(blockTime && blockTime.blockTime && getCurrentHours() >= blockTime.blockTime){
document.body.innerHTML = "<div> Sorry you can't look at this website it's past bed time! </div>";
}
}
browser.storage.sync.get("blockTime").then(blockPage);
Chapter 4: Compatibility with Chrome
Everything done so far has been for Firefox. I knew at the start I'd have to do some work to port it over to Chrome, but it's something I should have looked more into before writing code. Lets look at the trouble this got me into.
Obviously if I want to publish this on the Chrome store I have to get it working on Chrome. So I loaded the extension into Chrome and got errors as expected. Lucky for me Mozilla wrote a great article explaining the incompatibilities between FireFox and Chrome: Firefox and Chrome incompatibilities. This was one of the first places I looked to when trying to get things running in Chrome. Here are the changes I had to make:
- The
browser
namespace doesn't exist in Chrome. All the code I wrote using that namespace needed to be changed tochrome
. E.g.browser.storage.sync.get...
would becomechrome.storage.sync.get...
- The Web Extensions API is async. Firefox handles this with promises, but Chrome does so with callbacks. All the code that looked like:
// promise based
browser.storage.sync.get('blockTime').then(setBlockTime);
needed to become
// callback based
chrome.storage.sync.get('blockTime', setBlockTime);
- I didn't run into this, but it's worth mentioning. There are other small inconsistencies between the APIs. In general they're mostly the same, but it's might be worth developing extensions in tandem to help avoid headaches later down the road. One example of these inconstancies can be seen in the
tabs.create
method. It takes an object calledcreateProperites
, but what properties that object can have differs on the browser.
It would have been better to develop the extension on Chrome and port it to Firefox and heres why:
As a porting aid, the Firefox implementation of WebExtensions supports chrome, using callbacks, as well as browser, using promises.
This means that many Chrome extensions will just work in Firefox without any changes.
This isn't true for all browsers, but it is for Chrome and Firefox. I think Chrome will eventually use browser
since that's what the standard being developed specifies, but for now this is what we got. Here's more info on the spec/standard
Once I made these changes to the extension it worked in Chrome. For more information on the differences checkout the Firefox and Chrome incompatibilities article linked above.
Chapter 5: Packaging & Publishing
Alright I've got a web extension that I'll actually use, and will help me get my sleep schedule back in order. So now what? How do I publish it so other people can use it? Lets take a look at how we can publish an extension on Firefox and Chrome.
in a nutshell all publishing requires is packaging your extension and then submitting it to the store.
Packaging your application
I've got my code in a place that I like so the next step is to package the extension. All that's needed is to create a ZIP archive of all the files that make up the extension. I create a ZIP of the following files:
manifest.json
icons/trust-nobody-v2-48.png
popup.html
popup.js
bock-sites.js
Mozilla also has a tool called web-ext-build
that can be use for this. I didn't bother looking into it, because creating a ZIP was so easy. Thought it was worth mentioning though. More info on packaging your app and specific directions on how to do it can be found here.
Publishing to Firefox web store (AMO)
Once the extension is packaged it's almost time to submit it. Mozilla has a step by step guide on submitting here. I'll summarize the points in it, because it really just came down to these things for me:
- Look over the Add-on Policies and Developer Agreement. If you violate these your extension could be rejected or taken down.
- If you don't have an AMO account you'll need to create one.
- If you have an account head over to the "add-ons-developer-hub". This is where you can submit the extension.
- Follow the flow that AMO has setup for you to submit. From here it's just about filling out a few forms.
Once you submit you'll get an email notifying you of your submission and that it's being reviewed. If your exentsion is accepted then it'll be on the store for other people to download it! I submitted my application on Wednesday and it was accepted Thursday. Less than a day to approve my application. Overall the process was pretty easy. Package your app, create an add-ons account, fill out some forms, submit and wait for approval.
Publish to the Chrome web store
Chromes process is very similar to Mozillas. Just like Mozilla they have a step by step guide on submitting you can follow here. Again, the process isn't to hard so I'll summarize what it came down to for me:
- Again the first thing you'll need is a packaged version of your extension
- If you don't have a developer account create one.
- If you have a developer account then register as a developer with the Chrome web store. It'll cost you \$5 to do so 😭.
- Use the Chrome developer dashboard to upload your package.
- Finally fill out the necessary information and forms. Chrome requires you have an Icon and screenshot of your extension.
I submitted on Oct 29th, but still haven't heard back. My status says pending review
so it might take a while to get done cause of Covid n'all. We'll see how long it takes for them to accept my extension.
Chapter 6: The End Dawg
There it is. An extension from start to finish, and enough information to give you a solid foundation to build your own extensions. I didn't create my whole extension in this article, but I'm working on it! Using what I've built so far has actually helped me avoid staying on the internet past my bed time. Obviously there's more things I want to add, but one thing at a time. If you think having something block your browser after a certain time might be beneficial to you then you can checkout these links for the extension:
- Here for Firefox
- Here for Chrome - As I mentioned they haven't accepted my submission, but I'll update this page when it's approved.
I'm currently working on adding the other features I described at the beginning of the article, and I'll update the extension as I get to them.
To stay up to date with writings like these go checkout Learning Compuations
I already said there it is, but there it is. A practical guide to web extensions. All you need to do from here is expand on the foundation that you've built in web extension land. Now go build an extension and publish it! Get building and see you next time!
Top comments (0)