Creating My First Google Chrome Extension - Part 1

PART 1 | PART 2 | PART 3

Contents

Something’s been nagging me…

I’ve been on this kick recently, trying to untangle myself from my reliance on all things Google. Their services are great, and I don’t mean the following to be a rant, but a few things worry me…

egg-basket-broken

Should I put all my eggs in one basket?

It seems unlikely, but if Google goes down or gets hacked, or sees fit to freeze my account for some reason, I’m screwed from every angle. I lose my email, calendar, documents and photos, contacts, bookmarks, passwords… everything. Wisdom seems to dictate spreading my online footprint around a bit, to limit the damage from any one provider.

Can I get quality help when I really need it?

Google is a goliath with millions of users. Most of us pay nothing, so what level of service should we reasonably expect?

Someone I know got a phone with Android on it and had manually entered over a hundred contacts. I suggested they create a Google account and sync the phone to it, but the first “sync” operation wiped their local contacts. Poof, gone. We searched for some help online but found only other frustrated users. They have forums and docs, but I couldn’t find an actual help desk page. I think Google relies on the fact that for 99.999% of users, everything works well 99.999% of the time. But woe to the person who has a real problem and needs to find real help.

If you’re not paying, yadda yadda yadda…

Despite arguments against it, if you’re not paying for a product you really are the product. Google consumes everything you give them, and serves you with ads that you might be interested in. Some might argue that’s a good thing, but it creeps me out. I’d prefer to find a service that’s inexpensive, or has a feature-lite free version supported by a feature-rich pay version, while explicitly promising not to use your information for further profit.

Do one thing, and do it well

With all that in mind, I’ve been looking at services with a more singular focus, such as Pinboard. Written by one person to do one thing well, Pinboard solves a problem in the popular browsers – they suck at managing bookmarks. Beyond a few dozen it’s tough to keep them organized and find the exact one you’re looking for.

Pinboard is a fast, no-nonsense bookmarking site for people who value privacy and speed. There are no ads and no third-party tracking. You pay a few bucks a year, and that’s it.

Beyond that, it makes organization and finding what you need easy too, and even provides an API through which programmers can access its services from their own projects.

The Pinboard API, and using it in a Chrome extension, is what I want to (begin to) cover with this post.

But first…

Creating your first Chrome extension

It all starts with a manifest.json file. That’s where you set the location of your scripts and tell it which files to load first, request permissions from the user, set a name and description for your extension, and more. It sets everything else in motion.

Google has excellent documentation.

Imitation is the sincerest form of flattery

Next, you may want to see what others are doing. Pick a relatively straight-forward example like the ShowPassword extension, or any other extension you’re currently using and really think is good, and take a peek into it. Use the Chrome Extension Source Viewer to inspect other extensions (including itself).

Check out the manifest.json file and various html files, js scripts and other resources like images. Install the extension and compare various actions in it with what you see in the files, so you can see how it affects the user experience.

  • See how it specifies an “options” page and includes a reference to the “showPassword.js” script?
  • See how it specifies the icons, which are displayed in the main toolbar and in the “Extensions” section?
  • See how it requests permissions, in this case storage so it can save your preferences and sync them?

extension-source-viewer-source

Testing it out

When it comes time to test out your own extension, Chrome makes it dead simple.

Open the “Extensions” page, like you’re going to look for an extension to install, but then select the “Developer mode” checkbox in the upper-right. That allows you to load your extension from disk. If there’s anything wrong with your manifest.json file, or you’ve pointed to a resource that’s missing or inaccessible, it’ll let you know.

chrome-dev-mode-warnings

Fix your mistakes, if any, and click the “Reload” link.

Creating MY first Chrome extension

Once you’ve got the basics down, it’s time to start working on your idea, whatever that may be.

For me, the idea was to use the Pinboard API to pull a subset of bookmarks (based on tags) into the Chrome bookmarks bar. Like I said, I’m trying to be less dependent on the myriad services Chrome provides, including sync’ing bookmarks. I’ve got my bookmarks stored in Pinboard where I can access them from anywhere, but it’d be convenient to have some of the most-used ones available right from Chrome.

It might feel like a scramble up a steep hill at first, but try to have fun and play around. Initially, I just set my manifest.json file to show a popup page when the icon was clicked. I didn’t intend to keep it, but here’s what it looked like. I created my file based off what I found in the ShowPassword extension I was talking about earlier – that’s why the fields below look like that one.

first-extension-attempt-1

From there, I wanted to see if I could create an actual bookmark, which meant getting familiar with the chrome.bookmarks API. Just like Pinboard has an API to abstract away the hidden complexities and expose a simple interface for developers to use, so Chrome has its own API that does much the same thing.

