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 programs there where for the same thing. It made me think why don't I create a tailor made solution for my own problem. So I did. I made (truth be told I'm not completely done) a browser extension to fix my problem of not being able to stop myself from watching brainless entertainment.
In this weeks edition of learning computations we're going to looking at building web extensions for Chrome and Firefox!
Here's what we'll do:
Alright let's start by defining what the web extension should do. The extension I want should let me
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 is manifest.json
? They don't say, but we'll answer this question in a bit.
manifest.json
is content_scripts
I'll let the tutorial explain thisThe 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.
borderify.js
by adding it to content_scripts
in manifest.json
you add some JS to borderify.js
to make the border of mozilla.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.
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.
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.
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
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
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!
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.
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
and storage.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.
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.
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);
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:
browser
namespace doesn't exist in Chrome. All the code I wrote using that namespace needed to be changed to chrome
. E.g. browser.storage.sync.get...
would become chrome.storage.sync.get...
// promise based
browser.storage.sync.get('blockTime').then(setBlockTime);
needed to become
// callback based
chrome.storage.sync.get('blockTime', setBlockTime);
tabs.create
method. It takes an object called createProperites
, 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.
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.
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.
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:
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.
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:
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.
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:
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.
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!