Saturday, February 13, 2010

Google Chrome Extension: New Tab Redirect! STEP BY STEP

Background
(see http://code.google.com/chrome/extensions/overview.html for a thorough background of Chrome Extension development)
A Google Chrome Extension is a mini web application which runs in its own process and can perform actions at a browser-level or page-level.
An extension consists (or may consist) of a number of parts:
  • manifest.json
  • Web files (html, js, css, images)
  • NPAPI Plugins
  • background.html (the long-running process)
All of these files are bundled into a package with a .crx file extension. When uploading your extension to the Google Chrome Extensions Gallery, you only zip up the files your extension needs and Google will package it properly for you.

      The manifest.json file is a file which specifies properties of your extension in JavaScript Object Notation (aka JSON). For a quick intro to JSON, check out http://www.learn-ajax-tutorial.com/Json.cfm

The Manifest File
manifest.json:

   1:  {
   2:    "name": "New Tab Redirect!",
   3:    "description": "Sets a user-specified URL to load in new tabs.",
   4:    "version": "0.0.1.109",
   5:    "background_page": "background.html",
   6:    "options_page":"options.html",
   7:    "chrome_url_overrides": {
   8:      "newtab": "redirect.html"
   9:    },
  10:      "permissions": ["tabs"],
  11:      "icons": {
  12:      "128": "icon128.png",
  13:      "19":"icon19.png"
  14:    }
  15:  }
(end manifest.json)
The name, description, version, background_page, and options_page key/value pairs are pretty self-explanatory.

The "chrome_url_overrides" section is interesting. As you can see, the value for this entry is itself an object. This object allows me to override "newtab" with my own page, "redirect.html". Currently, the chromium team will only allow you to override newtab, however, I anticipate they'll allow you to override other pages in the future. For a list of url constants used within Chrome, check out the url_constants.cc file in the source code repository. Possible future overrides would be anything that is listed near the end of that file as 'const char kChromeUI*Host[]'

Back to the file. "permissions" takes an array of requested permissions. New Tab Redirect! requires permissions to update tabs. This is because we're not only redirecting to html web-hosted sites; New Tab Redirect allows you to set web pages, local files, or inherent Chrome pages.

"icons" specifies the location of the icons your extension will use. These icons will be displayed in the Gallery and in the Extensions page within Google Chrome

Remember: You must change your version number when updating your publicly hosted extension, or else Chrome won't find a new version and your users will not be updated.

The Background Page
background.html

   1:  <html>
   2:  <head>
   3:  <script type="text/javascript">
   4:  String.prototype.startsWith = function(str){
   5:      return (this.indexOf(str) === 0);
   6:  }
   7:   
   8:  var url = null;
   9:  var newTabId = undefined;
  10:  var protocol =  undefined;
  11:  var allowedProtocols = ["http://","https://","about:","file://", 
  12:    "file:\\", "file:///", "chrome://","chrome-internal://"];
  13:   
  14:  function setUrl(url) {
  15:          if(protocolPasses(url) 
  16:            && url.length > 8) { /* 8 is arbitrary */
  17:              this.url = url;     
  18:          } else {
  19:              protocol = 'http://'; /* force http */
  20:              var right = url.split('://')
  21:              if(right != undefined && right != null && right.length > 1) {
  22:                  this.url = protocol + right[1];
  23:              } else { 
  24:                  /* this will redirect to http:// if url is empty */
  25:                  this.url = protocol + url; }    
  26:          }
  27:          localStorage["url"]  = this.url;
  28:          
  29:      function protocolPasses(url) {
  30:          if(typeof(url) == 'undefined' || url == null) { return false; }
  31:          if (url.startsWith(allowedProtocols[3]) 
  32:              && !url.startsWith(allowedProtocols[5])) {
  33:              url.replace(allowedProtocols[3], allowedProtocols[5]);
  34:          } else if (url.startsWith(allowedProtocols[4])) {
  35:              url.replace(allowedProtocols[4], allowedProtocols[5]);
  36:          }
  37:          
  38:          for(var p in allowedProtocols) {
  39:              if(url.startsWith(allowedProtocols[p])) { return true;}
  40:          }
  41:          return false;
  42:      }
  43:  }
  44:   
  45:  function init() {
  46:     url = localStorage["url"] || "http://www.facebook.com";
  47:  }
  48:   
  49:  function r(tabId) {
  50:      chrome.tabs.update(tabId, {"url":this.url});
  51:  }
  52:   
  53:  chrome.tabs.onUpdated.addListener(function(tabId,info,tab) {
  54:      if (info.status === 'loading')
  55:          console.log('Loading url ... ' + tab.url)
  56:      if(info.status === 'complete')
  57:          console.log('Finished loading url ... ' + tab.url)
  58:  });
  59:   
  60:  chrome.tabs.onCreated.addListener(function(tab) {
  61:      newTabId = tab.id
  62:  });
  63:  </script>
  64:  </head>
  65:  <body onload="init()"></body>
  66:  </html>
(end background.html)
This page is pretty self-explanatory for the most part. The background page is your long-running process, and is usually used to house any variables that are shared between pages. However, this isn't a necessity of Chrome Extension architecture. Every page in an extension can interact with any other page through chrome.extension.getViews() or chrome.extension.getBackgroundPage() . Because of the nature of New Tab Redirect, I've used a background page.

One thing of note is the initialization of the extension. Since the operation is fairly simple, allowing a user to specify a url and then redirecting to that url, the only thing I need to worry about immediately is the url. This is stored using HTML 5's local storage. If the url doesn't exit, I set it to facebook.

The setUrl function is located in the Background page and is called from options.html. It is located here so the options page isn't trying to set the url variable that is maintained by the background page. This function checks my array of allowed protocols and validates the url with some simple rules.

r(tabId) is the function which actually updates the tab when the protocol isn't an http or https protocol. The function name is short because the the code in redirect.html should be as concise as possible. This function calls the chrome.tabs.update method (one culprit behind the tabs permission requirement)

That tabId is provided via the redirect.html, which gets the currently created tab's id. However, again note that this processing only occurs when the url is local.

Options

Because the options page is simple html and styles, I'll only include the JavaScript which sets the url.

options.js

   1:  String.prototype.startsWith = function(str) {
   2:      return (this.indexOf(str)===0);
   3:  }
   4:  var chromePages = {
   5:      Extensions : "chrome://extensions/",
   6:      History : "chrome://history/",
   7:      Downloads : "chrome://downloads/",
   8:      NewTab : "chrome-internal://newtab/"
   9:  }
  10:  var aboutPages = ["about:blank","about:version", "about:plugins","about:cache", 
  11:      "about:memory","about:histograms","about:dns",
  12:      "chrome://extensions/","chrome://history/",
  13:      "chrome://downloads/","chrome-internal://newtab/"];
  14:  var popularPages = { 
  15:      "Facebook":"www.facebook.com",
  16:      "MySpace":"www.myspace.com",
  17:      "Twitter":"www.twitter.com",
  18:      "Digg":"www.digg.com",
  19:      "Delicious":"www.delicious.com",
  20:      "Slashdot":"www.slashdot.org"
  21:  };
  22:   
  23:      
  24:  // save options to localStorage.
  25:  function save_options() {
  26:      var url = $('#custom-url').val();
  27:      if(url == ""){
  28:          url = aboutPages[0];
  29:      }
  30:      
  31:      if( $.inArray(String(url), aboutPages) || isValidURL(url)) {
  32:          save(true,url);
  33:      } else {
  34:          save(false,url);
  35:      }        
  36:  }
  37:   
  38:  function save(good,url) {
  39:      if(good) {
  40:          chrome.extension.getBackgroundPage().setUrl(url);
  41:          $('#status').text("Options Saved.");
  42:      } else {
  43:          $('#status').text( url + " is invalid. Try again (http:// is required)");    
  44:      }
  45:      
  46:      $('#status').css("display", "block");
  47:      setTimeout(function(){
  48:          $('#status').slideUp("fast").css("display", "none");
  49:      }, 1050);
  50:  }
  51:   
  52:  // Restores select box state to saved value from localStorage.
  53:  function restore_options() {
  54:      var url = chrome.extension.getBackgroundPage().url || "http://www.facebook.com/";    
  55:       $('#custom-url').val(url);
  56:  }
  57:   
  58:  function isValidURL(url) {
  59:      var urlRegxp = /^(http:\/\/www.|https:\/\/www.|ftp:\/\/www.|www.){1}([\w]+)(.[\w]+){1,2}$/;
  60:      if (urlRegxp.test(url) != true) {
  61:          return false;
  62:      } else {
  63:          return true;
  64:      }
  65:  }
  66:   
  67:  function saveQuickLink(url){
  68:      var uurl = unescape(url);
  69:       $('#custom-url').val(uurl);
  70:       save(true,uurl);
  71:       return false;
  72:  }
  73:   
  74:  $(document).ready(function(){
  75:      restore_options();
  76:      $.each(chromePages, function(k,v) {
  77:          var anchor = "<a href=\"javascript:saveQuickLink('"+v+"');\">"+k+"</a>";
  78:          $('#chromes').append("<li>" + anchor + "</li>");
  79:      });
  80:      $.each(aboutPages, function() {
  81:          if(this.startsWith("about:")) { /* quick fix to handle chrome pages elsewhere */
  82:              var anchor = "<a href=\"javascript:saveQuickLink('"+this+"');\">"+this+"</a>";
  83:              $('#abouts').append("<li>" + anchor + "</li>");
  84:          }
  85:      });
  86:      $.each(popularPages, function(k,v) {
  87:          var anchor = "<a href=\"javascript:saveQuickLink('"+v+"');\">"+k+"</a>";
  88:          $('#popular').append("<li>" + anchor + "</li>");
  89:      });
  90:  });
(end options.js)

As you can see, I've included jQuery for dom manipulation. I decided to do this because jQuery is so small and because the Options page can be whatever size you'd like (keeping in mind user experience, of course). This saved me some time from writing out a little bit of javascript.

Of note here is how I build the lists of Chrome pages, About pages, and Popular pages. This isn't included in the options.html file, which only has placeholders for the unordered lists. This allows me to go in and provide a page name and url for chromePages and popularPages, or just the url for aboutPages. If I want to add anything in the future, I'll just add a key/value pair (or url) to one of these objects. If I want to remove, likewise, I only touch the respective object.

Quick links (one from an above-mentioned object) are saved automatically, bypassing the url checking regex. However, you'll notice in the save_options function that I check first to see if the url is in the list of about pages, this is because a user will most likely enter about:memory or any valid url, and whould be less likely to enter chrome-internal://newtab/. In the future, I will probably add checks on all objects before checking a valid url, but this kind of thing happens in incremental development.
As you can see, if the user gets to save the url, the function setUrl from the background page is called via chrome.extension.getBackgroundPage().setUrl(url); Following good user interface techniques, we have to let the user know what happened, which is what the next line and the rest of the function accomplishes.

Note: The new tab url is chrome-internal:// instead of chrome://. Why is this, do you think? Don't think too hard, though. It's simple: redirect.html is now chrome://newtab/, because I've told chrome to override the newtab with my own file. You can test it out by typing chrome://newtab into Google Chrome. Try chrome-internal://newtab and see what happens? This can only be called internally, hence the name, and must be called from within an extension via chrome.tabs.update. If you don't use this internal url, when you set the url to chrome://newtab and hit CTRL+T or click the '+' tab, your tab will keep trying to call itself, and eventually stop, after doing nothing meaningful.

Redirect.html

Finally, the beast that does it all.

redirect.html

   1:  <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
   2:  <html>
   3:    <head>
   4:      <meta name="generator" content=
   5:      "HTML Tidy for Windows (vers 14 February 2006), see www.w3.org">
   6:      <title>
   7:        Redirecting...
   8:      </title>
   9:      <script type="text/javascript">
  10:      var wid, tid;
  11:   
  12:      function r() {
  13:          var url = localStorage["url"] || "";
  14:          if (url.toLowerCase().indexOf("http:") === 0 || 
  15:              url.toLowerCase().indexOf("https:") === 0) {
  16:              document.location.href = url;
  17:              return;
  18:          } else {
  19:              chrome.windows.getCurrent(function(w) {
  20:                  wid = w.id;
  21:                  chrome.tabs.getSelected(wid, function(t) {
  22:                      tid = t.id;
  23:                  });
  24:              });
  25:              chrome.extension.getBackgroundPage().r(tid);
  26:          }
  27:      }
  28:  </script>
  29:    </head>
  30:    <body onload="setTimeout('r()',100);r();return false;">
  31:      Redirecting...<br>
  32:      <em>If your page doesn't load within 5 seconds, 
  33:        <a href="javascript:r()">click here</a></em>
  34:    </body>
  35:  </html>
(end redirect.html)

Note: I have run the redirect.html file through HTML Tidy and a JavaScript formatter at http://jsbeautifier.org/, but this file should be as compact as possible. If you were to download New Tab Redirect and look at the source code, you'd see this code is all on one line. I'm writing this for your enjoyment, though, and one-liners aren't fun to look at.
The only thing special about this file is the the onload function. You'll notice how it runs r after 100 ms, then runs it again. There is quite an odd situation here: Chrome either has to initialize localStorage, doesn't allow us to get the current window and tab id immediately, or the processing required to do so takes longer than 100ms. You could, honestly, run the redirect function through a loop until it redirects (considering the document.location.href will immediately redirect the browser tab. If the url specified by the user is local, the function gets the current window, and from that the id of the current tab, and passes that to the background page's function.

I've been asked by a user or two to remove the redirect message, but I'll keep it for now. I consider a message necessary if, for some reason, a future release of Chrome breaks the extension.

The current source code for New Tab Redirect can be accessed via your Chrome Extensions directory after installing the extension, and is availing at the New Tab Redirect Project Page on Google Code.

If you have any questions or comments about this extension, please contact me.

Wednesday, February 10, 2010

Quickly create a Dictionary using Linq to Objects

When working with collections or lists, sometimes you just need to get a simple dictionary.
There are a number of ways to do this, but the ToDictionary method seems promising.
For isntance:

   1:  IList<Person> people = new List<Person>();
   2:  people.Add(new Person 
   3:      {
   4:          FirstName = "Jim", 
   5:          LastName="Schubert", 
   6:          Age=29, 
   7:          Phone="123-333-4444"
   8:      });
   9:  people.Add(new Person 
  10:      {
  11:          FirstName = "Joe", 
  12:          LastName="Schubert", 
  13:          Age=25, 
  14:          Phone="800-999-1111"
  15:      });
  16:      
  17:  Dictionary<int,string> names = people.ToDictionary(k => k.Age, v => v.FirstName);


There may not be many uses for this type of syntax, but simple transformations like this make Linq fun.

Archive