A talk about browser extensions in Chrome & Safari, why they’re so great for web hackers and how to build them.
Given at JSConf EU on September 26th, 2010 in Berlin, Germany.
Report
Share
Report
Share
1 of 89
More Related Content
Browser Extensions for Web Hackers
1. Browser Extensions
for Web Hackers
JSConf EU 2010, Berlin, Germany
Mark Wubben
A talk about browser extensions in Chrome & Safari, why they’re so great for web hackers
and how to build them.
Given at JSConf EU on September 26th, 2010 in Berlin, Germany.
Licensed under Creative Commons Attribution-Share Alike 2.5
http://creativecommons.org/licenses/by-sa/2.5/dk/
Photo by Steve Peck, http://www.flickr.com/photos/stevepeck/4615314670/. CC-BY-2.0.
2. I’m a Web Hacker
I’m a Web Hacker. That might scare some people, but I don’t hack highway signs or break
into computers. I (try to) build cool shit.
I think you’re web hackers as well, otherwise, why would you be here? But if you’re a web
hacker, why limit yourself to building websites? Why not hack other systems? Build, say,
browser extensions?
Photo by James Kim, http://www.flickr.com/photos/underbiteman/2638246638/. CC-
BY-2.0.
3. Add-ons, Plugins, Extensions
My question to you is, have you ever build a browser extension? Did you try? Did you look
into it and got scared? That’s been my story—Firefox extension have always looked too
bewildering. You’d have to know about RDF, XUL, XPCOM. You had to restart your browser
every time you wanted to try something. Bah!
But luckily, times have changed. Chrome and Safari are showing a new way of building
extensions. Easy, based on Open Web Technology.
Photo by Jon Fife, http://www.flickr.com/photos/good-karma/652486713/. CC-BY-SA-2.0.
4. Open Web Technology
See, these new extension platforms are based on JavaScript, mostly. But also HTML and CSS.
And the cool new HTML5ish APIs like localStorage or geo-location.
Let’s dive in.
Photo by Kevin Dooley, http://www.flickr.com/photos/pagedooley/4126491780/. CC-
BY-2.0.
5. Diving in to… what?
We’ll have a look soon at the underlying concepts of these new extension platforms. But you
might wonder, I’ve mentioned Chrome & Safari, what about Mozilla?
The good news is that Mozilla is renewing its platform — through Jetpack. However the
current stage of development is focused on building out the fundamentals of its platform.
They also take a different approach that I can’t fit into the 30 minute time slot. We therefore
won’t cover Jetpack much in this talk.
Photo by Justin De La Ornellas, http://www.flickr.com/photos/ornellas/4380104720/. CC-BY
2.0.
6. We’re going to start off with Chrome. Later during this talk we’ll look at how the concepts
used in Chrome translate to Safari.
Photo by Matt Biddulph, http://www.flickr.com/photos/mbiddulph/2924278682/. CC-BY-SA
2.0.
7. This is the Extensions page in Chrome. It shows you which extensions you have installed. You
can disable or uninstall them, see their web page, allow them to run in Incognito mode.
At the bottom is a link to the Google Chrome Extensions Gallery. At the top is an option to
enable Developer mode.
8. Developer mode lets you load a new extension from your local file system. You can also
package extensions for release or force the installed extensions to be updated. Normally this
is done automatically when the browser is restarted.
9. So, what is an extension?!
Now the obvious question is, what *is* an extension?!
10. An extension is a folder
with a manifest.json file
In essence, an extension isn’t much more than a folder that contains a manifest.json file.
Let’s try loading a few folders as an extension.
12. {
"name": "Chrome Extensions Rock!",
"version": "1.0"
}
manifest.json
And there’s the manifest for our very simple extension. These are the only two required
properties, a name for your extension and a version number.
Admittedly, this extension doesn’t do much.
For more about the manifest file, see http://code.google.com/chrome/extensions/
manifest.html.
13. Extensions can have
content scripts.
One of the things a Chrome Extension can do is to run scripts on the pages you visit. These
are content scripts and should be familiar, because its a concept from the Firefox-based
Greasemonkey add-on.
14. Photo by Brian Sawyer, http://www.flickr.com/photos/olivepress/61618336/. CC-BY-SA 2.0.
15. Aaron Boodman / Greasemonkey
In fact, the guy who invented Greasemonkey, Aaron Boodman, has been at Google for a few
years now and is one of the guys behind the new Extensions platform. To put it differently,
Chrome Extensions is Greasemonkey on steroids.
Photo by Mark Wubben, http://www.flickr.com/photos/novemberborn/230693761/. CC-BY-
SA-2.0.
16. Fixing Twitter opening
links in new windows
You might have noticed how external links on Twitter always open in a new window. I find
this annoying, so I figured I’d write an extension to fix it.
17. {
"name": "Twitter Fixer",
"version": "1.0.0",
"description": "Fix the external links…",
"content_scripts": [{
"matches": ["http://*.twitter.com/*",
"https://*.twitter.com/*"],
"js": ["fixer.js"]
}]
}
manifest.json
The manifest for our new extension, dubbed Twitter Fixer.
18. {
"name": "Twitter Fixer",
"version": "1.0.0",
"description": "Fix the external links…",
"content_scripts": [{
"matches": ["http://*.twitter.com/*",
"https://*.twitter.com/*"],
"js": ["fixer.js"]
}]
}
manifest.json
Note how I’ve added a description.
19. {
"name": "Twitter Fixer",
"version": "1.0.0",
"description": "Fix the external links…",
"content_scripts": [{
"matches": ["http://*.twitter.com/*",
"https://*.twitter.com/*"],
"js": ["fixer.js"]
}]
}
manifest.json
You can specify multiple content scripts per extension.
20. {
"name": "Twitter Fixer",
"version": "1.0.0",
"description": "Fix the external links…",
"content_scripts": [{
"matches": ["http://*.twitter.com/*",
"https://*.twitter.com/*"],
"js": ["fixer.js"]
}]
}
manifest.json
A content scripts needs to match a page. This is done through match patterns. Here we
specify our extension will run on any page or subdomain on twitter.com, over HTTP as well as
HTTPS.
Keep in mind that the user is warned about the sites you might match. The more restrictive
your match pattern, the better.
To learn more, see http://code.google.com/chrome/extensions/match_patterns.html.
21. {
"name": "Twitter Fixer",
"version": "1.0.0",
"description": "Fix the external links…",
"content_scripts": [{
"matches": ["http://*.twitter.com/*",
"https://*.twitter.com/*"],
"js": ["fixer.js"]
}]
}
manifest.json
A content script itself can exist of any number of JavaScript files. They’re loaded in the same
order as you specify in the manifest.
You can also specify CSS files that need to be added to the page your content script runs on.
To learn more, see http://code.google.com/chrome/extensions/content_scripts.html.
22. var links = document.querySelectorAll(
"a[target=_blank]");
Array.prototype.slice.call(links).forEach(
function(a){
a.removeAttribute("target");
});
fixer.js
And the actual content script.
25. Isolated Worlds
Chrome has learned from the security problems that existed with Greasemonkey, and even
with Firefox add-ons as a whole. Each extension lives in a so-called “isolated world”,
meaning it’s isolated from other extensions save for a few tightly controlled communication
bridges.
Photo by F.H. Mira, http://www.flickr.com/photos/fhmira/3204656258/sizes/o/. CC-BY-
SA-2.0.
26. Content scripts run in
separate contexts
For example, the JavaScript inside your content scripts is evaluated in a separate context
from the page JavaScript. This means your code won’t affect the page code, and vice versa.
You can’t directly call page code, and it can’t directly call your code.
27. Shared DOM
Luckily the page document is shared between the various content scripts that might be
running on it. That way, you can change it!
28. Communicating with page
JavaScript
But with these isolated worlds, how can your content scripts talk to the page JavaScript? Well,
you’ve got access to the DOM, so you can insert your own JavaScript into the page! And, you
can use DOM events so the inserted JavaScript can talk back to you.
29. document.documentElement.addEventListener(
"ExtensionNotify",
function(){ alert("Notified!"); },
false
);
var s = document.createElement("script");
s.textContent = 'function notifyContentScript(){
var evt = document.createEvent("Event");
evt.initEvent("ExtensionNotify", false, false);
document.documentElement.dispatchEvent(evt);
}';
document.body.appendChild(s);
communication.js
This sets up a content script that insert the `notifyContentScript` method into the page.
When this method is called, a custom DOM event is dispatched on the document element,
which is used to notify the content script.
While you can’t send data along with the event, you can store it in the DOM. The content
script can then look it up.
30. document.documentElement.addEventListener(
"ExtensionNotify",
function(){ alert("Notified!"); },
false
);
var s = document.createElement("script");
s.textContent = 'function notifyContentScript(){
var evt = document.createEvent("Event");
evt.initEvent("ExtensionNotify", false, false);
document.documentElement.dispatchEvent(evt);
}';
document.body.appendChild(s);
communication.js
This sets up a content script that insert the `notifyContentScript` method into the page.
When this method is called, a custom DOM event is dispatched on the document element,
which is used to notify the content script.
While you can’t send data along with the event, you can store it in the DOM. The content
script can then look it up.
31. document.documentElement.addEventListener(
"ExtensionNotify",
function(){ alert("Notified!"); },
false
);
var s = document.createElement("script");
s.textContent = 'function notifyContentScript(){
var evt = document.createEvent("Event");
evt.initEvent("ExtensionNotify", false, false);
document.documentElement.dispatchEvent(evt);
}';
document.body.appendChild(s);
communication.js
This sets up a content script that insert the `notifyContentScript` method into the page.
When this method is called, a custom DOM event is dispatched on the document element,
which is used to notify the content script.
While you can’t send data along with the event, you can store it in the DOM. The content
script can then look it up.
32. document.documentElement.addEventListener(
"ExtensionNotify",
function(){ alert("Notified!"); },
false
);
var s = document.createElement("script");
s.textContent = 'function notifyContentScript(){
var evt = document.createEvent("Event");
evt.initEvent("ExtensionNotify", false, false);
document.documentElement.dispatchEvent(evt);
}';
document.body.appendChild(s);
communication.js
This sets up a content script that insert the `notifyContentScript` method into the page.
When this method is called, a custom DOM event is dispatched on the document element,
which is used to notify the content script.
While you can’t send data along with the event, you can store it in the DOM. The content
script can then look it up.
34. Content scripts are limited.
Background pages!
Content scripts are fairly limited though. They exist only as long as the page they run on
exists. They don’t have access to any permanent storage, so you can’t configure them. Nor
can they talk to other websites, so you can’t look up anything through an API.
Luckily, Chrome Extensions let you build background pages. These are normal HTML pages,
except that they’re not rendered. They’re loaded when the browser starts, and won’t be
unloaded until it’s closed.
Let’s build a more complicated extension.
35. Expanding bit.ly URLs on
Twitter
Due to character constraints URLs in Tweets are often shortened. But, I’d like to see where
I’m going! Let’s write Chrome Extension that can expand bit.ly URLs.
37. {
"name": "Twitter Fixer",
"version": "1.1.0",
"description": "Expands shortened URLs…",
"permissions": ["http://api.bit.ly/*"],
"background_page": "background.html",
"content_scripts": [{
"run_at": "document_end",
"matches": ["http://*.twitter.com/*",
"https://*.twitter.com/*"],
"js": ["fixer.js"]
}]
}
manifest.json
I’ve made two major modifications to the manifest.json we used previously. First is loading
the background page, this is done using the background_page property whose value is the
relative path (from the manifest.json file) to the background page. By convention this is
named background.html, but you can name it whatever you like.
The other change is that I’m now requesting permission to talk to the Bit.ly API. Chrome
forces the extension developer to request permission for almost anything. When the user
installs your extension he’s made aware of what you’re extension will have permission to,
therefore making it harder for nefarious Extension developers to sneak bad stuff into their
extensions without the users knowing about it.
38. {
"name": "Twitter Fixer",
"version": "1.1.0",
"description": "Expands shortened URLs…",
"permissions": ["http://api.bit.ly/*"],
"background_page": "background.html",
"content_scripts": [{
"run_at": "document_end",
"matches": ["http://*.twitter.com/*",
"https://*.twitter.com/*"],
"js": ["fixer.js"]
}]
}
manifest.json
Another change I made is to specify the `run_at` property for the content script. This way I
can make sure it runs right after the page document has finished parsing, so we don’t have
to wait too long before we can expand the bit.ly URLs.
39. var parsed = parseUrls();
chrome.extension.sendRequest(
parsed.hashes,
function(mapping){
for(hash in mapping){
parsed.links[hash].forEach(function(link){
link.textContent = mapping[hash];
});
}
}
);
fixer.js
The code to find the URLs in the page isn’t terribly important so I’ve not put it in this slide.
Suffice to say, `parsed` contains a list of bit.ly hashes, and a mapping from a hash to one or
more link elements.
40. var parsed = parseUrls();
chrome.extension.sendRequest(
parsed.hashes,
function(mapping){
for(hash in mapping){
parsed.links[hash].forEach(function(link){
link.textContent = mapping[hash];
});
}
}
);
fixer.js
The content script needs to talk to the background page to expand the hashes. This is done
through the `chrome.extension.sendRequest` API.
41. var parsed = parseUrls();
chrome.extension.sendRequest(
parsed.hashes,
function(mapping){
for(hash in mapping){
parsed.links[hash].forEach(function(link){
link.textContent = mapping[hash];
});
}
}
);
fixer.js
Also note how I can use forEach on an array. Chrome has a fully up-to-date JavaScript engine
so native forEach is available.
Same for using textContent to set the link text value.
42. <!doctype html>
<script src="dojo.js"></script>
<script>
chrome.extension.onRequest.addListener(
function(hashes, sender, sendResponse){
// …
sendResponse(mapping);
}
);
</script>
background.html
I won’t go into the specifics of how to talk to bit.ly and get the expanded URLs.
43. <!doctype html>
<script src="dojo.js"></script>
<script>
chrome.extension.onRequest.addListener(
function(hashes, sender, sendResponse){
// …
sendResponse(mapping);
}
);
</script>
background.html
Note how I can pull other code into the background page. It’s a web page, after all.
44. <!doctype html>
<script src="dojo.js"></script>
<script>
chrome.extension.onRequest.addListener(
function(hashes, sender, sendResponse){
// …
sendResponse(mapping);
}
);
</script>
background.html
The important bit is how you register a handler for requests from the content script. You get
the request object, a reference to the tab from which the request was sent, and a callback
function to call once you have a response.
This communication mechanism is completely asynchronous.
46. Debugging
Web Inspector
You can easily debug your extension using the Web Inspector you would normally use to
debug your web pages. You can set break points or use the debugger keyword. An inspector
for the background page can be opened by clicking on the “background.html” link in the
Extensions page (if you have Developer mode enabled).
You may also notice how the background page is actually at “chrome-extension://some-
unique-identifier/background.html”. This is the domain it runs in, so the extension can
piggyback on the normal same-origin restrictions!
47. Quick Recap
To recap:
* Chrome extensions are folders containing a manifest.json file
* You can have Greasemonkey-like content scripts that act on the page
* You can have HTML-driven background pages for generic/shared logic, talking to other
sites
* You can easily communicate between content scripts and background pages
48. So, how do these principles translate to Safari?
Photo by William Warby, http://www.flickr.com/photos/wwarby/2405490902/. CC-BY 2.0.
50. In order to start developing extensions, make sure to enable the Develop menu. Then, under
the Develop menu, you’ll find an “Extension Builder” item.
51. This then is the Extension Builder, which initially is really quite barren. Let’s make a very
simple extension by clicking the “+” icon in the bottom left.
52. Safari lets you manage your extension configuration through a nice enough UI, whereas
Chrome makes you edit a JSON file. Another difference is that Apple requires you to register
with their Safari developer program (for free). This then gives you access to your own Safari
Developer Certificate. Without the certificate, you can’t even *test* your extension locally.
56. Content scripts?
Yes, Safari also has content scripts, although it’s called “Injected Extension Content”. You
configure them through the Extension Builder. Let’s see about implementing the Twitter Fixer
extension in Safari.
57. First we set the permissions for our extension. It can access some domains, namely
twitter.com and subdomains, including secure pages.
58. First we set the permissions for our extension. It can access some domains, namely
twitter.com and subdomains, including secure pages.
59. Then we add the content script, setting it to run when the document has loaded.
60. Info.plist is Safari’s manifest.json. It’s a typical Apple file, that isn’t nice to edit manually. Yay
for Extension Builder!
fixer.js is of course our content script. In fact, it’s the exact same script we used for Chrome!
65. Differences…
Of course the Chrome extension didn’t *just* work in Safari. The extension APIs are different!
For this extension, the only platform specific APIs we used were for communicating between
the content script and the background page. Let’s compare Chrome to Safari.
66. var parsed = parseUrls();
chrome.extension.sendRequest(
parsed.hashes,
function(mapping){
for(hash in mapping){
parsed.links[hash].forEach(function(link){
link.textContent = mapping[hash];
});
}
}
);
fixer.js
This is the original content script as used in Chrome.
67. var parsed = parseUrls();
chrome.extension.sendRequest(
parsed.hashes,
function(mapping){
for(hash in mapping){
parsed.links[hash].forEach(function(link){
link.textContent = mapping[hash];
});
}
}
);
fixer.js
Let’s focus on the communication to the background page. We send a payload to the
background page and provide a callback that can be called by the background page when it
has a response.
68. var parsed = parseUrls();
safari.self.tab.dispatchMessage("expandHashes",
parsed.hashes);
safari.self.addEventListener("message",
function(evt){
if(evt.name == "mappingComplete"){
var mapping = evt.message;
// …
}
}, false);
fixer.js
Safari takes a different approach. It has a special message event that content scripts and
global pages (etc) can listen to. The event has a name and a message property which contains
the original message object. `dispatchMessage()` is used to send these events.
This example is for content scripts. The messages arrive at different objects for global pages.
69. var parsed = parseUrls();
safari.self.tab.dispatchMessage("expandHashes",
parsed.hashes);
safari.self.addEventListener("message",
function(evt){
if(evt.name == "mappingComplete"){
var mapping = evt.message;
// …
}
}, false);
fixer.js
Safari takes a different approach. It has a special message event that content scripts and
global pages (etc) can listen to. The event has a name and a message property which contains
the original message object. `dispatchMessage()` is used to send these events.
70. var parsed = parseUrls();
safari.self.tab.dispatchMessage("expandHashes",
parsed.hashes);
safari.self.addEventListener("message",
function(evt){
if(evt.name == "mappingComplete"){
var mapping = evt.message;
// …
}
}, false);
fixer.js
Safari takes a different approach. It has a special message event that content scripts and
global pages (etc) can listen to. The event has a name and a message property which contains
the original message object. `dispatchMessage()` is used to send these events.
71. var parsed = parseUrls();
safari.self.tab.dispatchMessage("expandHashes",
parsed.hashes);
safari.self.addEventListener("message",
function(evt){
if(evt.name == "mappingComplete"){
var mapping = evt.message;
// …
}
}, false);
fixer.js
Safari takes a different approach. It has a special message event that content scripts and
global pages (etc) can listen to. The event has a name and a message property which contains
the original message object. `dispatchMessage()` is used to send these events.
72. <!doctype html>
<script src="dojo.js"></script>
<script>
chrome.extension.onRequest.addListener(
function(hashes, sender, sendResponse){
// …
sendResponse(mapping);
}
);
</script>
background.html
Here then the original background page script as used in Chrome.
73. <!doctype html>
<script src="dojo.js"></script>
<script>
chrome.extension.onRequest.addListener(
function(hashes, sender, sendResponse){
// …
sendResponse(mapping);
}
);
</script>
background.html
Here then the original background page script as used in Chrome.
74. <!doctype html>
<script src="dojo.js"></script>
<script>
safari.application.addEventListener("message",
function(evt){
if(evt.name != "expandHashes"){ return; }
// …
evt.target.page.dispatchMessage(
"mappingComplete", mapping);
},
false);
</script>
background.html
For the background page, we listen to the message event on `safari.application`. We can
send a response by dispatching a message to the content script the message event originates
from.
75. <!doctype html>
<script src="dojo.js"></script>
<script>
safari.application.addEventListener("message",
function(evt){
if(evt.name != "expandHashes"){ return; }
// …
evt.target.page.dispatchMessage(
"mappingComplete", mapping);
},
false);
</script>
background.html
For the background page, we listen to the message event on `safari.application`. We can
send a response by dispatching a message to the content script the message event originates
from.
76. <!doctype html>
<script src="dojo.js"></script>
<script>
safari.application.addEventListener("message",
function(evt){
if(evt.name != "expandHashes"){ return; }
// …
evt.target.page.dispatchMessage(
"mappingComplete", mapping);
},
false);
</script>
background.html
For the background page, we listen to the message event on `safari.application`. We can
send a response by dispatching a message to the content script the message event originates
from.
80. safari.extension.settings
You can read the settings from `safari.extension.settings`, though not in content scripts.
You’ll have to use the communication APIs to retrieve the settings from the background page.
81. Can we do this in Chrome, too? Well, yes, except that Chrome doesn’t have a nice UI and API
for managing settings. You’ll have to do it yourself, with the help of a special background
page and localStorage.
Photo by Matt Biddulph, http://www.flickr.com/photos/mbiddulph/2924278682/. CC-BY-SA
2.0.
82. {
"name": "Twitter Fixer",
"version": "1.2.0",
"description": "Expands shortened URLs…",
"permissions": ["http://api.bit.ly/*"],
"options_page": "options.html",
"background_page": "background.html",
"content_scripts": [{
"run_at": "document_end",
"matches": ["http://*.twitter.com/*",
"https://*.twitter.com/*"],
"js": ["fixer.js"]
}]
}
manifest.json
Chrome supports Options pages, which are just like background pages except that they can
be opened directly.
83. {
"name": "Twitter Fixer",
"version": "1.2.0",
"description": "Expands shortened URLs…",
"permissions": ["http://api.bit.ly/*"],
"options_page": "options.html",
"background_page": "background.html",
"content_scripts": [{
"run_at": "document_end",
"matches": ["http://*.twitter.com/*",
"https://*.twitter.com/*"],
"js": ["fixer.js"]
}]
}
manifest.json
Chrome supports Options pages, which are just like background pages except that they can
be opened directly.
85. So, about Jetpack…
The approach taken by both Chrome and Safari is one of content scripts and background
pages, in essence normal HTML. Jetpack is taking a CommonJS approach where your
extension consists of a main script that adds the UI elsewhere. I do encourage you to check it
out.
86. Bonus!
Safari/Chrome hybrid extension
Given that both Chrome and Safari use folders for their extensions, it’s quite possible to build
a hybrid extension that can be loaded into both Chrome *and* Safari.
(Of course, once compiled for release, this would no longer be the case).
I’ve combined the latest version of Twitter Fixer so it works in both browsers. This is done by
abstracting the communication and settings logic. For Chrome, the Options page is built
dynamically based on the Settings configured for Safari.
87. Thank you
Mark Wubben
supercollider.dk & novemberborn.net
twitter.com/novemberborn
Slides: 11born.net/jsconfeu/slides
Code: 11born.net/jsconfeu/code
Licensed under Creative Commons Attribution-Share Alike 2.5
http://creativecommons.org/licenses/by-sa/2.5/dk/
And that concludes this talk. Thank you for your attention.
88. Steve Peck
James Kim
Jon Fife
Kevin Dooley F H Mira
Justin De La Ornellas William Warby
Matt Biddulph Jeff Kubina
Brian Sawyer Matt Jones
Many, many thanks to the wonderful people on Flickr who licensed their photos under
Creative Commons.
Photo by Jeff Kubina, http://flickr.com/photos/kubina/903033693/. CC-BY-SA 2.0.
89. Now, as Matt Jones would put it, GET EXCITED and MAKE THINGS!
Illustration by Matt Jones, CC-BY-SA-NC, http://www.flickr.com/photos/blackbeltjones/
3365682994/.