ChromeExtensions Message Passing

Google Chrome is an extensively used browser and is quite popular among developer too. A part of Chrome’s appeal is owed to its excellent extensions. Its not really hard to write simple extension for added convenience.

I am a Ruby on Rails developer and spend quite good amount of time using Google Chrome for development and testing Rails applications. I prefer some background music while working, it helps me cut out out the crap and concentrate better on my work.

Youtube is excellent music service for me. With youtube autoplay enabled, the game becomes even simpler.

Only problem now is, when I have to pause music I have to switch to youtube tab and then hit the pause button. And same when I want resume it.  This is a pain.

Solution:  Build a chrome extension to play/pause youtube without having to switch tabs.
All it needs is a little javascript, html and may be some css if you want things to be a little more pretty. Since we won’t do any user-interface here, we don’t need to think html/css.

How it Works ?

Extensions allow you to add functionality to Chrome without diving deeply into native code. You can create new extensions for Chrome with those core technologies that you're already familiar with from web development: HTML, CSS, and JavaScript.

We’ll do this by implementing a UI element Chrome calls  browser action, which allows us to place a clickable icon right next to Chrome’s Omnibox for easy access. Clicking that icon will play/pause the youtube video irrespective of the current active tab.

ChromeExtYTPP

Lets get started…

Every chrome extension needs to have a file named manifest.json in extension root directory. Lets create one.