Generating a bookmark

Here was my first attempt at creating a bookmark.

The first function is a loop that looks for the Bookmarks Bar (the same one that shows up at the top of Chrome) under the “root” folder, which also contains “Other Bookmarks” and “Mobile Bookmarks”. Since the only guarantee we have is that the root folder id is “0”, I have to scroll through its immediate children to make sure bookmarks are being created in the right place. On my dev machine, the Bookmarks Bar, Other Bookmarks and Mobile Bookmarks happen to be “1”, “2” and “3”, but I could find nothing that guarantees that.

findBookmarksBar('0');  // root folder

function findBookmarksBar(id) {  
    chrome.bookmarks.getChildren(id, function(children) {
        for (var i = 0; i < children.length; i++) {
            var bookmark = children[i];
            console.debug(bookmark.title);
            if (bookmark.title == 'Bookmarks Bar') {
                createTestBookmark(bookmark.id, 'Test Bookmark', 'http://code.google.com/chrome/extensions');
                break;
            }
        }
    });
}

function createTestBookmark(parentId, title, url) {  
    chrome.bookmarks.create({'parentId': parentId,
                             'title': title,
                             'url': url},
                            function(newBookmark) {
                                console.log("added bookmark: " + newBookmark.title);
                            });
}

Once I’ve found it, the second function above uses the chrome.bookmarks API to create the test bookmark seen below.

first-extension-create-first-bookmark

Let me back up a second. Before the above will work, I had to specify the “bookmarks” permission in the manifest.json file. That way, the user is notified that the extensions wants to mess with the bookmarks in some way. If I didn’t include it, the above code would throw an error like this instead.

first-extension-missing-permission

Generating a bookmark inside of a folder

That’s all well and good, but one bookmark won’t do much. I’ll want to create many bookmarks, and organize them inside folders. That’s another layer of difficulty, since creating folders/bookmarks is asynchronous. You can’t just wait for one to complete and grab the generated ID.

In other words, this code will not work as expected. The value of the created folder will not be assigned to id and returned to the calling function, because it’s not available yet when return id; is executed. That makes it more difficult to build a hierarchy of bookmarks.

function createFolder(parentId, title) {  
    var id;
    chrome.bookmarks.create({'parentId': parentId,
                             'title': title},
                            function(newFolder) {
                                id = newFolder.id;
                                console.log("added folder: " + newFolder.title);
                            });
    return id;
}

Instead, whatever you want to do after the fact has to be placed inside the callback function, which only executes once the current bookmark is created. Here, createTestBookmark is another function, which will create a bookmark when the folder is created.

function createTestFolder(parentId, title) {  
    chrome.bookmarks.create({'parentId': parentId,
                             'title': title},
                            function(newFolder) {
                                createTestBookmark(newFolder.id, 'Test Bookmark', 'http://code.google.com/chrome/extensions');
                                console.log("added folder: " + newFolder.title);
                            });
}

first-extension-bookmark-in-folder

Generating many bookmarks (recursion)

That takes care of a single bookmark nested in a folder, but I want to create an indeterminate number of nested folders, each of which may have one or more bookmarks inside them. This smells like recursion…

My first thought was to create some sort of structure representing each bookmark or folder, which I could build up into a tree and then pass into the function which could traverse the tree, recursively creating each item. Here’s my first pass. It builds up a tree of nested “nodes”, which would presumably come from Pinboard somehow, and then iterates through them to build the bookmarks structure in Chrome.

var googBookmarks = [  
    new node('Calendar', 'http://calendar.google.com'),
    new node('Email', 'https://www.gmail.com'),
    new node('Search', 'http://www.google.com')
];
var techBookmarks = [  
    new node('Microsoft', 'http://www.microsoft.com'),
    new node('Google', null, googBookmarks),
    new node('Mozilla', 'http://developer.mozilla.org')
];
var topLevel = [  
    new node('Tech', null, techBookmarks),
    new node('Trello', 'https://www.trello.com'),
    new node('Hmm', 'http://www.grantwinney.com')
];
var all = new node('Ignore', null, topLevel);

populateBookmarks(iterateNodes, all);


function populateBookmarks(func, nodes) {  
    chrome.bookmarks.getChildren("0", function(children) {
        for (var i = 0; i < children.length; i++) {
            if (children[i].title == 'Bookmarks Bar') {
                func(nodes, children[i].id);
                break;
            }
        }
    });
}

