This follows on to last Wednesday’s post. To recap, I have recently been investigating some possible projects that would benefit from having a simple browser extension to pass along real-time data on the user’s actions. It’s simple enough, but has enough detail to make a viable post…or two.

Plugs

In this case, our extension is going to report every visited URL to a configurable remote address. The URL Rat extension, by last week’s post, was able to record every visited URL and send that information to a hard-coded URL. So far, so good. I’ll assume that you read that, since it’s short.

But now, we need to make that user-configurable, so we need to work with the browser storage.

Setting Configuration

First, we need to set up the extension to allow for some configuration work, which means adding some elements to manifest.json.

"options_ui": {
  "page": "popup/configure.html",
  "browser_style": true
},
"browser_action": {
  "default_icon": "icons/urlrat32.png",
  "default_title": "URL Rat"
},
"background": {
  "scripts": [
    "background.js"
  ]
}

Each of the three does its small part.

Browser Action

The browser_action element creates a toolbar button, in this case with an additional image that fits the button profile. All things considered, it’s pretty boring. Name and image.

The handlers go in…

Background

In the background.scripts element, we list the files containing the toolbar button’s event handlers. In this case, we just have one file, since we’re only handling one click. But if we had multiple buttons and/or had multiple features, we might consider separating that code into multiple files, listing them there.

I’ll talk about our click-handler in a bit.

Configuration Interface

Finally, the options_ui element tells the browser where to find the page with the configuration controls. In my case, I created a popup folder—even though it doesn’t actually pop up, but I initially considered that approach and never changed the name—where I’ve dumped all the code related to that options page.

These three items guide most of the work from here.

Event Handlers

As mentioned, we keep the toolbar handlers in a script that the browser runs in the background. In our case, we don’t have much to do, so it’s just this.

function handleClick() {
  browser.runtime.openOptionsPage();
}
browser.browserAction.onClicked.addListener(handleClick);

Just…open the options page when we hear that the button has been clicked.

Honestly, we don’t even need this, since the options page is going to be accessible from the list of browser extensions, but we might want to add features later and I wanted to make the extension visible, since it’s such a terrible idea to run it.

Configuration

I’ll spare you the boring HTML and CSS for URL Rat’s options page. It’s a form with controls. If you’re not familiar with how those work, I’m not being dismissive when I say that you can find a better explanation than I could write here in just about any HTML tutorial.

Instead, we’ll just focus on the JavaScript code, since that’s the part that interacts with the browser storage. There are a few pieces.

function saveOptions(e) {
  browser.storage.sync.set({
    dest: document.querySelector('#dest').value,
    isActive: document.querySelector('#on').checked.toString()
  });
  e.preventDefault();
}
document.querySelector('form').addEventListener('submit', saveOptions);

Save options will take our two options (a URL and an on/off setting) and push them to browser.storage.sync, where we can pick them up later. I slightly reorganized the file for clarity, but the call happens when our options form is submitted.

In other words, the user clicks “Save” and the settings are stored in the browser.

function restoreOptions() {
  var storageItem = browser.storage.managed.get();
  storageItem.then((res) => {
    setOptions(res);
  });

  var gettingItem = browser.storage.sync.get();
  gettingItem.then((res) => {
    setOptions(res);
  });
}
document.addEventListener('DOMContentLoaded', restoreOptions);

When we open the options page, we want the controls to reflect the saved data. So, restoreOptions() takes two steps, instead of just the one involved in saving.

The first step is to pull the default options out of the browser.storage.managed area. After pushing that information to the options page, we check browser.storage.sync to see if the user has saved anything and, if so, set those options and overwrite the managed version.

The setOptions() function isn’t worth showing here, just a utility function to avoid duplicating some updates to the controls. You can find it in the repository along with the HTML and CSS, if you need it.

Wait, What Default Values?

You noticed that there’s no way to populate the manage storage, too, right? Getting this to work was probably the least-appealing part of the process for me.

This may only be for Firefox, but we need a JSON file to get information there. You might remember that the manifest.json file included a browser_specific_settings.gecko.id element. Implausible as it sounds, we use that ID to identify a new JSON file that holds our default values. In this case, it’s literally named urlrat@example.com.json, because our identifier is that imaginary e-mail address. And it looks like the following.

{
  "name": "urlrat@example.com",
  "description": "ignored",
  "type": "storage",
  "data":
  {
    "dest": "http://localhost:8080/",
    "isActive": "true"
  }
}

Copy, link, or move that file—depending on your preferences and how often you expect to update and keep the file around—to your ~/.mozilla/managed-storage/ folder, and you have now initialized your browser’s Managed Storage.

I did warn you that it wasn’t appealing.

Use the Configuration

From here, we should already know what to do. The main extension code url-rat.js, for this example, now needs to replace its hard-coded values with values from the browser’s storage. I took the following approach.

browser.storage.managed
  .get()
  .then((managed) => {
    browser.storage.sync
      .get()
      .then((local) => {
        var store = Object.assign(managed, local);

        // Assign the values in the "store" variable to individual
        // variables already used by the extension, and then put the
        // original extension code here.
      });
  });

I nested the then() handlers to make sure we have the data, even though that’s probably unnecessary, and then used Object.assign() to merge the two configuration objects into one.

Like the comment says, I should now have store.dest with the saved URL and store.isActive to decide whether to send the current URL to the server.

Overall

All things considered, if we ignore the managed storage shenanigans, the development process seems smooth. The only real snag I hit was in not realizing that the sync and managed storage areas were different, breaking the configuration page.

Also, the configuration page is ugly.

Other than minor issues like that, though, the fact that it only took a couple of hours in total to hack out a fully functional browser extension that performs a defined task starts to make this look appealing for certain kinds of projects.

Next Week

Normally, I wouldn’t announce the tech tip in advance, since I never know what they’ll be until I’m writing myself notes on something I did. But it’s a safe bet that I’ll be looking at what it takes to write an extension for Visual Studio Code…or VSCodium, as the case may be.


Credits: The header image is Untitled by an anonymous PxHere user, made available under the terms of the CC0 1.0 Universal Public Domain Dedication.