Greasemonkey: Code Injection is Bliss

Greasemonkey: Code Injection is Bliss

Got something to say?

Share your comments on this topic with other web professionals

In: Articles

By Jeff Rudesyle

Published on February 12, 2008

Don’t you hate it when someone develops an idea that’s been in your head for a while? Ever since I started messing around with web pages many moons ago, I had this “thought” about developing a tool that would allow me to alter the HTML sent to my browser to suit my own needs. But, while I wasted my time chasing girls and drinking beer, the mind readers who created Greasemonkey came up with such a tool.

Greasemonkey is a Firefox extension (there are other implementations out there for IE and other browsers) that allows you to inject javascript into an HTML page after it has been loaded into your browser. This is a powerful tool, as you can alter just about anything on a web page you can think of. Please note that you are altering the page in your browser only; you are not actually altering the source code of the original page, even though that would be pretty cool! The next version of Greasemonkey (0.8) will even include the ability to reference external libraries. There is a whole site called userscripts.org devoted to Greasemonkey scripts that other people have written, which you can download and install yourself. Some examples of these scripts are:

These scripts have the ability to manipulate what is done in your browser—but so does any extension you download for Firefox. It always pays to do a little research beforehand to make sure your computer won’t explode and die from a malicious script or program.

What I’m going to show you in this article is how to write a Greasemonkey script to use with this web site.

Our user requirements

The almighty Editor of this site is demanding (he really did, and he cursed at me) a drop-down menu in the left nav that will display the article topics from the articles by topic page. When a topic is selected, the body of the current page will then display links to all the articles for the topic, similar to a specific topic page.

How we’ll meet the user requirements

To start, we’ll need to get the topics for the drop-down. There are two ways we can handle this:

  • Create the options with static “hard-coded” values directly in our script
  • Dynamically obtain the values from /topics/

The sexy way to do this is with option two, so we’ll go that route. We’ll use Ajax to remotely obtain the contents from /topics/.

We then need to display the articles for each topic, so we’re faced with the same decision as above (static vs. dynamic content). We’ll use Ajax again to retrieve the content for the selected topic, and then replace the main page content with the content returned from our Ajax request.

Let’s set up our script

After downloading and installing the Greasemonkey extension, the first thing we do is create a blank text file, and then open it up in our favorite text editor. We then copy the following text our new script:

// UserScript // @name           Browse By Type for digital-web.com // @namespace      digital-web // @include        /* // /UserScript  alert("My script is working"); 

