Greasemonkey Hacks/Developer Tools

From WikiContent

< Greasemonkey Hacks(Difference between revisions)
Jump to: navigation, search
(Initial conversion from Docbook)
Current revision (23:10, 11 March 2008) (edit) (undo)
(Initial conversion from Docbook)
 

Current revision

Greasemonkey Hacks


Contents

Hacks 40–46: Introduction

This chapter is devoted to tools for web developers. More than anyone else, developers of web sites and web applications know the limitations of the medium. Browsers are optimized for viewing and interacting, not authoring. Yet the browser is where the web developer is forced to work.

Recent advances in web technology have made these limitations even clearer. Developers can no longer rely on tried and true methods of tablebased layouts. Now it's all CSS, with page styles scattered through hundreds of different rules in several different files. Likewise, web forms can't just be submitted anymore. For the best user experience, you need to submit data in the background with XMLHttpRequest and refresh the page without reloading it.

Does this make for a better Web? Undoubtedly. Does it make the browser a better web development environment? Not a chance.

The hacks in this chapter have helped me in my own web development projects. I hope they help you, too.

Remove All Page Styles on Selected Sites

Disable all CSS styling on sites that go out of their way to make themselves unreadable.

Firefox has options to ignore fonts and colors defined on web pages, buried behind the Colors button in the Preferences window. These are global settings, and they affect every site you visit until you go back to the preferences dialog and change them. They're also deceptively incomplete; disabling page fonts will affect which font is used, but Firefox will still respect other font styles defined by the page: italics, bold, even font size. Firefox has an option (under View → Page Style) to completely disable a page's style, but this is a temporary setting that resets as soon as you follow a link or refresh the page.

This hack aims for a middle ground. It disables all styles on selected sites, based on the list of pages you include in the script configuration.

The Code

This user script runs on all pages, but you will probably want to modify the @include line to include just the sites that annoy you (unless you really like browsing the Web as if it were 1992). This script removes three types of styling:

  • Styles defined in externally linked stylesheets. Firefox helpfully collects these in the document.styleSheets collection. (Note the camelCase capitalization!)
  • Styles defined in <style> elements in the <head> section of the page.
  • Styles defined on individual elements, either with the style attribute, or a wide variety of proprietary but supported attributes, such as size, face, color, bgcolor, and background.

Save the following user script as unstyle.user.js:

	// ==UserScript==
	// @name		Unstyle
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description remove all CSS (Cascading Style Sheets)CSS styles
	// @include		*
	// ==/UserScript==

	// disable all externally linked stylesheets
	for (var i = document.styleSheets.length - 1; i >= 0; i--) {
		document.styleSheets[i].disabled = true;
	}

	var arAllElements = (typeof document.all != 'undefined') ?
		document.all : document.getElementsByTagName('*');
	for (var i = arAllElements.length - 1; i >= 0; i--) {
		var elmOne = arAllElements[i];
		if (elmOne.nodeName.toUpperCase() == 'STYLE') {
			// remove <style> elements defined in the page <head>
			elmOne.parentNode.removeChild(element);
		} else { 
			// remove per-element styles and style-related attributes
			elmOne.setAttribute('style', '');
			elmOne.setAttribute('size', '');
			elmOne.setAttribute('face', '');
			elmOne.setAttribute('color', '');
			elmOne.setAttribute('bgcolor', '');
			elmOne.setAttribute('background', '');
		}
	}

Running the Hack

Before installing the user script, go to http://diveintomark.org. Take a moment to appreciate my lovely page design, shown in Figure 5-1, on which I slaved and fretted for many long hours on the offhand chance that someone like you would stumble onto my site.

Figure 5-1. http://diveintomark.org with original styles

http://diveintomark.org with original styles

Now, install the user script (Tools → Install This User Script), and refresh http://diveintomark.org. The page now displays without any styling at all, as shown in Figure 5-2.

It is still surprisingly readable, thanks to my clean markup and proper use of HTML elements. Not all sites will change this radically when you remove their page styles. For example, a site that uses nested tables for layout will still look more or less the same, since this script does not alter the table structure.

Figure 5-2. http://diveintomark.org unstyled

http://diveintomark.org unstyled

Tip

Using proper HTML markup (supplemented with CSS for styling) can help your rank in search engines. This hack really shows how search engines see your site: just the HTML markup, without CSS styling. This seems obvious, but many people seem to be under the impression that Google indexes sites by loading them up in Internet Explorer and taking screenshots.

Hacking the Hack

As I mentioned before, you can change where this script runs by changing the @include line in the script's metadata section. If you want to unstyle only http://diveintomark.org, change the @include line from this:

	@include *

to this:

	@include http://diveintomark.org/*

Refresh Pages Automatically

Reload selected pages every 20 minutes.

Although it's not generally considered "friendly" behavior, there are several reasons why you might want to have some pages refresh themselves automatically. One is simply to keep an eye on the latest news. Another is to keep your login sessions alive longer, on sites that log you out after a period of inactivity.

Greasemonkey allows a lot of freedom, and many user scripts abuse it and behave badly. I recommend moderation and common sense when creating additional load on other people's web servers. A delay of 20 minutes seems reasonable, so that's the default I used for this script.

The Code

This is one of the simplest user scripts you can imagine. When it executes, it sets a timer to call a function after a delay. The function in this case is document.location.reload, which reloads the page.

