Building a Bookmark-esque Chrome Extension

In this blog post I will touch on how I created a bookmark style Chrome Extension that persists links to a database using a Rails API.

I will cover the basics of setting up the skeleton for a Chrome Extension and get into how I used the Chrome API and JavaScript to build this feature.

Step I: Manifest.json

The first thing needed to set up a Chrome Extension is the manifest.json file. Every Chrome App or Extension needs a manifest file, which describes the app. Create a new directory and inside of it create a new file called manifest.json.

{
  "manifest_version": 2,

  "name": "Janus",
  "description": "This extension will let you save bookmarks to your Janus Dashboard",
  "version": "1.0",

   "browser_action": {
   "default_icon": "ChromeJanus.png",
   "default_title": "Click to add to Janus Dashboard"
},

   "background": {
   "scripts": ["background.js"]
},
    "content_scripts": [
   {
     "matches": [
       "<all_urls>"
     ],
      "js": ["storage.js"]
   }
 ],
  "permissions": [
     "contentSettings",
     "system.storage",
     "cookies",
     "tabs",
     "http://*/*"
   ]
}
Extension Basics

The first section of this json file describe the basics of your application. In my case, the name of the extension is called Janus, and I have a basic description and version. The key browser_action not only allows our extension to show up in the toolbar in the Chrome Browser, but makes sense for the purposes of our extension, which is being able to store any link as a bookmark. Chrome recommends using browser actions for features that make sense for most websites. You could also use page actions for features that apply to a few pages. In order to get your icon to show up, simply add the path to the default_icon key in your manifest file.

Background and Content Scripts

The background and content_scripts section of the manifest file will make more sense later. For now, add background.js to the backgrounds key and add storage.js to your content scripts. These two files are where we will build out all of the logic of our extension.

Permissions

This section of the manifest file is important and allows you to access certain information from a user and their browser. For this example, we need to access the users cookies, system storage, tabs, and content. You can check out all of the permissions that you can declare here. Only declare what you need, and the user of your extension will be notified of these declarations before installation ;).

Okay, on to the fun stuff.

Step II: Background.js

This file is going to contain the event listener for a click action and send data to our content scripts. First, make sure that you add a file to your directory named background.js and that this file is included in your manifest.

In this file we are going to add an onClicked listener.

chrome.browserAction.onClicked.addListener(function(tab))

this creates the browser actions that will respond when the user clicks the icon to your extension. The purpose of this function, in broad strokes, is to: capture the link data, send the data to the content script, and catch any errors.

Link Query

The Chrome API allows you to access the current tab by using its tabs.query method. In the browser action, add the following:

chrome.browserAction.onClicked.addListener(function(tab){ 
  chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
var activeTab = tabs[0];
  });
});

This particular function takes in two arguments, an object containing options, and a callback function. The options we pass in are active:true and currentWindow:true, because the feature we are trying to build requires having access to the tab that user is currently viewing when the click event occurs.

Message Passing

The next step that needs to be taken is passing this tab information back to our content script. Here is what the chrome documentation has to say about message passing.

Since content scripts run in the context of a web page and not the extension, they often need some way of communicating with the rest of the extension.

Add the following code to the event listener:

chrome.tabs.sendMessage(activeTab.id, {"message": "clicked_browser_action"}, function(response){
  var lastError = chrome.runtime.lastError;
    if(lastError){
      console.log(lastError.message);
    } else {
      console.log(response);
  }
});

Here we are sending the information we got from the click event and passing it to your content scripts inside of the extension. Because we might eventually have more than one content script running in our extension we are also passing a custom key:value pair containing a message. We will use {"message": "clicked_browser_action"} for the logic in our content script. The response portion of the section is for debugging. If there are any errors, they will be logged into your console.

The entirety of your onClicked listener should look like this:

chrome.browserAction.onClicked.addListener(function(tab) {
 chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
   var activeTab = tabs[0];
   chrome.tabs.sendMessage(activeTab.id, {"message": "clicked_browser_action"}, function(response){
  var lastError = chrome.runtime.lastError;
    if(lastError){
      console.log(lastError.message);
    } else {
      console.log(response);
     }
   });
  });
});