function iterateNodes(parentNode, parentId) {  
    for (var i = 0; i < parentNode.nodes.length; i++) {
        createPageOrFolder(parentNode.nodes[i], parentId);
    }
}

function createPageOrFolder(node, parentId) {  
    chrome.bookmarks.create({'parentId': parentId,
                             'title': node.title,
                             'url': node.url},
                            function(newPageOrFolder) {
                                writeLogMessage(newPageOrFolder, parentId);
                                if (node.isFolder) {
                                    iterateNodes(node, newPageOrFolder.id)
                                }
                            });
}

function node(title, url, nodes=[]) {  
    this.id = "-1";
    this.title = title;
    this.url = url;
    this.isFolder = (url == null);
    this.nodes = nodes;
}

function writeLogMessage(node, parentId) {  
    console.log("Added page or folder '" + node.title + "' (" + node.id +
                ") to parent folder id " + parentId + ".");
}

It ended up doing exactly what I had envisioned, although I’m not sure it’ll fit my needs yet. Notice in the console log messages that some of the bookmarks (pages) are children of other folders.

first-extension-implementing-recursion

Querying the Pinboard API

Now that I could create a folder/bookmark structure recursively, it was time to get bookmarks from Pinboard instead of using my hard-coded values. That means delving into the Pinboard API to authenticate with them and read data.

Authenticating with Pinboard

Straight from the Pinboard API documentation, the recommended API URL is this, where “username:TOKEN” is an authentication token that can be found on a user’s settings page.

https://api.pinboard.in/v1/method?auth_token=user:NNNNNN

Unfortunately, no matter what I tried I kept getting this:

XMLHttpRequest cannot load https://api.pinboard.in/v1/user/apitoken?format=json&authtoken=:. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘chrome-extension://fpjohoehdnepioimfgeaefoijahifgll’ is therefore not allowed access.

This is actually a good thing, as it prevents certain types of cross-site-script attacks. A website can state in a header that it trusts a site it’s trying to load resources from, and that other site has to include an “access-control-allow-origin” flag in its header that has the original website in it. In this case though, I’m attempting to load resources from Pinboard’s site but it doesn’t reference my site so my access is disallowed.

This took the longest time to troubleshoot up to this point but, like so many hard problems, the answer was simple.

But first, I read a load of forum posts. I messed around with jQuery and AngularJS and ajax, and read up on XSS and CORS. I posted to Google Groups and sent a support ticket but didn’t get a response in either channel (more than a little disappointing honestly – I hope Maciej responds if I ever have a real problem).

I finally took to Twitter and received a response from someone else who had created a bookmarklet called Pincushion. Unfortunately, that bookmarklet submits the auth token to another site, and I’m not sure what it’s doing to actually authenticate with Pinboard, so it didn’t help me too much. What it *did *prove to me though is that the API was working, because the bookmarklet worked even though I was logged out of Pinboard, so that was something…

In the end, after a ton of searching, I found the answer in a Stack Overflow post. The two answers have some good suggestions, and you may want to read them anyway. But they both linked to a Referencing External Resources doc that had the exact answer I needed. The manifest.json file has to request permission to the external site! Presumably the user will be notified of this requested permission just like any other permissions.

Referencing external resources

The Content Security Policy used by apps disallows the use of many kinds of remote URLs, so you can’t directly reference external images, stylesheets, or fonts from an app page. Instead, you can use use cross-origin XMLHttpRequests to fetch these resources, and then serve them via blob: URLs.

Manifest requirement

To be able to do cross-origin XMLHttpRequests, you’ll need to add a permission for the remote URL’s host:

"permissions": [ "...", "https://supersweetdomainbutnotcspfriendly.com/" ]

Cross-origin XMLHttpRequest

Fetch the remote URL into the app and serve its contents as a blob: URL:

var xhr = new XMLHttpRequest(); xhr.open('GET', 'https://supersweetdomainbutnotcspfriendly.com/image.png', true); xhr.responseType = 'blob'; xhr.onload = function(e) { var img = document.createElement('img'); img.src = window.URL.createObjectURL(this.response); document.body.appendChild(img); }; xhr.send();

That’s it. I already had basically the sake XMLHttpRequest code as above. After I included “https://api.pinboard.in/” in the list of permissions, everything clicked and I was good to go.

So where am I now?

Well, I can create bookmarks and make requests with the Pinboard API. Here’s something else I was playing around with, using a little CSS to display one tile per tag. I’m thinking the user will be able to select tags to generate bookmarks from, but this is all still a work-in-progress.

first-extension-display-all-tags

Subscribe to Weekly Updates!

Get an email with the latest posts, once per week...
* indicates required