{
"manifest_version": 2,
"name": "YTPP",
"short_name": "YTPP",
"version": "0.1",
"description": "YTPP: YouTube Play/Pause without switching tabs",
"icons": {"128": "icon.png", "48": "icon_48.png", "16": "icon_16.png" },
"author": {"name": "spidergears", "twitter_handle": "spider_gears", "github": "http://github.com/spidergears"},
"browser_action": { "default_icon": "icon.png", "default_title": "YTPP"},
"permissions": [
"tabs"
],
"background": { "scripts": ["ytpp_background_script.js"]},
"content_scripts": [ {"matches": ["http://www.youtube.com/*", "https://www.youtube.com/*"%5D, "js": ["ytpp_content_script.js"]} ]
}
view raw manifest.json hosted with ❤ by GitHub

manifest_version:2 We are using Chrome Extension menifest specification version 2.

name:YTPP Name for extension

version:0.1 Release version number. We need to increment this with every future release.

description:"YTPP: YouTube Play/Pause without switching tabs" Short description for out extension.

icons: {"128": "icon.png", "48": "icon_48.png", "16": "icon_16.png"} Icon files for different sizes.

author: {"name": "spidergears", "twitter_handle": "spider_gears", "github": "http://github.com/spidergears"} Author Info

browser_action: { "default_icon": "icon.png", "default_title": "YTPP"} Specifications for browser action.

"permissions": ["tabs"] List of browser permissions extension needs to be functional.

"background": {"scripts": ["ytpp_background_script.js"]} Register javascript to be run background. Will call code from this script on click of the browser action button.

"content_scripts": [{"matches": ["http://www.youtube.com/*", "https://www.youtube.com/*"], "js": ["ytpp_content_script.js"]}] Register javascript to be inject into page content. matches keys helps specify on which all websites will the script be injected.

We have manifest file ready. Lets bring in our content and background scripts.

Background Script

Background scripts are a part of background page that  runs in the extension process. Background page exists for the lifetime of the extension, and only one instance of it at a time is active. (Exception: if your extension uses incognito “split” mode, a second instance is created for incognito windows.)

In a typical extension with a background page, the UI — browser action button in our case is implemented by dumb views. When the view needs some state, it requests the state from the background page. When the background page notices a state change, the background page tells the views to update.

Now we want to play/pause youtube media when `browser action` button is clicked.

To accomplish this, we attach a click listener to our `action button` . On button click we loop through all open tabs and check if they are tabs running youtube. For all tabs running youtube, we send a message to our injected script. The inject script, on receipt of message will actually take action to play/pause youtube media.

chrome.browserAction.onClicked.addListener(function(tab) {
chrome.tabs.query({}, function(tabs){
for (var i=0; i < tabs.length; i++) {
if (/https?:\/\/www\.youtube\.com/.test(tabs[i].url)) {
chrome.tabs.sendMessage(tabs[i].id, {action: "toggle_playback"}, function(response) {});
}
}
})
});

Content Script

Content scripts are JavaScript files that run in the context of web pages. By using the standard Document Object Model (DOM), they can read details of the web pages the browser visits, or make changes to them.

Here are some examples of what content scripts can do:

  • Find unlinked URLs in web pages and convert them into hyperlinks
  • Increase the font size to make text more legible
  • Find and process microformat data in the DOM

However, content scripts have some limitations. They cannot:

These limitations aren’t as bad as they sound. Content scripts can indirectly use the chrome.* APIs, get access to extension data, and request extension actions by exchanging messages with their parent extension. Content scripts can also make cross-site XMLHttpRequests to the same sites as their parent extensions, and they can communicate with web pages using the shared DOM.

Let’s setup our content script that will be injected into the page. Our script will listen for a message from our browser action button. On message receipt it will grab the youtube play/pause button and click it to toggle media playback.

chrome.extension.onMessage.addListener(function(message, sender, sendResponse) {
if(message.action == "toggle_playback"){
play_pause_button = document.getElementsByClassName('ytp-play-button ytp-button')[0]
if(play_pause_button){
play_pause_button.click();
}
}
});

Easy!!!

Full source code is available in Github Repository.

Extension is published here.

 

Writing a Chrome extension

With a large number of services available online, we tend to to spend most of our time on our browser. Well browser do come handy for a large number of activities from reading news to making travel and dining reservations. What can be better than a browser that can do some cool stuff with a click of mouse or a simple key stroke… I guess nothing

Google Chrome is the best web browser around right now (see usage statistics), and part of Chrome’s appeal is owed to its excellent extensions. Its not really hard write simple extension to get your work done.
All it needs is a little javascript, html and may be some css if you want things to be a little more pretty.

So lets build one…

Purpose: Export Chrome bookmarks to Pocket account.

Requirements:

  • Some javascript skills
  • poor html skills
  • since we want to talk to servers at Pocket, we need to know how the api works.

Authenticating with pocket api

  1. Obtain platform consumer key from Pocket
  2. Obtain a request token
  3. Redirect user to pocket to continue authentication
  4. Receive callback from pocket
  5. Convert request code from step 2 into a Pocket access token
  6. Begin making authenticated request to Pocket

Step 1 above needs to be performed only once while you will have to go through remaining steps overtime want to connect to pocket. More details can be obtained from Pocket docs rtfm.

When making a request to get request token from Pocket, we need to pass a redirect url, this is application specific url where Pocket will redirect our request after processing. We can obtain an unique redirect url for our chrome extension using the Chrome Identity api. The api provides a getRedirectURL() function just for this purpose. Hence we do chrome.identity.getRedirectURL() call to obtain the redirect url.

Writing Chrome browser extension

manifest.json:  the Anchor

Every chrome extension must have at the least one file named manifest.json, which holds the basics of the extension- its name, description, version number, permissions, etc.

So, in order to begin create a new folder for the extension, and within the folder a file called manifest.json.
Put following content in the file:

{
"name": "ChromeToPocket",
"short_name": "ChromeToPocket",
"description": "Import your chrome bookamrks to Pocket",
"version": "0.1",
"manifest_version": 2,
"browser_action": {
"default_icon": "import.png",
"default_popup": "popup.html"
},
"icons": { "64": "import.png" },
"author": {"name": "spidergears", "twitter_handle": "spider_gears", "github": "http://github.com/spidergears&quot;},
"content_scripts": [ {
"js": [ "import.js" ],
"matches": [ "https://*.getpocket.com/*"%5D
}],
"permissions": [ "bookmarks", "identity" ]
}
view raw manifest.json hosted with ❤ by GitHub

the first few fields are self explanatory, for others
browser_action: tells Chrome that we are making a browser extension that have a icon called import.png and default popup will show from file popup.html.
then there is some information about the icon and obviously the author.

content_scripts: Content scripts are JavaScript files that run in the context of web pages. By using the standard Document Object Model (DOM), they can read details of the web pages the browser visits, or make changes to them.
We will use import.js for all our js code required. We also specify that we will only communicate with getpocket.com domain and its subdomains, if any.

permissions: API to request declared optional permissions at run time rather than install time, so users understand why the permissions are needed and grant only those that are necessary.

  • bookmarks: API to create, organize, and otherwise manipulate bookmarks.
  • identity: API to get OAuth2 access tokens

Now we have our manifest.json in a good shape let’s begin with import.js, the core of the extension.

popup.html: User interaction

Within the folder create another file called popup.html and put in the following code

<html>
<head>
<title>ChromeToPocket</title>
<script src="import.js"></script>
<style>
* {
margin: 5;
padding: 5;
}
html, body {
overflow: auto;
}
#progress {
overflow: auto;
}
</style>
</head>
<body>
<div id="progress-icon"></div>
<div id="progress-text"></div>
</body>
</html>
view raw popup.html hosted with ❤ by GitHub

this is simple enough and goes without any explanation.

import.js:  Core application logic

Within the folder crate another file named import.js.

Put in the following content in the file.

var __request_code;
var __access_token_string;
function process_bookmarks ( bookmarks ) {
document.getElementById("progress-icon").innerHTML = "<img height='50px' width='50px' src='spinner-uploading.gif'></img>"
document.getElementById("progress-text").innerHTML = "Uploadig bookmarks to Pocket"
console.log("Beginning import...")
for ( var i =0; i < bookmarks.length; i++ ) {
var bookmark = bookmarks[i];
if ( bookmark.url ) {
xmlhttp = make_xmlhttprequest("POST", "https://getpocket.com/v3/add&quot;, true)
console.log( "Adding url: "+ bookmark.url );
//chota_div = document.createElement('div')
//chota_div.innerHTML= "Added Url: "+ bookmark.url;
//document.getElementById("progress").appendChild(chota_div)
xmlhttp.send("consumer_key="+ consumer_key +"&" + __access_token_string +"&url="+ encodeURI(bookmark.url) + "&tags=ChromeBookmarks")
}
if ( bookmark.children ) {
process_bookmarks( bookmark.children );
}
}
console.log("Completed import...")
document.getElementById("progress-icon").innerHTML = ""
document.getElementById("progress-text").innerHTML = "Completed upload."
}
function get_redirect_url () {
return chrome.identity.getRedirectURL();
}
//TODO place your pocket consumer key here
function get_pocket_consumer_key () {
return "your_pocket_consumer_key"
}
function make_xmlhttprequest (method, url, flag) {
xmlhttp = new XMLHttpRequest();
xmlhttp.open(method, url, flag);
xmlhttp.setRequestHeader( "Content-type","application/x-www-form-urlencoded" );
return xmlhttp
}
function get_request_code (consumer_key) {
redirect_url = get_redirect_url();
xmlhttp = make_xmlhttprequest ('POST', 'https://getpocket.com/v3/oauth/request&#39;, true)
xmlhttp.onreadystatechange = function () {
if ( xmlhttp.readyState === 4 ) {
if (xmlhttp.status === 200){
request_code = xmlhttp.responseText.split('=')[1];
__request_code = request_code
lauch_chrome_webAuthFlow_and_return_access_token(request_code);
}
else {
document.getElementById("progress-icon").innerHTML = "<img height='50px' width='50px' src='failed.png'></img>"
document.getElementById("progress-text").innerHTML = "Authentication failed!!"
}
}
}
xmlhttp.send("consumer_key="+ consumer_key +"&redirect_uri="+ redirect_url)
}
function get_access_token () {
xmlhttp = make_xmlhttprequest('POST', 'https://getpocket.com/v3/oauth/authorize&#39;, true);
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
access_token_string = xmlhttp.responseText.split('&')[0]
__access_token_string = access_token_string
chrome.bookmarks.getTree( process_bookmarks );
}
}
xmlhttp.send( "consumer_key="+ consumer_key +"&code="+ request_code )
}
function lauch_chrome_webAuthFlow_and_return_access_token (request_code) {
redirect_url = get_redirect_url();
chrome.identity.launchWebAuthFlow(
{'url': "https://getpocket.com/auth/authorize?request_token="+ request_code + "&redirect_uri="+ redirect_url, 'interactive': true},
function(redirect_url) {
//Get access token
get_access_token(consumer_key, request_code);
});
}
function import_my_chrome_bookmarks () {
document.getElementById("progress-icon").innerHTML = "<img height='50px' width='50px' src='spinner-authenticating.gif'></img>"
document.getElementById("progress-text").innerHTML = "Authenticating with Poket"
consumer_key = get_pocket_consumer_key();
get_request_code(consumer_key);
}
window.onload = function(){
import_my_chrome_bookmarks();
};
view raw import.js hosted with ❤ by GitHub

the file contains the code for making XMLHttpRequests to Pocket api.

On page load e.i. when the pop-up has been initialised we call our first function  import_chrome_bookmarks.

This function invokes other functions in a chain in order to complete all the six steps mentioned above and hence successfully upload all our bookmarks to Pocket account.

We have used two of the Chrome APIs to fulfil our task

  • Bookamarks: chrome.bookmarks.getTree() api call gives us access to user’s bookmarks and also accepts a callback function to manipulate the bookmarks.
  • Identity: chrome.identity.launchWebAuthFlow() api call enables us launch webAuthentication process with Pocket api and accepts a callback function to process the redirect and generate access token.

Link to github Repo