Next will be adding the code in our storage.js content script that listens for the message that this function sends.

Step II: Storage.js

The name of this file is completely arbitrary.

The next thing we want to do is add an onMessage listener in our content script that will handle what to do next. In broad strokes it will: listen for a message, package up the data, and send a message to the background file that will send this data to our API.

Here is what this will look like:

chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {

var userToken =  localStorage.getItem('ember_simple_auth:session');
if( request.message === "clicked_browser_action") {
  if(sender !== undefined){
  chrome.runtime.sendMessage({
           method: "POST",
           action: "xhttp",
           url: "http://janus-api.herokuapp.com/api/v1/bookmarks/chrome",
           data: sender,
           token: userToken.match(/(?:"token":")(\w+)(?:","email")/)[1]
       }, function(responseText) {
           console.log(responseText);
    });
  } else {
    alert("the url you tried to save was invalid, please try again.");
  }
 }
 }
);

I know, it's a lot, but most of this should already look a little familiar or will shortly. Disregard the userToken variable for the purposes of this post. Part II of this post will show how I grab a session token in order to verify a user in the backend.

The onMessage event will be triggered whenever a message is sent from an extension process or content script. When using the Chrome API function for an onMessage listener, it must be structured in this way: chrome.runtime.onMessage.addListener(function(request, sender, sendResponse)

Request is up to the developer, but this will contain any message or custom data sent along with the message from the background file or a content script.

Sender is an object that contains data specific to whatever sent the message, including the tab information that we want to store.

Sender Reponse is the function to call (at most once) when you have a response. But for the purposes of our extension we will not be using it, but I am sure you can find a clever way to use it. Find more information about it here.

Message Check

As I mentioned earlier, there may be a time where our extension will grow and will contain multiple content scripts and listeners, so we need a way for the right function to respond. This is what the first conditional statement is doing in this particular listener: if( request.message === "clicked_browser_action"). Recall that the message that got us to this point contained a message set equal to clicked_browser_action. The second conditional makes sure that the sender object was passed along with the message, and will prevent our next message from sending if it is undefined.

Message Numero Dos

The next part of this function is to send the right information to another listener, which will then send the url to our Rails API. The message that we send this time is a little more complex because we need to set up the XMLHttpRequest. Since we are persisting data, we are using a POST request and our action key will act similar to our message key, which will trigger the right listener. The url is set equal to the url that we will be sending the request to. In my case, the endpoint to a Rails API. Data will contain the information from the first message, which contains the link that we want to bookmark.

Step III: Background.js, again

We're going, going, back, back, to the background, background.

The next step is to add another onMessage listener that will create the XHTTP request.

The code:

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
if( request.action === "xhttp" ) {
    var title = "Bookmark saved from JanusChrome: " + sender.url;
    var xhttp = new XMLHttpRequest();
    xhttp.open(request.method, request.url);
    xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
    xhttp.send(JSON.stringify({bookmark:
      {link: sender.url,
        title: title},
      user:{token: request.token}
    }));
   }
  }
);

For the most part, we are receiving and checking this message in the same way that we did in the content script. The first conditional checks if the message or action came from the right place, in our case request.action = "xhttp".

In order to make a cross-site request we are going to use XMLHttpRequest. Here we open a request with the method that was received from the content scripts message, and the end point from the same message. You can learn more about the types of headers and the type of data that you need to send to your API here. In my case, the API needs JSON instead of an object, so I use JSON.stringify to send the data back in a way that my backend can interpret.
uh, uh, mind power

Until the Next Episode

What you do from here is specific to the kind of backend that you are using and what you plan on doing with this feature. I hope that this post has provided you with some insight on how Chrome Extensions work, and some of the powerful things you can do with it's flexibility. I will be writing a follow up post to show how I verify a user to store the bookmark in our application's database.

Could not have done it without these:

Resources

Chrome Extension Documentation

On Bookmarks

On Grabbing URLs

XHTTP Request

Show Comments
comments powered by Disqus