Tip

Technically, timers are not threads; they simply interrupt whatever is executing at the time. But they are the closest thing to multithreading in JavaScript. You will see setTimeout and its cousin setInterval in many scripts that animate the user interface.

The multiplying factor, 60*1000, converts the timeout delay from minutes to milliseconds, as required by the setTimeout function.

Tip

Although it's ignored in this script, setTimeout has a return value: the ID for the timer that was set. With this ID, it is possible to cancel the timer by calling clearTimeout.

Save the following user script as autoreload.user.js:

	// ==UserScript==
	// @name		Auto Reload
	// @namespace	http://blog.monstuff.com/archives/cat_greasemonkey.html
	// @description Reload pages every 20 minutes
	// @include		http://slashdot.org/
	// @include		http://www.slashdot.org/
	// ==/UserScript==

	// based on code by Julien Couvreur
	// and included here with his gracious permission

	var numMinutes = 20;
	window.setTimeout("document.location.reload();", numMinutes*60*1000);

Running the Hack

Before installing this script, configure the URLs that you want to reload automatically. You can do this by editing the script, or by adding URLs in the install dialog. Slashdot (http://slashdot.org) is included by default. If you open the Slashdot front page, it will now reload every 20 minutes, showing you the latest news.

You can also modify the frequency for refreshing in the script, by changing the numMinutes variable.

Julien Couvreur

Make External Stylesheets Clickable

Ever want to see a page's stylesheets? Stop digging through source code to find them.

Have you ever seen a standards-based site that used CSS in an innovative way, and you asked yourself, "How did they do that?" Then you had to view source, scan through all those angle brackets, find the link to the stylesheet, and load it manually in your browser. Make it easier on yourself! This hack adds a navigation bar along the top of each page with links to each of the page's stylesheets.

The Code

This user script runs on all web pages. It relies on the fact that Firefox maintains a global list of stylesheets, document.styleSheets (note the camelCase capitalization).

There is just one problem: if the page defines additional styles inline, such as with a <style> element in the <head> of the page, or in a style attribute on one specific element, Firefox creates a separate entry for each style in the document.styleSheets list, using the page's URL as the address of the stylesheet (which is technically true, but unhelpful for our purposes). As we loop through document.styleSheets, we need to check for this condition and filter out stylesheets that point back to the current page.

Save the following user script as showstylesheets.user.js:

	// ==UserScript==
	// @name		Show Stylesheets
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description adds links to all of page's stylesheets
	// @include		http://*
	// @include		https://*
	// ==/UserScript==

	var arHtmlStylesheetLinks = new Array();
	for (var i = document.styleSheets.length - 1; i >= 0; i--) {
		var oStylesheet = document.styleSheets[i];
		if (oStylesheet.href == location.href) continue;
		var ssMediaText = oStylesheet.media.mediaText;
		if (ssMediaText) {
				ssMediaText = 'media=&quot;' + ssMediaText + '&quot;';
		}
		arHtmlStylesheetLinks.push('<a title="' +
			ssMediaText + '" href="' +
			oStylesheet.href + '">' +
			oStylesheet.href.split('/').pop() + '</a>');
	}
	if (!arHtmlStylesheetLinks.length) return;
	var elmWrapperDiv = document.createElement('div');
	elmWrapperDiv.innerHTML = 'Stylesheets: ' +
		arHtmlStylesheetLinks.join(' &middot; ');
	elmWrapperDiv.style.textAlign = 'center';
	elmWrapperDiv.style.fontSize = 'small';
	elmWrapperDiv.style.fontFamily = 'sans-serif';
	document.body.insertBefore(elmWrapperDiv, document.body.firstChild);

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.fsf.org. At the top of the page, you will see a list of links showing all the page's stylesheets. If you hover the cursor over one of the stylesheet links, you will see a tool tip that gives more information about the stylesheet, such as whether it is meant for screen or print media, as shown in Figure 5-3.

Clicking a stylesheet link displays the stylesheet in your browser, as shown in Figure 5-4.

Hacking the Hack

This hack displays only external stylesheets; it goes out of its way to filter out references to inline styles. However, you might want to know whether the page defines any inline styles. As we loop through document.styleSheets, if we find a stylesheet that points back to the original page, we can create a special type of link that will open the source view of the current page (i.e., the view you would get if you selected View Source on the page). An example of a page with inline styles is Amazon.com, as shown in Figure 5-5.

Figure 5-3. FSF's stylesheets

FSF's stylesheets

Save the following user script as showstylesheets2.user.js:

	// ==UserScript==
	// @name		Show Stylesheets 2
	// @namespace   http://diveintomark.org/projects/greasemonkey/
	// @description adds links to all of page's stylesheets + inline styles
	// @include		*
	// ==/UserScript==

	var arHtmlStylesheetLinks = new Array();
	var bHasInlineStyles = false;
	for (var i = document.styleSheets.length - 1; i >= 0; i--){
		var oStylesheet = document.styleSheets[i];

		if (oStylesheet.href == location.href) {
			bHasInlineStyles = true;
		}

		var ssMediaText = oStylesheet.media.mediaText;
		if (ssMediaText) {

		ssMediaText = 'media=&quot;' + ssMediaText + '&quot;';
	}
	arHtmlStylesheetLinks.push('<a title="' +
		ssMediaText + '" href="' +
		oStylesheet.href + '">' +
		oStylesheet.href.split('/').pop() + '</a>');
	}	

	if (bHasInlineStyles) {
		arHtmlStylesheetLinks.push('<a href="view-source:' +
			location + '">inline styles</a>');
	}

	if (!arHtmlStylesheetLinks.length) return;
	var elmWrapperDiv = document.createElement('div');
	elmWrapperDiv.innerHTML = 'external stylesheetsStylesheets: ' +
		arHtmlStylesheetLinks.join(' &middot; ');
	elmWrapperDiv.style.textAlign = 'center';
	elmWrapperDiv.style.fontSize = 'small';
	elmWrapperDiv.style.fontFamily = 'sans-serif';
	document.body.insertBefore(elmWrapperDiv, document.body.firstChild);

Figure 5-4. FSF's print stylesheet

FSF's print stylesheet

Figure 5-5. Page with inline styles

Page with inline styles

Show Image Information

Generate a report of all the images on a page.

Here's a feature I've always wanted and never found in a browser: the ability to generate a report that shows all possible information about all the images on a page. It would be extremely helpful in debugging my own complex web pages, and it's just generally useful and fun to get to see a different view of the images that constitute someone else's site. Firefox sort of does this, in the Media tab of the Page Info dialog. But it's unwieldy to use for complex pages, since it only shows you the URL and type of each image, not the image itself.

The Code

This user script runs on all web pages. The code is divided into three parts:

  1. Create the link that the user clicks to generate and display the image report. This is positioned in the lower-left corner of the screen with position: fixed, so it will remain anchored there even if the user scrolls the page.
  2. Once the user clicks the "Image report" link, cycle through all the images (using the document.images collection) and gather the information on each image by using a combination of the image's attributes (alt, title, src) and the image's style (by calling the getComputedStyle function).
  3. This is the really magical part. Instead of trying to display the report on the original page (which might react badly with the page's style or layout), this script generates a data: URL that contains the complete HTML source of the report and sets the window location to the data: URL. This creates the illusion of following a link to a separate report page, which seems normal enough until you realize that the report page isn't generated by or stored on a remote server. Everything is done entirely on the end user's machine.

Tip

data: URLs are defined in RFC 2397, available online at http://www.ietf.org/rfc/rfc2397.

Save the following user script as showimageinformation.user.js:

	// ==UserScript==
	// @name		developer toolsshow image informationShow Image Information
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description display information on all images on a page
	// @include		http://*
	// ==/UserScript==

	var elmWrapper = document.createElement('div');
	elmWrapper.innerHTML = '<div style="position: fixed; bottom: 0; ' +
		'left: 0; padding: 1px 4px 3px 4px; background-color: #ddd; ' +
		'color: #000; border-top: 1px solid #bbb; border-left: 1px ' +
		'solid #bbb; font-family: sans-serif; font-size: x-small;">' +
		'<a href="#" title="Display report of all images on this page" ' +
		'id="displayinfo" style="background-color: transparent; ' + 
		'color: black; font-size: x-small; font-family: sans-serif; ' +
		'text-decoration: none;">Image report</a></div>';
	document.body.append(elmWrapper);

	document.getElementById('displayinfo').addEventListener(
		'click', function(event) {
		var html = '<html><head><title>' + document.title +
			'</title></head><body>';
		var oImages = new Object();
		for (var i = 0; i < document.images.length; i++) {
			var elmImage = document.images[i];
			var urlSrc = elmImage.src || '';
			if (!urlSrc) { continue; }
			if (oImages[urlSrc]) { continue; }
			oImages[urlSrc] = 1;
			var style = getComputedStyle(elmImage, '');
			var iWidth = parseInt(style.width);
			var iHeight = parseInt(style.height);
			var sTitle = elmImage.title || '';
			var sAlt = elmImage.alt || '';
			var urlLongdesc = elmImage.longdesc || '';
			html += '<p><img width="' + iWidth + '" height="' + iHeight +
				'" src="' + urlSrc + '"></p><table border="1" ' +
				'cellpadding="3" cellspacing="0"><tr><th>src</th><td>' +
				'<a href="' + urlSrc + '">' + urlSrc + '</a></td></tr>' +
				'<tr><th>width</th><td>' + iWidth + '</td></tr>' + 
				'<tr><th>height</th><td>' + iHeight + '</td></tr>';
			if (sTitle) {
				html += '<tr><th>title</th><td>' + sTitle + '</td></tr>';
			}
			if (sAlt) {
				html += '<tr><th>alt</th><td>' + sAlt + '</td></tr>';
			}
			if (urlLongdesc) {
				html += '<tr><th>longdesc</th><td><a href="' + urlLongdesc +
				'">' + urlLongdesc + '</a></td></tr>';
			}
			html += '</table><br><hr>';
		}
		html += '</body></html>';
		GM_openInTab('data:text/html,' + html);
		event.preventDefault();
	}, true);

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.oreilly.com. In the bottom-left corner of the screen, you will see a link titled "Image report," as shown in Figure 5-6.

Click the small "Image report" link in the lower-left corner of your browser window, and you will see an autogenerated report of all the images on the page, as shown in Figure 5-7.

Figure 5-6. O'Reilly home page with "Image report" button

O'Reilly home page with "Image report" button

Figure 5-7. O'Reilly home page image report

O'Reilly home page image report

The report includes the source URL of each image, the image's dimensions, and the image's alternate text and title (if defined). You can click any image URL to see that image in isolation.

Filter Code Examples on MSDN

Display only the MSDN code samples and APIs for the languages you care about.

One thing has always bugged me about the MSDN reference pages. Viewing them locally within Visual Studio allows you to hide the code snippets for languages you're not interested in, but viewing them online always displays code examples in every language. If you're a VB programmer, you probably don't care about C# snippets, and vice versa.

This hack allows you to choose which language you care about and hides other code samples in the online MSDN documentation.

The Code

This user script runs on http://msdn.microsoft.com. The biggest question for overlaying the feature on top of MSDN is, "How structured is the content? How easy is it to identify sections showing a specific language?" Even though the markup isn't as clean as I had hoped, it is barely regular enough that I was able to filter code examples by language.

When I looked at the source of some MSDN reference pages, the markup for code snippets read something like this:

	              <grouping>
		<span class="lang">C#</span> …many nodes…
		<span class="lang">JScript</span> …many nodes…
	</grouping>
            

The grouping tag varies from page to page. Sometimes it's a <div>, but I also found <pre> elements on some pages. Although this markup is good enough for styling the page, it doesn't lend itself to easy filtering. Each language section doesn't have its own container, which makes it difficult to identify all the DOM nodes for the code sample.

The script starts by finding all the span elements that have a class="lang" attribute. It then scans the content of each <span> to identify known language names. The ShowCS, ShowVB, ShowCPP, and ShowJScript variables let you customize which languages to show or hide. If the code sample is an identifiable language and the corresponding Show variable is true, we keep it; otherwise, we remove it.

Finally, the CleanSpan function handles filtering out a language section, for a given starting <span>, by also providing the next known language <span> (if any). It removes all the sibling nodes that follow the starting <span>, until it reaches the <span> for the following language section or until there is no next sibling node (i.e., until we reach the end of the grouping). This is the best we can do, given the paucity of structured markup.

Save the following user script as MSDNLanguageFilter.user.js:

	// ==UserScript==
	// @name		MSDN Language Filter
	// @namespace	http://blog.monstuff.com/archives/cat_greasemonkey.html
	// @description Allows you to filter the samples on MSDN for certain
languages
	// @include		http://msdn.microsoft.com/*
	// ==/UserScript==

	// based on code filters for MSDN reference pagescode by Julien Couvreur
	// and included here with his gracious permission

	var ShowCPP = false;
	var ShowVB = false;
	var ShowJScript = false;
	var ShowCS = true;

	var MSDN (Microsoft Developer Network) reference pagesMSDNLanguageFilter = {
		FilterLanguages: function()
		{	
			var xpath = "//span[@class = 'lang']";
			var res = document.evaluate(xpath, document, null,
				XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

			for (var i = 0; i < res.snapshotLength; i++)
			{
				var spanHTML = res.snapshotItem(i).innerHTML;

				var isVB = (spanHTML.match(/Visual.*Basic/i) != null);
				var isCS = (spanHTML.match(/C#/i) != null);
				var isCPP = (spanHTML.match(/C\+\+/i) != null);
				var isJScript = (spanHTML.match(/JScript/i) != null);

				if (!isVB && !isCS && !isCPP && !isJScript)
				{
				return;
				}	

				var keepLang =
				(isCPP && ShowCPP) ||
				(isCS && ShowCS) ||
				(isVB && ShowVB) ||
				(isJScript && ShowJScript) ||
				(!isCPP && !isCS && !isVB && !isJScript);
			  
				if (!keepLang)
				{
				this.CleanSpan(res.snapshotItem(i), res.snapshotItem(i+1));
				}
			}
		},

		CleanSpan: function(startSpan, endSpan)
		{
			var currentNode = startSpan;
			while (currentNode != null &&
				(endSpan == null || currentNode != endSpan))
			{
				var nextNode = currentNode.nextSibling;
				currentNode.parentNode.removeChild(currentNode);
				currentNode = nextNode;
			}

		}
	 }

	 MSDN (Microsoft Developer Network) reference pagesMSDNLanguageFilter.FilterLanguages();

Running the Hack

Before installing the user script, navigate to http://msdn.microsoft.com and search for console.write. Select the first search result. As shown in Figure 5-8, each of the signatures listed has four variants, once for each Microsoft language.

Figure 5-8. Code samples in four languages

Code samples in four languages

Now, install the user script (Tools → Install This User Script) and refresh the MSDN page. The script hides unwanted code examples, and the page will look significantly less cluttered, as shown in Figure 5-9.

You can edit the script to configure which languages you want to filter out. Go to Tools → Manage User Scripts, select MSDN Language Filter from the list of installed scripts, and click Edit to live edit your installed copy [Hack #9]. Change the ShowVB, ShowCS, ShowCPP, and ShowJScript variables to true or false to set the languages you want to see.

Julien Couvreur

Figure 5-9. Filtered code samples

Filtered code samples

Intercept and Modify Form Submissions

Gain the ultimate control over web forms.

Web forms have two modes: GET and POST. Search engines are examples of forms that use the GET method; once you submit the form, you can see all the form values in the URL of the next page. POST forms, on the other hand, are opaque. You can tell you submitted the form, but the URL doesn't reveal any information about what data you actually submitted. Most e-commerce sites, weblog commenting applications, and even some site searches use the POST method.

This hack gives you the ultimate control over POST forms. When you submit the form, it pops up a window that displays all the form fields and their values, and lets you edit any of the fields—even fields that were hidden on the original page.

The Code

This user script runs on all pages. It looks for forms with a method="POST" attribute and adds an onsubmit event. As discussed in "Enter Textile Markup in Web Forms" [Hack #35], this is not enough to guarantee that the script will intercept all form submissions, so we also override the submit method on the HTMLFormElement class.

When the user attempts to submit a form, the script looks for all editable or hidden form fields and constructs a form-editing window. In this window, the user can modify the value of any form field and submit the modified values, submit the original form values, or cancel the form submission altogether.

Save the following user script as post-interceptor.user.js:

	// ==UserScript==
	// @name POST Interceptor
	// @description Intercept POST requests and let user modify before submit
	// @namespace http://kailasa.net/prakash/greasemonkey/
	// @include http*://example.com/*
	// ==/UserScript==
	
	// based on code by Prakash Kailasa
	// and included here with his gracious permission

	//
	// IMPORTANT: Be sure to change/add @include lines for the sites you
	//			  want the script to work on

	const POST_INTERCEPT = 'PostIntercept';
	var intercept_on;
	var is_modified = false;

	function toggle_intercept(flag)
	{
		intercept_on = flag;
		GM_setValue(POST_INTERCEPT, intercept_on);
		setup_pi_button();
	}

	function setup_pi_button()
	{
		var pi = document.getElementById('__pi_toggle');
		if (!pi) {
				pi = new_node('span', '__pi_toggle');	
				pi.textContent = '[PI]';
				document.getElementsByTagName('body')[0].appendChild(pi);
				pi.addEventListener('click',
						 function() {toggle_intercept(!intercept_
	on)},
						 false);
				
			var pi_toggle_style = ' \
		#__pi_toggle { \ 
		  position: fixed; \
		  bottom: 0; right: 0; \
		  display: inline; \

		 padding: 2px; \
		 font: caption; \
		 font-weight: bold; \
		 cursor: crosshair; \
	   } \
	   #__pi_toggle:hover { \ 
	     border-width: 2px 0 0 2px; \
		 border-style: solid none none solid; \
		 border-color: black; \
	   } \
	   ';
				add_style("__pi_toggle_style", pi_toggle_style);
			}

			if (intercept_on) {
				pi.textContent = '[PI] is On';
				pi.setAttribute('title', 'Click to turn POST Interceptor Off');
				pi.style.backgroundColor = '#0c2369';
				pi.style.color = '#ddff00';
			} else {
				pi.textContent = '[PI] is Off';
				pi.setAttribute('title', 'Click to turn POST Interceptor On');
				pi.style.backgroundColor = '#ccc';
				pi.style.color = '#888';
		    }
		}

		function interceptor_setup()
		{
			if (typeof GM_getValue != 'undefined') {
				intercept_on = GM_getValue(POST_INTERCEPT, false);
				setup_pi_button();
			} else {
				intercept_on = true;
			}

			// override submit formsinterception and modificationhandling
			developer toolsform submissionsHTMLFormElement.prototype.real_submit = HTMLFormElement.prototype.
		submit;
			HTMLFormElement.prototype.submit = interceptor;
			
			// define our 'submit' handler on window, to avoid defining
			// on individual forms
			window.addEventListener('submit', function(e) {
				// stop the event before it gets to the element and causes
		onsubmit to
				// get called.
				e.stopPropagation();

				// stop the form from submitting
				e.preventDefault();

				interceptor(e);
		}, true);
	}
	
	// interceptor: called in place of developer toolsform submissionsform.submit()
	// or as a result of submit formsinterception and modificationhandler on window (arg: event)
	function interceptor(e) {
		var frm = e ? e.target : this;
		if (!interceptor_onsubmit(frm)) { return false; }
		if (intercept_on) {
			show(frm);
			return false;
		} else {
			HTMLFormElement.prototype.real_submit.apply(frm);
		}
	}
	
	// if any form defined an onsubmit handler, it was saved earlier.
	// call it now
	function interceptor_onsubmit(f) {
		return !f.onsubmit || f.onsubmit();
	}
	
	function show(frm) {
		var content = build(frm);
		content.open();
	}
	
	function build(frm) {
		add_window_style();

		var container = new_node('div', 'post_interceptor');
		container.className = '_ _pi_window';
		var title = new_node('h1');
		title.className = '_ _pi_title';
		title.appendChild(new_text_node('Intercepting POST ' + post_url(frm)));
		container.appendChild(title);

		var note = new_node('div', '_ _pi_note');
		note.appendChild(new_text_node('Click on any value to modify it'));
		container.appendChild(note);

		var data = build_post_data(frm);
		container.appendChild(data);

		var buttons = new_node('div', '_ _pi_buttons');
		var btn_send_mod = new_node('button', '_ _pi_btn_send_mod');
		btn_send_mod.className = '_ _pi_button';
		btn_send_mod.appendChild(new_text_node('Send Modified'));
		buttons.appendChild(btn_send_mod);
		btn_send_mod.addEventListener('click', function(e) {
			submit_modified(win);
		}, false);

		var btn_send_orig = new_node('button', '_ _pi_btn_send_orig');
		btn_send_orig.className = '_ _pi_button';
		btn_send_orig.appendChild(new_text_node('Send Original'));
		buttons.appendChild(btn_send_orig);
		btn_send_orig.addEventListener('click', function(e) {
			submit_original(win);
		}, false);

		var btn_cancel = new_node('button', '_ _pi_btn_cancel');
		btn_cancel.className = '_ _pi_button';
		btn_cancel.appendChild(new_text_node('Cancel'));
		buttons.appendChild(btn_cancel);
		container.appendChild(buttons);
		btn_cancel.addEventListener('click', function(e) {
			cancel_submit(win);
		}, false);

		var win = Window(container, frm);

		return win;
	}

	// POST content
	function build_post_data(f)
	{
		var table = new_node('table');

		// heading
		var thead = new_node('thead');
		var th_row = new_node('tr');
		var attrs = new Array('name', 'type', 'value');
		for (var a = 0; a < attrs.length; a++) {
			var th = new_node('th');
			th.appendChild(new_text_node(attrs[a].ucFirst()));
			th_row.appendChild(th);
		}
		thead.appendChild(th_row);
		table.appendChild(thead);

		// data
		var tbody = new_node('tbody');
		for (var i = 0; i < f.elements.length; i++) {
			var row = new_node('tr');
			row.className = i % 2 == 0 ? '_ _pi_row_even' : '_ _pi_row_odd';
			//for (var a in attrs) {
			for (var a = 0; a < attrs.length; a++) {
				var cell = new_node('td', '_ _pi_cell_' + attrs[a] + '_' + i);
				cell.className = '_ _pi_cell_' + attrs[a];
				var data;
				if (attrs[a] == 'value') {
				data = new_node('input', '_ _pi_cell_value_text_' + i);
				data.value = f.elements[i][attrs[a]];
				data.readOnly = true;

				data.className = '_ _pi_view_field';
				data.maxLength = 1000;
				cell.addEventListener("click", show_edit, false);
				} else {
				data = new_text_node(f.elements[i][attrs[a]]);
				}
				cell.appendChild(data);
				row.appendChild(cell);
			}
			tbody.appendChild(row);
		}
		table.appendChild(tbody);
		var data = new_node('div', '_ _pi_post_info');
		data.className = '_ _pi_post_info';
		data.appendChild(table);

		return data;
	}

	// hide value formsinterception and modificationand show edit field

	function show_edit(e)
	{
	
		var view, cell;
			if (e.target.nodeName == 'INPUT') {	
				view = e.target;
				cell = view.parentNode;
			} else {
				cell = e.target;
				view = cell.firstChild;
			}
			view._ _origValue = view.value;
			view.className = '_ _pi_edit_field';
			view.readOnly = false;
			view.addEventListener("blur", show_view, false);
		}
		
		// hide edit field and show modified value
		function show_view(e)
			{
				var view = e.target;
				view.className = '_ _pi_view_field';
				view.addEventListener("click", show_edit, false);
				if (view.value != view._ _origValue) {
				is_modified = true;
				view.parentNode.parentNode.className += ' _ _pi_modified';
				}
			}

			// build POST url
		function post_url(f)
		{
			// absolute URL?

		if (f.action.match(/^https?:/))
			return f.action;
	
		// relative URL; build complete URL
		var url = document.location.protocol + '//' + document.location.
	hostname;
		if (f.action.match(/^\//)) {
			url += f.action;
		} else {
				url += document.location.pathname + '/' + f.action;
		}
		return url;
	}

	// cancel submit; just close the Interceptor window
	function cancel_submit(win) {
		win.close();
	}

	// ignore developer toolsform submissionsform modifications formsinterception and modificationand submit original form
	function submit_original(win) {
		win.close();
		HTMLFormElement.prototype.real_submit.apply(win.form);
	}

	// submit form with modified parameters
	function submit_modified(win) {
		if (is_modified) {
			update_form(win);
		}
		submit_original(win);
	}

	// update the form being submitted with user modifications
	function update_form(win) {
		var f = win.form;
		var diff = 'submitting ' + f.name + ':\n';
		for (var i = 0; i > f.elements.length; i++) {
			var edit = document.getElementById('_ _pi_cell_value_text_' + i);
			if (edit && edit.value != f.elements[i].value) {
				diff += f.elements[i].name + ': |' +
				f.elements[i].value + '| -> |' + edit.value + '|\n';
				// update the original form param
				f.elements[i].value = edit.value;
			}
		}
	}

	// helper functions
	function new_node(type, id) {
		var node = document.createElement(type);
		if (id && id.length > 0) {
			node.id = id;

		}
		return node;
	}
	
	function new_text_node(txt) {
		return document.createTextNode(txt);
	}

	function add_style(style_id, style_rules) {
		if (document.getElementById(style_id)) {
			return;
		}
		var style = new_node("style", style_id);
		style.type = "text/css";
		style.innerHTML = style_rules;
		document.getElementsByTagName('head')[0].appendChild(style);
	}

	// style for the interceptor window
	function add_window_style() {
		var pi_style_rules = ' \
	.post_interceptor { \
	margin: 0; padding: 0; \
	} \
	 \
	._ _pi_window { \
	  background-color: #bfbfff; \
	  border-color: #000040; \
	  border-style: solid; \
	  border-width: 2px; \
	  /* opacity: .90; */ \
	  margin: 0px; \
	  padding: 1px 2px; \
	  position: absolute; \
	  text-align: center; \
	  visibility: hidden; \
	  \
	  -moz-border-radius: 15px; \
	 } \
	  \
	 ._ _pi_title { \
	  background-color: #4040ff; \
	  color: #ffffff; \
	  margin: 1px; padding: 1px; \
	  font: caption; \
	  font-weight: bold; \
	  text-align: center; \
	  white-space: nowrap; \
	  overflow: hidden; \
	 \
	  -moz-border-radius: 20px; \
	 } \
	  \

	#_ _pi_note { \
	  border: solid 0px black; \
	  color: #800000; \
	  margin: 0; \
	  font: caption; \
	  font-weight: bold; \
	  text-align: center; \
	} \
	 \
	#_ _pi_buttons { \
	  width: 99%; \
	  text-align: center; \
	  position: absolute; \
	  bottom: 5px; \
	} \
	 \
	._ _pi_button { \
      background-color: #4040ff; \
	  color: #fff; \
	  margin: 0 5px; padding: 2px; \
	  font: icon; \
	  font-weight: bold; \
	} \
	 \
	._ _pi_button:hover { \
	  background-color: #ff4040; \
	  cursor: pointer; \
	} \
	 \
	._ _pi_post_info { \
	  max-height: 335px; \
	  overflow: auto; \
	  margin: 3px 2px; padding: 0; \
	  border: 1px solid #008080; \
	} \
	 \
	._ _pi_post_info table { \
      width: 100%; \
	 font: bold .7em "sans serif"; \
	} \
     \
	._ _pi_post_info table thead tr { \
      background-color: black; \
	  color: white; \
	} \
	 \
	._ _pi_row_odd { \
      background-color: #eee; \
	} \
	 \
	._ _pi_row_even { \
      background-color: #ccc; \  
	} \

	 \
	._ _pi_view_field { \
	  background-color: inherit; \
	  border: 0px solid black; \
	  width: 20em; \
	  font: bold 1em "sans serif"; \
	} \
	 \
	._ _pi_edit_field { \
	  background-color: #ffc; \
	  color: blue; \
	  border: 1px solid black; \
	  padding: -1px; \
	  width: 20em; \
	  font: bold 1em "sans serif"; \
	} \
	 \
	tr._ _pi_modified td, tr._ _pi_modified input { \
	  color: red; \
	} \
	 \
	';
		add_style("_ _pi_style", pi_style_rules);
	}

	//===============================================================
	// Popup Window

	function Window(el, frm) {
		document.getElementsByTagName('body')[0].appendChild(el);

		var win = {
			frame: el,
			developer toolsform submissionsform: frm,
			open: function() {
				var width = 550;
				var height = 400;
				this.frame.style.width = width + 'px';
				this.frame.style.height = height + 'px';
				this.frame.style.left = parseInt(window.scrollX +
				(window.innerWidth - width)/2) + 'px';
				this.frame.style.top = parseInt(window.scrollY +
				(window.innerHeight - height)/2) + 'px';
				this.frame.style.visibility = "visible";
			},
			
			close: function() {
				this.frame.style.visibility = "hidden";
			},
		};
		
		return win;
	}

	String.prototype.ucFirst = function () {
		var firstLetter = this.substr(0,1).toUpperCase()
		return this.substr(0,1).toUpperCase() + this.substr(1,this.length);
	}
	
	interceptor_setup();

Running the Hack

As you install the user script (Tools → Install This User Script), be sure to add http://www.google.com/* to the list of included pages. Once the user script is installed, go to http://www.google.com/advanced_search. In the bottom-right corner of the screen, you will see a small button titled "[PI] is Off," as shown in Figure 5-10.

Figure 5-10. Post Interceptor, off by default

Post Interceptor, off by default

By default, Post Interceptor does nothing until you click this button to turn it on. Click it now and it will change to read "[PI] is On."

Now, click the Google Search button in the upper-right corner of the page. Instead of taking you directly to the search results, Post Interceptor will pop up a window that displays details about the form you are about submit, as shown in Figure 5-11.

Figure 5-11. Post Interceptor window

Post Interceptor window

The form has not been submitted yet. Now you have several options. You can click any form field to modify the value—even hidden fields.

If you are happy with your changes, you can submit the modified form. If you don't like the changes you've made but want to submit the form with the data you originally entered (and the values of the hidden form fields that the page originally set), you can submit the original form. If you decide not to submit the form at all, you can cancel the submission and return to the original page.

Trace XMLHttpRequest Activity

Log XMLHttpRequest calls into JavaScript Console.

XMLHttpRequest is a JavaScript technique that enables a page to interact with the server without having to reload the entire page. This nonstandard API was first developed by Microsoft for Internet Explorer, but it was later picked up and implemented by most other browsers, including Firefox. Once used by only a few, it is now becoming more mainstream in the development of web applications.

The renewed interest for rich web applications such as Gmail, MSN Web-Messenger, and A9 Search, has crystallized a new nickname for the technique: AJAX.

Tip

Jesse James Garrett coined this term in early 2005 as a shorthand for "Asynchronous JavaScript And XML."

A large number of frameworks now make use of the XMLHttpRequest object, trying to abstract the API and make it easier to use for a larger portion of hackers. However, as with most abstractions, there are still many times when you need to look under the hood.

Traditional debugging tools allow you to do that. You can install HTTP sniffers such as the LiveHTTPHeaders extension; you can test code interactively with the Venkman JavaScript debugger or the JavaScript shell; you can just litter your code with JavaScript alert statements. But these tools often offer too much or too little of what you actually need. This user script approaches debugging from a different angle, by focusing on the XMLHttpRequest interactions themselves and providing lightweight and instant tracing.

The Code

A typical usage scenario of XMLHttpRequest starts with creating a new XMLHttpRequest instance, wiring some callbacks (such as onreadystatechange, onload, or onerror), and then calling open and send.

The basic approach of this script is to replace the open and send methods on any XMLHttpRequest instance that gets created. The replacement code mimics the behavior of the original methods, but it also traces the input parameters and adds some extra instrumentation on callback events.

Most common object-oriented languages differentiate the concept of class (the definition of an object) and object (an instance of a class).

Instead, JavaScript has classes only. When creating a new object, it uses another object as a template or prototype, rather than following an abstract blueprint (a class). It is called a prototype-based language.

This script takes advantage of this characteristic by modifying the prototype of the XMLHttpRequest constructor to replace the open and send methods on all XMLHttpRequest instances.

It overrides XMLHttpRequest.prototype.open and XMLHttpRequest.prototype.send with new implementations and keeps references to the original methods by backing them up into XMLHttpRequest.prototype.oldOpen and XMLHttpRequest.prototype.oldSend.

Tip

To keep the state of your UI, you often don't have to build your own structure in parallel with that of the document. The objects from the DOM can be extended with your own properties. In this case, the script uses the XMLHttpRequest object itself to store the unique ID for the object.

Because multiple calls to the server may occur simultaneously, using multiple XMLHttpRequest objects, it is useful to have an instance ID along with the traced information.

When first called on an XMLHttpRequest object, the uniqueID function will generate a random ID number and store it in the uniqueIDMemo property on the object. Subsequent calls will load that saved value and reuse it.

Tip

Greasemonkey lets you log events to JavaScript Console via the GM_log method. That feature was added in Greasemonkey starting with Version 0.3, but didn't exist in earlier versions. In cases like that, you should test whether the feature is present and degrade gracefully if it is missing.

Save the following user script as xmlhttprequest-tracing.user.js:

	// ==UserScript==
	// @name		XmlHttpRequest Tracing
	// @namespace	http://blog.monstuff.com/archives/cat_greasemonkey.html
	// @description Trace XmlHttpRequest XMLHttpRequestlogging calls into a JavaScript consolecalls into the Javascript Console
	// @include		http://pick.some.domain
	// ==/UserScript==

	// based on code by Julien Couvreur
	// and included here with his gracious permission

	XMLHttpRequest.prototype.uniqueID = function() {
		if (!this.uniqueIDMemo) {
			this.uniqueIDMemo = Math.floor(Math.random() * 1000);
		}
		return this.uniqueIDMemo;
	}

	developer toolsXMLHttpRequest activityXMLHttpRequest.prototype.oldOpen = XMLHttpRequest.prototype.open;

	var newOpen = function(method, url, async, user, password) {
		GM_log("[" + this.uniqueID() + "] intercepted open (" +
				method + " , " +
				url + " , " +
				async + " , " +
				user + " , " +
				password + ")");
		this.oldOpen(method, url, async, user, password);
	}

	XMLHttpRequest.prototype.open = newOpen;

	XMLHttpRequest.prototype.oldSend = XMLHttpRequest.prototype.send;

	var newSend = function(a) {
		var xhr = this;
		GM_log("[" + xhr.uniqueID() + "] intercepted send (" + a + ")");
		var onload = function() {
			GM_log("[" + xhr.uniqueID() + "] intercepted load: " +
				xhr.status +
				" " + xhr.responseText);
		};
		
		var onerror = function() {
			GM_log("[" + xhr.uniqueID() + "] intercepted error: " +				xhr.status);
		};
		
		xhr.addEventListener("load", onload, false);
		xhr.addEventListener("error", onerror, false);
		
		xhr.oldSend(a);
	}

	XMLHttpRequest.prototype.send = newSend;

Running the Hack

To demonstrate this hack, I'll use Backpack, an AJAX-based information management tool. Go to http://backpackit.com and sign up for a free account.

After logging in, you can try out the application by creating a page and editing it. You will notice that these interactions won't cause the page to reload.

Under the covers, the application uses XMLHttpRequest to send the data back to the central server. That's where our user script comes in.

When installing the user script (Tools → Install This User Script), modify the list of included domains. Change the default http://pick.some.domain to http://*.backpackit.com/*, as shown in Figure 5-12.

Figure 5-12. Configuration for Backpack debugging

Configuration for Backpack debugging

Select Tools → JavaScript Console, and then change the filter in the console window to display Messages only. Now, edit your BackpackIt page again— for example, by changing the title to "Greasemonkey Hacks." The script catches the XMLHttpRequest interaction and displays it in JavaScript Console.

What exactly gets logged? In this case, three events, as illustrated in Figure 5-13. The browser calls the open and send methods on the XMLHttpRequest object, and the server responds with a confirmation page.

Backpack expects HTML content to be returned from the server. This script logs both the HTML confirmation page and the "200 OK" HTTP status code.

Figure 5-13. Backpack debugging output

Backpack debugging output

In addition, each logged event includes the unique ID of the XMLHttpRequest instance; in this example, it was 445.

Julien Couvreur

Personal tools