This is how we’ll start every Greasemonkey script. The @name attribute simply gives our script a nice viewable name in the Greasemonkey options window, but the important thing here is the @include attribute, because this will tell us what pages to run this script on. We’ll want our drop-down to appear on every page, and the asterisk after Digital Web’s home url (/*) signifies this.

At this point, we’ll save our script (although all it does is display an alert box) as digitalweb.user.js (the user.js tells Firefox that this is a Greasemonkey script). To load it, all we have to do is the following:

  • Open up Firefox
  • Go to File/Open File
  • Browse to where we saved digitalweb.user.js and click “OK”
  • We’ll then be presented with the following dialogue, which we’ll click “Install”:Greasemonkey installer panel

If you open up your Greasemonkey preferences, you should see our script is loaded and enabled:

Loaded scripts in Greasemonkey

If you navigate to a page within the Digital Web domain, our script is added to the returned HTML. To test, navigate to the site homepage—when the page loads, you should see an alert box that reads “My script is working”.

With our script loaded, we can now start to edit it. What we’ll do first is open up the Greasemonkey preferences, select our script from the left-hand column, and click edit. Note: You DO NOT want to open the script directly, because to get any changes you make to it, you’ll have to go through the script install process again.

Retrieving topic data

We’re now ready to start actually writing code for our script. The first thing we want to do is to retrieve the topics for the drop-down menu. Typically with Ajax, you’ll make a remote call from client-side script via the XMLHttpRequest object. With Firefox, you are not allowed (by /files/includes/default.css) to make requests to pages outside of the domain where the script is running. Greasemonkey comes with a built in function called GM_xmlhttpRequest which will allow you to make cross-domain calls. We’ll use this function here, just to demonstrate how to use it in case you’d like to make cross-domain calls in the future.

We’ll first create a global variable called aryTopics, which will be a multi-dimensional array responsible for holding our topic data. It will hold a topic name and the page URL for that topic data. After that, we’ll grab the HTML located at /topics/:

// UserScript // @name           Browse By Type for digital-web.com // @namespace      digital-web // @include        /* // /UserScript  var aryTopics = new Array();  GM_xmlhttpRequest({     method: 'GET',     url: '/topics/',     onload: function(responseDetails)      {         GM_log(responseDetails.responseText)     }      });

What happens here is we make a request to the topic page, and when it loads, we make a call to an anonymous function that takes the HTML page (responseDetails) as a parameter. Then to show things are working, we’ll display the HTML in Firefox’s error console by calling another Greasemonkey built-in function called GM_log. GM_Log /files/includes/print.csss messages to Firefox’s error console. To open the error console, go to “Tools/Error Console” on the Firefox menu. You should see something like this:

Error console output

Now that we have the HTML being returned to us, we can look for a way to get at the topics on the page. If you navigate to /topics/ and view the page source (“View/Page Source” in Firefox), you’ll see the topics are displayed in a list as:

<div id="content" class="column">    <div id="storyList">   <h1>Articles by Topic: Basics</h1>   <ol>   <li>     <h2><a href="/articles/collecting_for_design/">       Collecting for Design</a>     </h2>     <p class="date">Published on November 26, 2007</p>   </li> 

We need to obtain the contents of the div tag with the id content. After doing that, we can then obtain all the anchor tags, which contain the topic details we will need. We will assign the text in between the anchor tags for the drop-down box’s options, and assign the href attribute to the option value. At this point our script looks like:

// UserScript // @name           Browse By Type for digital-web.com // @namespace      digital-web // @include        /* // /UserScript  var aryTopics = new Array();  GM_xmlhttpRequest({    method: 'GET',    url: '/topics/',    onload: function(responseDetails)     {      //Create a div to store the returned html in. We need to do this     //so we can use the DOM to get at the topics      var oDiv = document.createElement('div');      oDiv.setAttribute('id', 'gmTopicList');      oDiv.setAttribute('name', 'gmTopicList');      oDiv.innerHTML = responseDetails.responseText;       //get all the divs in the returned html.  The content div      //is what we want, because that's where the topics are      var o = oDiv.getElementsByTagName("div");       for(var i=0;i<=o.length-1;i++)      {        if(o[i].id=="content")        {          var oAnchors = o[i].getElementsByTagName("a");           for(z=0;z<=oAnchors.length-1;z++)          {            aryTopics[z]=[oAnchors[z].href,oAnchors[z].text];          }          aryTopics.sort();          break;        }      }      alert(aryTopics.length);    } });

Our script has now loaded up an array with topic values. Let’s run a quick test to make sure all is well. You’ll see the following line as one of the last lines in the script:

alert(aryTopics,length);

If you refresh /, you should see an alert that displays the number of topics in our arrays (I get 64, but by the time you read this the number of topics could be different).

Creating a new topics drop-down list

Now with our topics in hand, we’re able to go and create a drop-down box and display them. We’ll create a new function in our script called createOptionsForSelect(). We’ll place the call to the function inside of our anonymous function. So what we’ll do is replace:

GM_log(aryTopics[0][1]);

with

createOptionsForSelect();

This is important for syncing reasons; we need to make sure we get our topics back from our Ajax call before trying to do something with them.

The first thing we need to decide is where our drop-down will appear. For me, the logical place is right above the left-hand navigation links for filtering the site’s articles:

Drop-down menu in effect

So what we’ll need to do is view the source of this page and see where those links are. We see they’re contained within a div tag entitled sortNav. What we’ll do then is create a new select element within that div. We’ll do this by:

  • creating a string variable to hold all of our options
  • call the sort() method of the array so the options are displayed alphabetically
  • loop through our array of topics, and append a new option for each topic
  • adding the select and options to the innerHTML of the div

Our script, with our new function added, looks like this so far:

 function createOptionsForSelect() {   var list = document.getElementById("sortNav");   var options="<option value=''>Filter by Topic</option>";      aryTopics.sort();    for(var i=0;i<=aryTopics.length-1;i++)   {     options+="<option value='" + aryTopics[i][0] + "'>" + ...       aryTopics[i][1] + "</option>";   }    list.innerHTML = "<select id='gsFilter' " + "style='width: 180px'>" ...     + options + "</select>" + list.innerHTML;            var oSelect= document.getElementById("gsFilter");  }

If we save and refresh the main page, we should see a select box populated with the options at the top of the left-hand navigation:

Populated drop-down

You’ll see that nothing happens when we select a topic from our drop-down. We need to wire an event to occur each time an option changes. What we want to do is to use Ajax to retrieve the contents from the selected option’s URL. We add the following lines of code to do just that:

var oSelect= document.getElementById("gsFilter"); oSelect.addEventListener("change",loadPage, true); 

As you can see above, when the drop-down options change, we call a function entitled loadPage(). This is the final function we’ll create for this script.

Fetching and displaying the topic list

The first thing we’ll need to do in our loadPage() function is to get the value of the selected option in our drop-down. Again, the drop-down value holds the exact URL for the page we need to display. For instance, the option value for blogging is “/topics/blogging”. Knowing this, we can then use GM_xmlhttpRequest to retrieve the HTML for that page:

function loadPage() {   var page = document.getElementById("gsFilter") ...     .options[document.getElementById("gsFilter") ...     .options.selectedIndex].value;      GM_xmlhttpRequest({     method: 'GET',     url: page,     onload: function(responseDetails)      {          }   }); } 

Now that we have the actual content that we need, we have to figure out what to display and where to display it. If we open up the source of both the topic URL page and our main page (/), we see that both pages display their main content in div tags entitled content. This makes thing nice, because all we’ll be doing is taking the HTML from the main page and replacing it with the HTML from the topic page.

There is one little detail that gave me trouble when doing this, and it had to do with the header tags (ie, h1,h2, etc) within each content div. The main page’s content starts off using an h2 tag, because an h1 tag is used outside of this div first. The first header tag returned from our topic’s content div tag is an h1 (not an h2 tag). I know, you’re saying “Who cares?”, but the ordering of the tags really messed up the HTML when loaded. Anyhow, I got around this by downgrading the header tags (change h2 to h1, h3 to h2) returned from the topic HTML.

Our complete script, with the loadPage() function, is as follows:

// UserScript // @name           Browse By Type for digital-web.com // @namespace      digital-web // @include        /* // /UserScript  var aryTopics = new Array();  GM_xmlhttpRequest({   method: 'GET',   url: '/topics/',   onload: function(responseDetails)    {     //Create a div to store the returned html in. We need to do this     //so we can use the DOM to get at the topics     var oDiv = document.createElement('div');     oDiv.setAttribute('id', 'gmTopicList');     oDiv.setAttribute('name', 'gmTopicList');     oDiv.innerHTML = responseDetails.responseText;          //get all the divs in the returned html.  The content div     //is what we want, because that's where the topics are     var o = oDiv.getElementsByTagName("div");          for(var i=0;i<=o.length-1;i++)     {       if(o[i].id=="content")       {         var oAnchors = o[i].getElementsByTagName("a");                  for(z=0;z<=oAnchors.length-1;z++)         {           aryTopics[z]=[oAnchors[z].href,oAnchors[z].text];         }                break;       }     }        createOptionsForSelect();   } });  function loadPage() {   var page = document.getElementById("gsFilter") ...     .options[document.getElementById("gsFilter") ...      .options.selectedIndex].value;      GM_xmlhttpRequest({     method: 'GET',     url: page,     onload: function(responseDetails)      {       var s = responseDetails.responseText;       var oDiv = document.createElement('div');              oDiv.setAttribute('id', 'gmContent');       oDiv.innerHTML = responseDetails.responseText;              var o = oDiv.getElementsByTagName("div");              for(var i=0;i<=o.length-1;i++)       {         if(o[i].id=="storyList")         {           o[i].id="currentFeature";         }       }              for(var i=0;i<=o.length-1;i++)       {         if(o[i].id=="content")         {           //Need to swap some header tags so html returned from           //topic page plays nice with the main page           var s = o[i].innerHTML           s=s.replace(/h2>/gi,"h3>");           s=s.replace(/h1>/gi,"h2>");           document.getElementById("content").innerHTML="";           document.getElementById("content").innerHTML=s;           break;         }              }     }   }); }  function createOptionsForSelect() {   var list = document.getElementById("sortNav");   var options="<option value=''>Filter by Topic</option>";      aryTopics.sort();      for(var i=0;i<=aryTopics.length-1;i++)   {     options+="<option value='" + aryTopics[i] + "'>" + aryTopics[i] + "</option>";   }      list.innerHTML = "<select id='gsFilter' " + ...      "style='width: 180px'>" + options + "<select>" + list.innerHTML;      var oSelect= document.getElementById("gsFilter");   oSelect.addEventListener("change",loadPage, true); } 

I conclude you’re ready to hack

Greasemonkey is an awesome tool that can change the way any web page looks or acts. It truly allows you to have things the way you want them. Don’t like where I put the drop-down box in this example? Then change it! That’s what Greasemonkey is all about.

Resources

Got something to say?

Share your comments  with other professionals (2 comments)

Related Topics: Programming, Browsers

Jeff Rudesyle lives and works as a Web Developer in Shippenville, PA. He is a proud husband and father of four. When he’s not blogging or complaining about the Philadelphia Eagles, you can find him drinking beer, coaching soccer, or hunting, but never all at once.