From 3ac1838cba725705a96a9d7b65721b15f4ec67b1 Mon Sep 17 00:00:00 2001
From: Einar Egilsson
Date: Fri, 26 Aug 2011 14:37:27 +0200
Subject: Moved everything to a js module, removed custom xpcom interfaces

---
 chrome/code/browserOverlay.xul.js           | 104 ++++++++
 chrome/code/component.js                    |  21 ++
 chrome/code/editRedirect.xul.js             | 103 ++++++++
 chrome/code/redirect.js                     | 218 ++++++++++++++++
 chrome/code/redirector.js                   | 370 ++++++++++++++++++++++++++++
 chrome/code/redirectorprefs.js              |  93 +++++++
 chrome/code/settings.xul.js                 | 276 +++++++++++++++++++++
 chrome/code/unittest/run.html               |  99 ++++++++
 chrome/code/unittest/testcases.js           | 129 ++++++++++
 chrome/content/code/redirector.prototype.js | 343 --------------------------
 chrome/ui/browserOverlay.xul                |  34 +++
 chrome/ui/editRedirect.xul                  |  58 +++++
 chrome/ui/help.html                         | 182 ++++++++++++++
 chrome/ui/settings.xul                      | 118 +++++++++
 chrome/ui/skin/movedown.png                 | Bin 0 -> 294 bytes
 chrome/ui/skin/movedowndisabled.png         | Bin 0 -> 361 bytes
 chrome/ui/skin/moveup.png                   | Bin 0 -> 360 bytes
 chrome/ui/skin/moveupdisabled.png           | Bin 0 -> 282 bytes
 chrome/ui/skin/redirector.css               |  15 ++
 chrome/ui/skin/redirector.png               | Bin 0 -> 1462 bytes
 chrome/ui/skin/statusactive.png             | Bin 0 -> 360 bytes
 chrome/ui/skin/statusinactive.png           | Bin 0 -> 396 bytes
 22 files changed, 1820 insertions(+), 343 deletions(-)
 create mode 100644 chrome/code/browserOverlay.xul.js
 create mode 100644 chrome/code/component.js
 create mode 100644 chrome/code/editRedirect.xul.js
 create mode 100644 chrome/code/redirect.js
 create mode 100644 chrome/code/redirector.js
 create mode 100644 chrome/code/redirectorprefs.js
 create mode 100644 chrome/code/settings.xul.js
 create mode 100644 chrome/code/unittest/run.html
 create mode 100644 chrome/code/unittest/testcases.js
 delete mode 100644 chrome/content/code/redirector.prototype.js
 create mode 100644 chrome/ui/browserOverlay.xul
 create mode 100644 chrome/ui/editRedirect.xul
 create mode 100644 chrome/ui/help.html
 create mode 100644 chrome/ui/settings.xul
 create mode 100644 chrome/ui/skin/movedown.png
 create mode 100644 chrome/ui/skin/movedowndisabled.png
 create mode 100644 chrome/ui/skin/moveup.png
 create mode 100644 chrome/ui/skin/moveupdisabled.png
 create mode 100644 chrome/ui/skin/redirector.css
 create mode 100644 chrome/ui/skin/redirector.png
 create mode 100644 chrome/ui/skin/statusactive.png
 create mode 100644 chrome/ui/skin/statusinactive.png

(limited to 'chrome')

diff --git a/chrome/code/browserOverlay.xul.js b/chrome/code/browserOverlay.xul.js
new file mode 100644
index 0000000..0d2f46e
--- /dev/null
+++ b/chrome/code/browserOverlay.xul.js
@@ -0,0 +1,104 @@
+
+Components.utils.import("chrome://redirector/content/code/redirector.js");
+
+var RedirectorOverlay = {
+
+	strings 	: null,
+	prefs		: null,
+
+	onLoad : function(event) {
+		try {
+
+			// initialization code
+			document.getElementById('contentAreaContextMenu')
+				.addEventListener("popupshowing", function(e) { RedirectorOverlay.showContextMenu(e); }, false);
+			
+			this.strings = document.getElementById("redirector-strings");
+			this.prefs = new RedirectorPrefs();
+			this.changedPrefs(this.prefs);
+			this.prefs.addListener(this);
+		} catch(e) {
+			if (this.strings) {
+				alert(this.strings.getString("initError") + "\n\n" + e);
+			} else {
+				alert(e);
+			}
+		}
+	},
+	
+	onUnload : function(event) {
+		this.prefs.dispose();
+		Redirector.debug("Finished cleanup");
+	},
+
+	changedPrefs : function(prefs) {
+		var statusImg = document.getElementById('redirector-statusbar-img');
+
+		if (prefs.enabled) {
+			statusImg.src = 'chrome://redirector/skin/statusactive.png'
+			statusImg.setAttribute('tooltiptext', this.strings.getString('enabledTooltip'));
+		} else {
+			statusImg.src = 'chrome://redirector/skin/statusinactive.png'
+			statusImg.setAttribute('tooltiptext', this.strings.getString('disabledTooltip'));
+		}
+
+		document.getElementById('redirector-status').hidden = !prefs.showStatusBarIcon;
+		document.getElementById('redirector-context').hidden = !prefs.showContextMenu;
+	},
+	
+	showContextMenu : function(event) {
+		if (gContextMenu.onLink) {
+			document.getElementById("redirector-context").label = this.strings.getString('addLinkUrl');
+		} else {
+			document.getElementById("redirector-context").label = this.strings.getString('addCurrentUrl');
+		}
+	},
+
+	onContextMenuCommand: function(event) {
+		var redirect = new Redirect(window.content.location.href, window.content.location.href);
+		if (gContextMenu.onLink) {
+			redirect.redirectUrl = gContextMenu.link.toString();
+		}
+
+		var args = { saved : false, 'redirect' : redirect };
+		window.openDialog("chrome://redirector/content/ui/editRedirect.xul", "redirect", "chrome,dialog,modal,centerscreen", args);
+		if (args.saved) {
+			Redirector.addRedirect(args.redirect);
+		}
+	},
+		
+	onMenuItemCommand: function(event) {
+		this.openSettings();
+	},
+
+	toggleEnabled : function(event) {
+		this.prefs.enabled = !this.prefs.enabled;
+	},
+
+	openSettings : function() {
+		var windowName = "redirectorSettings";
+		var windowsMediator = Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(Components.interfaces.nsIWindowMediator);
+		var win = windowsMediator.getMostRecentWindow(windowName);
+		if (win) {
+			win.focus();
+		} else {
+			window.openDialog("chrome://redirector/content/ui/settings.xul",
+					windowName,
+					"chrome,dialog,resizable=yes,centerscreen", this);
+		}
+	
+	},
+	
+	statusBarClick : function(event) {
+		var LEFT = 0, RIGHT = 2;
+
+		if (event.button == LEFT) {
+			RedirectorOverlay.toggleEnabled();
+		} else if (event.button == RIGHT) {
+			this.openSettings();
+		}
+	}
+
+};
+window.addEventListener("load", function(event) { RedirectorOverlay.onLoad(event); }, false);
+window.addEventListener("unload", function(event) { RedirectorOverlay.onUnload(event); }, false);
diff --git a/chrome/code/component.js b/chrome/code/component.js
new file mode 100644
index 0000000..453d134
--- /dev/null
+++ b/chrome/code/component.js
@@ -0,0 +1,21 @@
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Ci = Components.interfaces;
+Cr = Components.results;
+
+Components.utils.import("chrome://redirector/content/code/redirector.js");
+
+function RedirectorComponent() { }
+
+RedirectorComponent.prototype = {
+  classDescription: "My Hello World Javascript XPCOM Component",
+  classID:          Components.ID("{b7a7a54f-0581-47ff-b086-d6920cb7a3f7}"),
+  contractID:       "@einaregilsson.com/redirector;1",
+  QueryInterface: function(iid) {
+	if (iid.equals(Ci.nsISupports) || iid.equals(Ci.nsIContentPolicy) || iid.equals(Ci.nsIChannelEventSink)) {
+		return Redirector;
+	}
+	throw Cr.NS_ERROR_NO_INTERFACE;
+  }
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([RedirectorComponent]);
diff --git a/chrome/code/editRedirect.xul.js b/chrome/code/editRedirect.xul.js
new file mode 100644
index 0000000..0c8f34b
--- /dev/null
+++ b/chrome/code/editRedirect.xul.js
@@ -0,0 +1,103 @@
+//// $Id$
+
+var EditRedirect = {
+	txtExampleUrl : null,
+	txtIncludePattern : null,
+	txtRedirectUrl : null,
+	txtExcludePattern : null,
+	chkUnescapeMatches : null,
+	rdoRegex : null,
+	rdoWildcard : null, 
+	
+	onLoad : function() {
+		var args = window.arguments[0];
+		var redirect = args.redirect;
+		this.txtExampleUrl = document.getElementById('txtExampleUrl');
+		this.txtIncludePattern = document.getElementById('txtIncludePattern');
+		this.txtRedirectUrl= document.getElementById('txtRedirectUrl');
+		this.txtExcludePattern= document.getElementById('txtExcludePattern');
+		this.chkUnescapeMatches= document.getElementById('chkUnescapeMatches');
+		this.rdoWildcard= document.getElementById('rdoWildcard');
+		this.rdoRegex = document.getElementById('rdoRegex');
+	
+		this.txtExampleUrl.value = redirect.exampleUrl;
+		this.txtIncludePattern.value = redirect.includePattern;
+		this.txtExcludePattern.value = redirect.excludePattern;
+		this.txtRedirectUrl.value = redirect.redirectUrl;
+		this.chkUnescapeMatches.setAttribute('checked', redirect.unescapeMatches);
+		this.rdoRegex.setAttribute('selected', redirect.isRegex());
+		this.rdoWildcard.setAttribute('selected', redirect.isWildcard());
+
+		this.txtIncludePattern.focus();
+		this.strings = document.getElementById("redirector-strings");
+	},
+
+	onAccept : function() {
+		var args = window.arguments[0];
+		var msg, title;
+		args.saved = true;
+		this.saveValues(args.redirect);
+		
+		var oldDisabled = args.redirect.disabled;
+		args.redirect.disabled = false;
+		if (!/^\s*$/.test(args.redirect.exampleUrl)) {
+			var result = args.redirect.getMatch(args.redirect.exampleUrl);
+			if (!result.isMatch) {
+				title = this.strings.getString('warningExampleUrlDoesntMatchPatternTitle');
+				msg = this.strings.getString('warningExampleUrlDoesntMatchPattern');
+				var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService);
+				var rv = ps.confirmEx(window, title, msg, ps.STD_YES_NO_BUTTONS, ps.BUTTON_TITLE_YES, ps.BUTTON_TITLE_NO, null, null, {});				
+				return rv == 0;
+			} else {
+				var resultUrl = result.redirectTo;
+				if (!resultUrl.match(/https?:/)) {
+					var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
+					var uri = ioService.newURI(args.redirect.exampleUrl, null, null); 
+					resultUrl = uri.resolve(resultUrl);
+				} 
+		
+				var secondResult = args.redirect.getMatch(resultUrl);
+				if (secondResult.isMatch) {
+					title = this.strings.getString('errorExampleUrlMatchesRecursiveTitle');
+					msg = this.strings.getFormattedString('errorExampleUrlMatchesRecursive', [args.redirect.exampleUrl, resultUrl]);
+					this.msgBox(title, msg);
+					return false;
+				}
+			}
+		}
+		return true;
+	},
+
+	msgBox : function(title, text) {
+		Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+			.getService(Components.interfaces.nsIPromptService)
+				.alert(window, title, text);
+	},
+	
+	saveValues : function(redirect) {
+		redirect.exampleUrl = this.txtExampleUrl.value;
+		redirect.includePattern = this.txtIncludePattern.value;
+		redirect.excludePattern = this.txtExcludePattern.value;
+		redirect.redirectUrl = this.txtRedirectUrl.value;
+		redirect.patternType = this.rdoRegex.getAttribute('selected') == 'true' ? Redirect.REGEX : Redirect.WILDCARD;
+		var val = this.chkUnescapeMatches.getAttribute('checked');
+		redirect.unescapeMatches = val === 'true' || val === true;
+		//Disabled cannot be set here
+	},
+	
+	testPattern : function() {
+		try {
+			var redirect = new Redirect();
+			this.saveValues(redirect);
+			var extName = this.strings.getString('extensionName');
+			var result = redirect.test();
+			if (result.isMatch) {
+				this.msgBox(extName, this.strings.getFormattedString('testPatternSuccess', [redirect.includePattern, redirect.exampleUrl, result.redirectTo]));
+			} else if (result.isExcludeMatch) {
+				this.msgBox(extName, this.strings.getFormattedString('testPatternExclude', [redirect.exampleUrl, redirect.excludePattern]));
+			} else {
+				this.msgBox(extName, this.strings.getFormattedString('testPatternFailure', [redirect.includePattern, redirect.exampleUrl]));
+			}
+		} catch(e) {alert(e);}
+	}
+};
\ No newline at end of file
diff --git a/chrome/code/redirect.js b/chrome/code/redirect.js
new file mode 100644
index 0000000..f9b7f00
--- /dev/null
+++ b/chrome/code/redirect.js
@@ -0,0 +1,218 @@
+
+var EXPORTED_SYMBOLS = ['Redirect'];
+
+
+function Redirect(exampleUrl, includePattern, redirectUrl, patternType, excludePattern, unescapeMatches, disabled) {
+	this._init(exampleUrl, includePattern, redirectUrl, patternType, excludePattern, unescapeMatches, disabled);
+}
+
+//Static
+Redirect.WILDCARD = 'W';
+Redirect.REGEX = 'R';
+
+Redirect.prototype = {
+	
+	//attributes
+	exampleUrl : null,
+			
+	get includePattern() { return this._includePattern; },
+	set includePattern(value) { 
+		this._includePattern = value;
+		this._rxInclude = this._compile(value); 
+	},
+	
+	get excludePattern() { return this._excludePattern; },
+	set excludePattern(value) { 
+		this._excludePattern = value; 
+		this._rxExclude = this._compile(value); 
+	},
+	
+	redirectUrl : null,
+	
+	get patternType() { return this._patternType; },
+	set patternType(value) { 
+		this._patternType = value;
+		this.compile();
+	},
+
+	unescapeMatches : false,
+	
+	disabled : false,
+	
+	//Functions
+	clone : function() {
+		return new Redirect(this.exampleUrl, this.includePattern, 
+							this.redirectUrl, this.patternType, 
+							this.excludePattern, this.unescapeMatches,
+							this.disabled);    
+	},
+	
+	compile : function() {
+		this._rxInclude = this._compile(this._includePattern); 
+		this._rxExclude = this._compile(this._excludePattern); 
+	},
+
+	copyValues : function(other) {
+		this.exampleUrl = other.exampleUrl;
+		this.includePattern = other.includePattern;
+		this.excludePattern = other.excludePattern;
+		this.redirectUrl = other.redirectUrl;
+		this.patternType = other.patternType;
+		this.unescapeMatches = other.unescapeMatches;
+		this.disabled = other.disabled;
+	},
+
+	deserialize : function(str) {
+		if (!str || !str.split) {
+			throw Error("Invalid serialized redirect: " + str);
+		}	
+		var parts = str.split(',,,');
+		if (parts.length < 5) {
+			throw Error("Invalid serialized redirect, too few fields: " + str);
+		}
+		this._init.apply(this, parts);
+	},
+	
+	equals : function(redirect) {
+		return this.exampleUrl == redirect.exampleUrl
+			&& this.includePattern == redirect.includePattern
+			&& this.excludePattern == redirect.excludePattern
+			&& this.redirectUrl == redirect.redirectUrl
+			&& this.patternType == redirect.patternType
+			&& this.unescapeMatches == redirect.unescapeMatches
+		;
+	},
+	
+	getMatch: function(url) {
+		var result = { 
+			isMatch : false, 
+			isExcludeMatch : false, 
+			isDisabledMatch : false, 
+			redirectTo : '',
+			toString : function() { return "{ isMatch : " + this.isMatch + 
+										   ", isExcludeMatch : " + this.isExcludeMatch + 
+										   ", isDisabledMatch : " + this.isDisabledMatch + 
+										   ", redirectTo : \"" + this.redirectTo + "\"" +
+										   "}"; }
+		};
+		var redirectTo = null;
+
+		redirectTo = this._includeMatch(url);
+		if (redirectTo !== null) {
+			if (this.disabled) {
+				result.isDisabledMatch = true;
+			} else if (this._excludeMatch(url)) {
+				result.isExcludeMatch = true;
+			} else {
+				result.isMatch = true;
+				result.redirectTo = redirectTo;
+			}
+		}
+		return result;	 
+	},
+	
+	isRegex: function() {
+		return this.patternType == Redirect.REGEX;
+	},
+	
+	isWildcard : function() {
+		return this.patternType == Redirect.WILDCARD;	
+	},
+
+	serialize : function() {
+		return [ this.exampleUrl
+			   , this.includePattern
+			   , this.redirectUrl
+			   , this.patternType
+			   , this.excludePattern
+			   , this.unescapeMatches
+			   , this.disabled ].join(',,,');
+	},
+	
+	test : function() {
+		return this.getMatch(this.exampleUrl);	
+	},
+
+	
+	//Private functions below	
+
+	_includePattern : null,
+	_excludePattern : null,
+	_patternType : null,
+	_rxInclude : null,
+	_rxExclude : null,
+	
+	_preparePattern : function(pattern) {
+		if (this.patternType == Redirect.REGEX) {
+			return pattern; 
+		} else { //Convert wildcard to regex pattern
+			var converted = '^';
+			for (var i = 0; i < pattern.length; i++) {
+				var ch = pattern.charAt(i);
+				if ('()[]{}?.^$\\+'.indexOf(ch) != -1) {
+					converted += '\\' + ch;
+				} else if (ch == '*') {
+					converted += '(.*?)';
+				} else {
+					converted += ch;
+				}
+			}
+			converted += '$';
+			return converted;
+		}
+	},
+
+	_compile : function(pattern) {
+		if (!pattern) {
+			return null;
+		}
+		return new RegExp(this._preparePattern(pattern),"gi");
+	},
+	
+	_init : function(exampleUrl, includePattern, redirectUrl, patternType, excludePattern, unescapeMatches, disabled) {
+		this.exampleUrl = exampleUrl || '';
+		this.includePattern = includePattern || '';
+		this.excludePattern = excludePattern || '';
+		this.redirectUrl = redirectUrl || '';
+		this.patternType = patternType || Redirect.WILDCARD;
+		this.unescapeMatches = (unescapeMatches === 'true' || unescapeMatches === true);
+		this.disabled = (disabled === 'true' || disabled === true);
+	},
+	
+	toString : function() {
+		return 'REDIRECT: {'
+			+  '\n\tExample url 	 : ' + this.exampleUrl
+			+  '\n\tInclude pattern  : ' + this.includePattern
+			+  '\n\tExclude pattern  : ' + this.excludePattern
+			+  '\n\tRedirect url	 : ' + this.redirectUrl
+			+  '\n\tPattern type	 : ' + this.patternType
+			+  '\n\tUnescape matches : ' + this.unescapeMatches
+			+  '\n\tDisabled		 : ' + this.disabled
+			+  '\n}\n';
+	},
+	
+	_includeMatch : function(url) {
+		if (!this._rxInclude) {
+			return null;
+		}	
+		var matches = this._rxInclude.exec(url);
+		if (!matches) {
+			return null;
+		}
+		var resultUrl = this.redirectUrl;
+		for (var i = 1; i < matches.length; i++) {
+			resultUrl = resultUrl.replace(new RegExp('\\$' + i, 'gi'), this.unescapeMatches ? unescape(matches[i]) : matches[i]);
+		}
+		this._rxInclude.lastIndex = 0;
+		return resultUrl;
+	},
+	
+	_excludeMatch : function(url) {
+		if (!this._rxExclude) {
+			return false;	
+		}
+		var shouldExclude = !!this._rxExclude.exec(url);	
+		this._rxExclude.lastIndex = 0;
+		return shouldExclude;
+	}
+};
\ No newline at end of file
diff --git a/chrome/code/redirector.js b/chrome/code/redirector.js
new file mode 100644
index 0000000..8744483
--- /dev/null
+++ b/chrome/code/redirector.js
@@ -0,0 +1,370 @@
+var EXPORTED_SYMBOLS = ['Redirector', 'rdump'];
+Ci = Components.interfaces;
+Cc = Components.classes;
+Ci = Components.interfaces;
+Cr = Components.results;
+
+Components.utils.import("chrome://redirector/content/code/redirect.js");
+Components.utils.import("chrome://redirector/content/code/redirectorprefs.js");
+
+function rdump(msg) {
+	dump(msg + '\n');
+}
+
+Redirector = {
+	
+	get enabled() {
+		return this._prefs && this._prefs.enabled;	
+	},
+	
+	set enabled(value) {
+		if (this._prefs) {
+			this._prefs.enabled = value;
+		}
+	},
+
+	get redirectCount() {
+		return this._list.length;
+	},
+	
+	toString : function() {
+		return "Redirector";
+	},
+	
+	addRedirect : function(redirect) {
+		//This runaround is necessary because the redirect
+		//that was created somewhere up in the GUI doesn't
+		//have the Redirect function in scope.
+		var rx = new Redirect();
+		rx.copyValues(redirect);
+		this._list.push(rx);
+		this.save();
+	},
+
+	debug : function(msg) {
+		if (this._prefs.debugEnabled) {
+			this._cout.logStringMessage('REDIRECTOR: ' + msg);
+		}
+	},
+	
+	deleteRedirectAt : function(index) {
+		this._list.splice(index, 1);
+		this.save();
+	},
+	
+	exportRedirects : function(file) {
+		var fileStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
+		const PR_WRONLY 	 = 0x02;
+		const PR_CREATE_FILE = 0x08;
+		const PR_TRUNCATE	 = 0x20;
+
+		fileStream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 0644, 0);
+		var stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
+		stream.init(fileStream, "UTF-8", 16384, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+		stream.writeString(this._redirectsAsString('\n'));
+		stream.close();
+	},
+	
+	getRedirectAt : function(index) {
+		return this._list[index];	
+	},
+	
+	//Get the redirect url for the given url. This will not check if we are enabled, and
+	//not do any verification on the url, just assume that it is a good string url that is for http/s
+	getRedirectUrl : function(url) {
+		this.debug("Checking " + url);
+		
+		for each (var redirect in this._list) {
+			var result = redirect.getMatch(url);
+			if (result.isExcludeMatch) {
+				this.debug(url + ' matched exclude pattern ' + redirect.excludePattern + ' so the redirect ' + redirect.includePattern + ' will not be used');
+			} else if (result.isDisabledMatch) {
+				this.debug(url + ' matched pattern ' + redirect.includePattern + ' but the redirect is disabled');
+			} else if (result.isMatch) {
+				redirectUrl = this._makeAbsoluteUrl(url, result.redirectTo);
+				
+				//check for loops...
+				result = redirect.getMatch(redirectUrl);
+				if (result.isMatch) {
+					var title = this._getString('invalidRedirectTitle');
+					var msg = this._getFormattedString('invalidRedirectText', [redirect.includePattern, url, redirectUrl]);
+					this.debug(msg);
+					redirect.disabled = true;
+					this.save();					
+					this._msgBox(title, msg);
+				} else {
+					this.debug('Redirecting ' + url + ' to ' + redirectUrl);
+					return redirectUrl;
+				}
+			}
+		}
+		return null;
+	},
+	
+	QueryInterface: function(iid) {
+		if (iid.equals(Ci.nsISupports) || iid.equals(Ci.nsIContentPolicy) || iid.equals(Ci.nsIChannelEventSink)) {
+			return this;
+		}
+		throw Cr.NS_ERROR_NO_INTERFACE;
+	},
+	
+	
+	importRedirects : function(file) {
+		var fileStream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
+		fileStream.init(file, 0x01, 0444, 0); //TODO: Find the actual constants for these magic numbers
+
+		var stream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
+		stream.init(fileStream, "UTF-8", 16384, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+		stream = stream.QueryInterface(Ci.nsIUnicharLineInputStream);
+
+		var importCount = 0, existsCount = 0;
+		var lines = [];
+		var line = {value: null};
+		stream.readLine(line);
+		while (line.value) {
+			var redirect = new Redirect();
+			redirect.deserialize(line.value.replace('\n', ''));
+			if (this._containsRedirect(redirect)) {
+				existsCount++;
+			} else {
+				this._list.push(redirect);
+				importCount++;
+			}
+			stream.readLine(line);
+		}
+		stream.close();
+		this.save();
+		return importCount | (existsCount << 16);
+	},
+	
+	reload : function() {
+		loader.loadSubScript('chrome://redirector/content/code/redirector.prototype.js');
+		loader.loadSubScript('chrome://redirector/content/code/redirect.js');
+		var oldEnabled = this.enabled;
+		for (var key in Redirector.prototype) {
+			if (key != 'redirectCount' && key != 'enabled') {
+				this[key] = Redirector.prototype[key];
+			}
+		}
+		this._init();
+		this.enabled = oldEnabled;
+	}, 
+	
+	save : function() {
+		this._prefs.redirects = this._redirectsAsString(':::');
+	},
+
+	switchItems : function(index1, index2) {
+		var item = this._list[index1];
+		this._list[index1] = this._list[index2];
+		this._list[index2] = item;
+		this.save();
+	},
+	
+	
+	// nsIContentPolicy implementation
+	shouldLoad: function(contentType, contentLocation, requestOrigin, aContext, mimeTypeGuess, extra) {
+		if (contentLocation.scheme != "http" && contentLocation.scheme != "https") {
+			return Ci.nsIContentPolicy.ACCEPT;
+		} //Immediately, otherwise we will log all sorts of crap
+			
+		rdump('nsIContentPolicy::ShouldLoad ' + contentLocation.spec);
+		try {
+			//This is also done in getRedirectUrl, but we want to exit as quickly as possible for performance
+			if (!this._prefs.enabled) {
+				return Ci.nsIContentPolicy.ACCEPT;
+			}
+			
+			if (contentType != Ci.nsIContentPolicy.TYPE_DOCUMENT) {
+				return Ci.nsIContentPolicy.ACCEPT;
+			}
+
+			if (contentLocation.scheme != "http" && contentLocation.scheme != "https") {
+				return Ci.nsIContentPolicy.ACCEPT;
+			}
+			
+			if (!aContext || !aContext.loadURI) {
+				return Ci.nsIContentPolicy.ACCEPT;
+			}
+			
+			var redirectUrl = this.getRedirectUrl(contentLocation.spec);
+
+			if (!redirectUrl) {
+				return Ci.nsIContentPolicy.ACCEPT;
+			}			
+
+			aContext.loadURI(redirectUrl, requestOrigin, null);
+			return Ci.nsIContentPolicy.REJECT_REQUEST;
+		} catch(e) {
+			this.debug(e);	 
+		}
+		
+	},
+	
+	shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra) {
+		return Ci.nsIContentPolicy.ACCEPT;
+	},
+	//end nsIContentPolicy
+
+	//nsIChannelEventSink implementation
+	
+	//For FF4.0. Got this from a thread about adblock plus, https://adblockplus.org/forum/viewtopic.php?t=5895
+	asyncOnChannelRedirect: function(oldChannel, newChannel, flags, redirectCallback) {
+		this.onChannelRedirect(oldChannel, newChannel, flags);
+		redirectCallback.onRedirectVerifyCallback(0);
+	},	
+	
+	onChannelRedirect: function(oldChannel, newChannel, flags)
+	{
+		try {
+			let newLocation = newChannel.URI.spec;
+			rrdump('nsIChannelEventSink::onChannelRedirect ' + newLocation);
+
+			if (!(newChannel.loadFlags & Ci.nsIChannel.LOAD_DOCUMENT_URI)) {
+				//We only redirect documents...
+				return; 
+			}
+
+			if (!this._prefs.enabled) {
+				return;
+			}
+			
+			if (!newLocation) {
+				return;
+			}
+			let callbacks = [];
+			if (newChannel.notificationCallbacks) {
+				callbacks.push(newChannel.notificationCallbacks);
+			}
+			if (newChannel.loadGroup && newChannel.loadGroup.notificationCallbacks) {
+				callbacks.push(newChannel.loadGroup.notificationCallbacks);
+			}
+			var win;
+			var webNav;
+			for each (let callback in callbacks)
+			{
+				try {
+					win = callback.getInterface(Ci.nsILoadContext).associatedWindow;
+					webNav = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+					break;
+				} catch(e) {}
+			}
+			if (!webNav) {
+				return; 
+			}
+			var redirectUrl = this.getRedirectUrl(newLocation);
+
+			if (redirectUrl) {
+				webNav.loadURI(redirectUrl,null,null,null,null);
+				throw Cr.NS_BASE_STREAM_WOULD_BLOCK; //Throw this because the real error we should throw shows up in console...
+			}			
+			
+		} catch (e if (e != Cr.NS_BASE_STREAM_WOULD_BLOCK)) {
+			// We shouldn't throw exceptions here - this will prevent the redirect.
+			rdump("Redirector: Unexpected error in onChannelRedirect: " + e + "\n");
+		}
+	},
+	//end nsIChannelEventSink
+	
+	//Private members and methods
+			
+	_prefs : null,
+	_list : null,
+	_strings : null,
+	_cout : Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService),
+
+	_init : function() {
+		if (this._prefs) {
+			this._prefs.dispose();
+		}
+		this._cout.logStringMessage('REDIRECTOR CREATED');
+		this._prefs = new RedirectorPrefs();
+		//Check if we need to update existing redirects
+		var data = this._prefs.redirects;
+		var version = this._prefs.version;
+		this._loadStrings();
+		var currentVersion = '2.5';
+		//Here update checks are handled
+		if (version == 'undefined') { //Either a fresh install of Redirector, or first time install of v2.0
+			if (data) { //There is some data in redirects, we are upgrading from a previous version, need to upgrade data
+				var tempList = JSON.parse(data);
+				var arr;
+				var newArr = []
+				for each (arr in tempList) {
+					if (arr.length == 5) {
+						arr.push(''); //For those that don't have an exclude pattern. Backwards compatibility is a bitch!
+					}
+					arr.splice(3,1); //Remove the "only if link exists" data
+					newArr.push(arr.join(',,,'));
+				}
+				this._prefs.redirects = newArr.join(':::');
+			}
+			this._prefs.version = currentVersion;
+		} else if (version == '2.0' || version == '2.0.1' || version == '2.0.2') {
+			this._prefs.version = currentVersion;
+		}
+		//Update finished
+		
+		//Now get from the new format
+		data = this._prefs.redirects;
+		var arr;
+		this._list = [];
+		if (data != '') {
+			for each (redirectString in data.split(':::')) {
+				var redirect = new Redirect();
+				redirect.deserialize(redirectString);
+				this._list.push(redirect);
+			}
+		}
+	},
+	
+	_loadStrings : function() {
+		var src = 'chrome://redirector/locale/redirector.properties';
+		var localeService = Cc["@mozilla.org/intl/nslocaleservice;1"].getService(Ci.nsILocaleService);
+		var appLocale = localeService.getApplicationLocale();
+		var stringBundleService = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService);
+		this._strings = stringBundleService.createBundle(src, appLocale);	 
+	},	  
+
+	_redirectsAsString : function(seperator) {
+		return [r.serialize() for each (r in this._list)].join(seperator);
+	},
+	
+	
+	_containsRedirect : function(redirect) {
+		for each (var existing in this._list) {
+			if (existing.equals(redirect)) {
+				return true;
+			}	
+		}
+		return false;
+	},
+	
+	_getString : function(name) {
+		return this._strings.GetStringFromName(name);
+	},
+	
+	_getFormattedString : function(name, params) {
+		return this._strings.formatStringFromName(name, params, params.length);
+	},
+	
+	_msgBox : function(title, text) {
+		Cc["@mozilla.org/embedcomp/prompt-service;1"]
+			.getService(Ci.nsIPromptService)
+				.alert(null, title, text);
+	},
+
+	_makeAbsoluteUrl : function(currentUrl, relativeUrl) {
+		
+		if (relativeUrl.match(/https?:/)) {
+			return relativeUrl;
+		} 
+		
+		var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+		var uri = ioService.newURI(currentUrl, null, null); 
+		
+		return uri.resolve(relativeUrl);
+	}
+};
+
+Redirector._init();
\ No newline at end of file
diff --git a/chrome/code/redirectorprefs.js b/chrome/code/redirectorprefs.js
new file mode 100644
index 0000000..101a3f0
--- /dev/null
+++ b/chrome/code/redirectorprefs.js
@@ -0,0 +1,93 @@
+var EXPORTED_SYMBOLS = ['RedirectorPrefs'];
+
+function RedirectorPrefs() {
+	this.init();	
+}
+
+RedirectorPrefs.prototype = {
+
+	//Preferences:
+	_version : null,
+	_enabled : null,
+	_showStatusBarIcon : null,
+	_showContextMenu : null,
+	_debugEnabled : null,
+	_defaultDir : null,
+	_redirects : null,
+	
+	_prefBranch : null,
+	
+	_listeners : null,
+	
+	//Preferences props
+	
+	get version() { return this._version; },
+	set version(value) { this._prefBranch.setCharPref('version', value); },
+
+	get enabled() { return this._enabled; },
+	set enabled(value) { this._prefBranch.setBoolPref('enabled', value); },
+	
+	get showStatusBarIcon() { return this._showStatusBarIcon; },
+	set showStatusBarIcon(value) { this._prefBranch.setBoolPref('showStatusBarIcon', value); },
+
+	get showContextMenu() { return this._showContextMenu; },
+	set showContextMenu(value) { this._prefBranch.setBoolPref('showContextMenu', value); },
+		
+	get debugEnabled() { return this._debugEnabled; },
+	set debugEnabled(value) { this._prefBranch.setBoolPref('debugEnabled', value); },
+
+	get defaultDir() { return this._defaultDir; },
+	set defaultDir(value) { this._prefBranch.setCharPref('defaultDir', value); },
+
+	get redirects() { return this._redirects; },
+	set redirects(value) { this._prefBranch.setCharPref('redirects', value); },
+
+	init : function() {
+		this._prefBranch = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService).getBranch("extensions.redirector.");
+		this.reload();
+		this._listeners = [];
+		this.service.addObserver('extensions.redirector', this, false);
+	},
+
+	dispose : function() {
+		this._listeners = null;
+		this.service.removeObserver('extensions.redirector', this);
+	},
+	
+	reload : function() {
+		this._version = this._prefBranch.getCharPref('version');
+		this._enabled = this._prefBranch.getBoolPref('enabled');
+		this._showStatusBarIcon = this._prefBranch.getBoolPref('showStatusBarIcon');
+		this._showContextMenu = this._prefBranch.getBoolPref('showContextMenu');
+		this._debugEnabled = this._prefBranch.getBoolPref('debugEnabled');
+		this._defaultDir = this._prefBranch.getCharPref('defaultDir');
+		this._redirects = this._prefBranch.getCharPref('redirects');
+	},
+	
+	get service() {
+		return Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranchInternal);
+	},
+
+	observe : function(subject, topic, data) {
+		if (topic != 'nsPref:changed') {
+			return;
+		}
+		this.reload();
+		for each (var listener in this._listeners) {
+			listener && listener.changedPrefs && listener.changedPrefs(this);	 
+		}
+	},
+
+	addListener : function(listener) {
+		this._listeners.push(listener);
+	},
+	
+	removeListener : function(listener) {
+		for (var i = 0; i < this._listeners.length; i++) {
+			if (this._listeners[i] == listener) {
+				this._listeners.splice(i,1);
+				return;
+			}
+		}
+	},
+}
\ No newline at end of file
diff --git a/chrome/code/settings.xul.js b/chrome/code/settings.xul.js
new file mode 100644
index 0000000..c7f780a
--- /dev/null
+++ b/chrome/code/settings.xul.js
@@ -0,0 +1,276 @@
+Components.utils.import("chrome://redirector/content/code/redirector.js");
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const nsLocalFile = Components.Constructor("@mozilla.org/file/local;1", "nsILocalFile", "initWithPath");
+
+var Settings = {
+
+	lstRedirects: null,
+	btnDelete	: null,
+	btnEdit 	: null,
+	btnUp		: null,
+	btnDown 	: null,
+	btnExport	: null,
+	btnImport	: null,
+	chkEnableRedirector : null,
+	chkShowStatusBarIcon : null,
+	chkShowContextMenu : null,
+	chkEnableDebugOutput : null,
+	prefs : null,
+	
+	onLoad : function() {
+		try {
+			//Get references to controls
+			this.lstRedirects = document.getElementById('lstRedirects');
+			this.btnDelete = document.getElementById('btnDelete');
+			this.btnEdit = document.getElementById('btnEdit');
+			this.btnUp = document.getElementById('btnUp');
+			this.btnDown = document.getElementById('btnDown');
+			this.btnExport = document.getElementById('btnExport');
+			this.btnImport = document.getElementById('btnImport');
+			this.chkEnableRedirector = document.getElementById('chkEnableRedirector');
+			this.chkShowStatusBarIcon = document.getElementById('chkShowStatusBarIcon');
+			this.chkShowContextMenu = document.getElementById('chkShowContextMenu');
+			this.chkEnableDebugOutput = document.getElementById('chkEnableDebugOutput');
+			
+			this.prefs = new RedirectorPrefs();
+			//Preferences
+			this.changedPrefs(this.prefs);
+			this.prefs.addListener(this);
+
+			//Redirect list
+			this.lstRedirects.selType = 'single'; 
+			this.template = document.getElementsByTagName('richlistitem')[0];
+			this.lstRedirects.removeChild(this.template);
+			var list = [];
+			for (var i = 0; i < Redirector.redirectCount; i++) {
+				list.push(Redirector.getRedirectAt(i));
+			}
+			this.addItemsToListBox(list);
+			this.selectionChange();
+			
+			this.strings = document.getElementById('redirector-strings');
+			this.strings.getPluralized = function(id, number) {
+				id += number == 1 ? 'Singular' : '';
+				return this.getFormattedString(id, [number]);	
+			};
+		} catch(e) {
+			alert(e);
+		}
+	},
+	
+	onUnload : function() {
+		this.prefs.dispose();
+	},
+
+	changedPrefs : function(prefs) {
+		this.chkEnableRedirector.setAttribute('checked', prefs.enabled);
+		this.chkShowStatusBarIcon.setAttribute('checked', prefs.showStatusBarIcon);
+		this.chkShowContextMenu.setAttribute('checked', prefs.showContextMenu);
+		this.chkEnableDebugOutput.setAttribute('checked', prefs.debugEnabled);
+	},
+	
+	addItemsToListBox : function(items) {
+
+		var item, row, value, newItem;
+		
+		for each (item in items) {
+			newItem = this.template.cloneNode(true);
+
+			newItem.getElementsByAttribute('name', 'dscrIncludePattern')[0].setAttribute('value', item.includePattern);
+			newItem.getElementsByAttribute('name', 'dscrExcludePattern')[0].setAttribute('value', item.excludePattern);
+			newItem.getElementsByAttribute('name', 'dscrRedirectTo')[0].setAttribute('value', item.redirectUrl);
+			var checkEnabled = newItem.getElementsByAttribute('name', 'chkEnabled')[0];
+			checkEnabled.setAttribute('checked', !item.disabled);
+			newItem.setAttribute('class', item.disabled ? 'disabledRedirect' : '');
+			newItem.item = item;
+			this.lstRedirects.appendChild(newItem);
+			newItem.setAttribute('selected', false)
+		}
+		
+		//Enable, disable functionality
+		this.lstRedirects.addEventListener('click', function(ev) { 
+			if (ev.originalTarget && ev.originalTarget.tagName == 'checkbox') {
+				var parent = ev.originalTarget.parentNode;
+				while (!parent.item) {
+					parent = parent.parentNode;   
+				}
+				parent.item.disabled = !ev.originalTarget.hasAttribute('checked');
+				parent.setAttribute('class', parent.item.disabled ? 'disabledRedirect' : '');
+				Redirector.save();
+			}
+		},false);
+	},
+	
+	moveUp : function(){
+		if (this.lstRedirects.selectedIndex <= 0) {
+			return;
+		}
+		this.switchItems(this.lstRedirects.selectedIndex-1);
+	},
+
+	moveDown : function() {
+		if (this.lstRedirects.selectedIndex == Redirector.redirectCount-1) {
+			return;
+		}
+		this.switchItems(this.lstRedirects.selectedIndex);
+	},
+
+	switchItems : function(firstIndex) {
+		Redirector.switchItems(firstIndex, firstIndex+1);
+		var firstItem = this.lstRedirects.children[firstIndex];
+		var secondItem = this.lstRedirects.children[firstIndex+1];
+		this.lstRedirects.removeChild(secondItem);
+		this.lstRedirects.insertBefore(secondItem, firstItem);
+		this.selectionChange();
+	}, 
+	
+	setListItemValues : function(listItem, item){
+		listItem.getElementsByAttribute('name', 'dscrIncludePattern')[0].setAttribute('value', item.includePattern);
+		listItem.getElementsByAttribute('name', 'dscrExcludePattern')[0].setAttribute('value', item.excludePattern);
+		listItem.getElementsByAttribute('name', 'dscrRedirectTo')[0].setAttribute('value', item.redirectUrl);
+	},
+	
+	preferenceChange : function(event) {
+		this.prefs[event.originalTarget.getAttribute('preference')] = event.originalTarget.hasAttribute('checked');
+	},
+	
+	addRedirect : function() {
+		var args = { saved : false, redirect : new Redirect() };
+		window.openDialog("chrome://redirector/content/ui/editRedirect.xul", "redirect", "chrome,dialog,modal,centerscreen", args);
+		if (args.saved) {
+			Redirector.addRedirect(args.redirect);
+			//Get it from redirector since it has processed it and it's no longer the same
+			//object as the one we added.
+			this.addItemsToListBox([Redirector.getRedirectAt(Redirector.redirectCount-1)]);
+			this.selectionChange();
+		}
+	},
+
+	editRedirect : function() {
+
+		if (this.lstRedirects.selectedIndex == -1) {
+			return;
+		}
+		//.selectedItem is still there after it has been removed, that's why we have the .selectedIndex check above as well
+		var listItem = this.lstRedirects.selectedItem;
+		if (!listItem) {
+			return;
+		}
+		var redirect = listItem.item;
+		var args = { saved: false, "redirect":redirect.clone()};
+		window.openDialog("chrome://redirector/content/ui/editRedirect.xul", "redirect", "chrome,dialog,modal,centerscreen", args);
+
+		if (args.saved) {
+			redirect.copyValues(args.redirect);
+			this.setListItemValues(listItem, redirect);
+			Redirector.save();
+			this.selectionChange(); 		   
+		}
+	},
+	
+	deleteRedirect : function() {
+		var index = this.lstRedirects.selectedIndex;
+
+		if (index == -1) {
+			return;
+		}
+		
+		var text = this.strings.getString("deleteConfirmationText");
+		var title = this.strings.getString("deleteConfirmationTitle");
+		var reallyDelete = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService).confirm(null, title, text);
+		if (!reallyDelete) {
+			return;
+		}		
+		
+		try {
+			this.lstRedirects.removeChild(this.lstRedirects.children[index]);
+			Redirector.deleteRedirectAt(index);
+			this.selectionChange();
+		} catch(e) {
+			alert(e);
+		}
+	},
+	
+	listKeypress : function(event) {
+		if (event.keyCode == 13) { //Enter button
+			this.editRedirect();   
+		} else if (event.keyCode == 46) { //Del button
+			this.deleteRedirect();	 
+		}
+	},
+
+	selectionChange : function() {
+		if (!this.lstRedirects) {
+			return;
+		}
+		var index = this.lstRedirects.selectedIndex;
+
+		this.btnEdit.disabled = (index == -1);
+		this.btnDelete.disabled = (index == -1);
+		this.btnUp.disabled = (index <= 0);
+		this.btnDown.disabled = (index == -1 || index >= Redirector.redirectCount-1);
+		this.btnExport.disabled = (Redirector.redirectCount== 0);
+	},
+
+	getFile : function(captionKey, mode) {
+		//Mostly borrowed from Adblock Plus
+		var picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+		picker.init(window, this.strings.getString(captionKey), mode);
+		picker.defaultExtension = ".rdx";
+		var dir = this.prefs.defaultDir;
+		if (dir) {
+			picker.displayDirectory = new nsLocalFile(dir);
+		}
+		picker.appendFilter(this.strings.getString('redirectorFiles'), '*.rdx');
+		
+		if (picker.show() == picker.returnCancel) {
+			return null;
+		}
+		this.prefs.defaultDir = picker.displayDirectory.path;
+		return picker.file;
+	},
+	
+	export : function() {
+		var file = this.getFile('exportCaption', Ci.nsIFilePicker.modeSave);
+		if (file) {
+			Redirector.exportRedirects(file);
+		}
+	},
+	
+	import : function() {
+		var file = this.getFile('importCaption', Ci.nsIFilePicker.modeOpen);
+		var result;
+		if (!file) {
+			return;
+		}
+		result = Redirector.importRedirects(file);
+		var msg, imported, existed;
+		imported = result & 0xFFFF;
+		existed = result >> 16;
+		
+		if (imported > 0) {
+			msg = this.strings.getPluralized('importedMessage', imported);
+			if (existed > 0) {
+				msg += ', ' + this.strings.getPluralized('existedMessage',existed); 
+			} else {
+				msg += '.'; 
+			}
+		} else if (imported == 0 && existed > 0) {
+			msg = this.strings.getPluralized('allExistedMessage', existed);
+		} else { //Both 0
+			msg = this.strings.getString('importedNone');
+		}
+
+		var title = this.strings.getString("importResult");
+		Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService).alert(null, title, msg);
+
+		if (imported > 0) {
+			var newlist = [];
+			for (var i = Redirector.redirectCount-imported; i < Redirector.redirectCount; i++) {
+				newlist.push(Redirector.getRedirectAt(i));
+			}				
+			this.addItemsToListBox(newlist);
+		}
+	}
+};
diff --git a/chrome/code/unittest/run.html b/chrome/code/unittest/run.html
new file mode 100644
index 0000000..cc178aa
--- /dev/null
+++ b/chrome/code/unittest/run.html
@@ -0,0 +1,99 @@
+<!-- $Id$ -->
+<html>
+	<head>
+		<title>Redirector Unit Tests</title>
+		<style type="text/css">
+			body { font-family: Verdana, Arial; color:black; background-color:white; font-size:0.8em; width:800px; margin:auto; text-align:center;}
+			a { color:blue; }
+			h1 { text-align:center; margin:10px 0px; }
+			table { margin:10px auto; border:solid 1px black; width:700px; border-collapse:collapse;}
+			td { border:solid 1px black; padding:3px; }
+			td.result { width:20px; height:20px; padding:0;}
+			td.result div { width:70%; height:70%; margin:auto;  }
+			button { margin:20px auto; }
+		</style>
+		<script type="text/javascript">
+
+		//Global variables
+		var subscriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"].getService(Components.interfaces.mozIJSSubScriptLoader);
+		var redirector = Components.classes["@einaregilsson.com/redirector;1"].getService(Components.interfaces.rdIRedirector);
+		
+		function setupTest(name, testcase) {
+			var table = document.createElement('table');
+			var row = document.createElement('tr');
+			var cell = document.createElement('th');
+			var testdata;
+			cell.setAttribute('colspan', 2);
+			row.appendChild(cell);
+			table.appendChild(row);
+			cell.innerHTML = name;
+			document.getElementById('tests').appendChild(table);
+			for (var i = 0; i < testcase.tests.length; i++) {
+				var testdata = testcase.tests[i];
+				row = document.createElement('tr');
+				cell = document.createElement('td');
+				cell.setAttribute('class', 'result');
+				var dot = document.createElement('div');
+				dot.setAttribute('id', name + '_' + i);
+				cell.appendChild(dot);
+				
+				row.appendChild(cell);
+				cell = document.createElement('td');
+				cell.innerHTML = testcase.describe(testdata);
+				row.appendChild(cell);
+				table.appendChild(row);
+			}
+		}
+		
+		function setup() {
+			//quick and dirty cleanup
+			document.getElementById('tests').innerHTML = '';
+			subscriptLoader.loadSubScript('chrome://redirector/content/code/redirect.js');
+			subscriptLoader.loadSubScript('chrome://redirector/content/unittest/testcases.js');
+			redirector.reload();
+			
+			var sorted = [];
+			for (var name in tests) {
+				sorted.push(name);
+			}	
+			
+			sorted.sort();			
+			for each(var name in sorted) {
+				setupTest(name, tests[name]);
+			}
+		}
+		
+		function runTests() {
+			for (var testcaseName in tests) {
+				var testcase = tests[testcaseName];
+				for (var i = 0; i < testcase.tests.length; i++) {
+					try {
+						var dot = document.getElementById(testcaseName + '_' + i);
+						var result = testcase.run(testcase.tests[i]);
+						if (result && result.passed) {
+							dot.style.backgroundColor = '#17f816';
+						} else {
+							dot.style.backgroundColor = '#ff0000';
+							if (result && result.message) {
+								dot.parentNode.nextSibling.innerHTML += '<br/><span style="color:red;">' + result.message + '</span>';
+							}
+						}
+					} catch(e) {
+						dot.style.backgroundColor = '#ff0000';
+						dot.parentNode.nextSibling.innerHTML += '<br/><span style="color:red;">' + e + '</span>';
+						;
+					}
+				}
+			}	
+		}
+		
+		</script>
+	</head>
+	<body onload="setup();">
+		<h1>Redirector Unit Tests</h1>
+		<button onclick="runTests();">Run tests</button>
+		<button onclick="setup();">Reload tests</button>
+		<div id="tests">
+		</div>
+	</body>
+</html>
\ No newline at end of file
diff --git a/chrome/code/unittest/testcases.js b/chrome/code/unittest/testcases.js
new file mode 100644
index 0000000..0c0c10c
--- /dev/null
+++ b/chrome/code/unittest/testcases.js
@@ -0,0 +1,129 @@
+//// $Id$
+var nsIContentPolicy = Components.interfaces.nsIContentPolicy;
+
+var tests = {
+	"Wildcard matches" : {
+		run : function(data,log) { 
+			var pattern = data[0],
+				url = data[1],
+				expected = data[2];
+			var parts = expected.split(',');
+			var redirectUrl = '';
+			if (!(parts.length == 1 && parts[0] == '')) {
+				for (var i in parts) {
+					redirectUrl += '$' + (parseFloat(i)+1) + ',';
+				}
+				redirectUrl = redirectUrl.substr(0, redirectUrl.length-1);
+			}
+			var redirect = new Redirect(null, pattern, redirectUrl, Redirect.WILDCARD);
+			var result = redirect.getMatch(url);
+			return { passed: result.isMatch && (result.redirectTo == expected), message : "Expected '" + expected + "', actual was '" + result.redirectTo + "'"};
+		},
+		
+		describe : function(data) { return data[0] + ' == ' + data[1] + ', matches=' + data[2]; },
+		tests : [
+			['http://foo*', 'http://foobar.is', 'bar.is'],
+			['http://foo*', 'http://foo', ''],
+			['*://foo.is', 'http://foo.is', 'http'],
+			['*http://foo.is', 'http://foo.is', ''],
+			['http*foo*', 'http://foobar.is', '://,bar.is'],
+			['http*foo*', 'http://foo', '://,'],
+			['*://f*.is', 'http://foo.is', 'http,oo'],
+			['*http://f*.is', 'http://foo.is', ',oo'],
+			['*foo*', 'http://foo', 'http://,'],
+			['*foo*', 'foobar.is', ',bar.is'],
+			['*foo*', 'http://foobar.is', 'http://,bar.is'],
+			['http://foo.is', 'http://foo.is', ''],
+			['*', 'http://foo.is', 'http://foo.is'],
+			['*://*oo*bar*', 'http://foo.is/bar/baz', 'http,f,.is/,/baz'],
+			['*://**oo*bar*', 'http://foo.is/bar/baz', 'http,,f,.is/,/baz'],
+		]
+	},
+	
+	"Regex matches" : {
+		run : function(data) { 
+			var pattern = data[0],
+				url = data[1],
+				expected = data[2];
+			var parts = expected.split(',');
+			var redirectUrl = '';
+			if (!(parts.length == 1 && parts[0] == '')) {
+				for (var i in parts) {
+					redirectUrl += '$' + (parseFloat(i)+1) + ',';
+				}
+				redirectUrl = redirectUrl.substr(0, redirectUrl.length-1);
+			}
+			var redirect = new Redirect(null, pattern, redirectUrl, Redirect.REGEX, null, null);
+			var result = redirect.getMatch(url);
+			return { passed: result.isMatch && result.redirectTo == expected, message : "Expected '" + expected + "', actual was '" + result.redirectTo + "'"};
+		},
+		
+		describe : function(data) { return data[0] + ' == ' + data[1] + ', matches=' + data[2]; },
+		tests : [
+			['http://foo(.*)', 'http://foobar.is', 'bar.is'],
+			['http://foo(.*)', 'http://foo', ''],
+			['(.*)://foo.is', 'http://foo.is', 'http'],
+			['(.*)http://foo\\.is', 'http://foo.is', ''],
+			['http(.*)foo(.*)', 'http://foobar.is', '://,bar.is'],
+			['http(.*)foo(.*)', 'http://foo', '://,'],
+			['(.*)://f(.*)\\.is', 'http://foo.is', 'http,oo'],
+			['(.*)http://f(.*)\\.is', 'http://foo.is', ',oo'],
+			['(.*)foo(.*)', 'http://foo', 'http://,'],
+			['(.*)foo(.*)', 'foobar.is', ',bar.is'],
+			['(.*)foo(.*)', 'http://foobar.is', 'http://,bar.is'],
+			['http://foo\.is', 'http://foo.is', ''],
+			['(.*)', 'http://foo.is', 'http://foo.is'],
+			['(.*)://(.*)oo(.*)bar(.*)', 'http://foo.is/bar/baz', 'http,f,.is/,/baz'],
+			['(.*)://(.*?)(.*)oo(.*)bar(.*)', 'http://foo.is/bar/baz', 'http,,f,.is/,/baz'],
+		]
+	},
+	
+	"nsIContentPolicy implementation" : {
+		run : function(data) {
+			var runTest = function() {
+				var args = {
+					contentType : nsIContentPolicy.TYPE_DOCUMENT, 
+					contentLocation : "http://foo.is", 
+					requestOrigin : null, 
+					aContext : { loadURI : function(){}}, 
+					mimeTypeGuess : null, 
+					extra : null
+				};
+				for (var key in data[1]) {
+					args[key] = data[1][key];
+				}
+				
+				var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);  
+				args.contentLocation = ioService.newURI(args.contentLocation, null, null);
+				var contentPolicy = redirector.QueryInterface(nsIContentPolicy);
+				var result = contentPolicy.shouldLoad(args.contentType, args.contentLocation, args.requestOrigin, args.aContext, args.mimeTypeGuess, args.extra);
+				return { passed: result == nsIContentPolicy.ACCEPT, message : "Expected nsIContentPolicy.ACCEPT, actual was " + result };
+			}
+			
+			if (typeof data[2] == "function") {
+				return data[2](runTest);
+			} else {
+				return runTest();
+			}
+		},
+		
+		describe : function(data) { return data[0]; },
+		tests : [
+			["Accepts if not TYPE_DOCUMENT", { contentType : nsIContentPolicy.TYPE_STYLESHEET}],
+			["Accepts if not http or https", { contentLocation : "resource://foo/bar"}],
+			["Accepts if no aContext", { aContext : null}],
+			["Accepts if aContext has no loadURI function", { aContext : { foo : function(){}}}],
+			["Accepts if Redirector is not enabled", {}, function(doFunc) {
+				try {
+					redirector.enabled = false;
+					return doFunc();
+					redirector.enabled = true;
+
+				} catch(e) {
+					redirector.enabled = true;
+					throw e;	
+				}
+			}]
+		]		
+	}
+};
diff --git a/chrome/content/code/redirector.prototype.js b/chrome/content/code/redirector.prototype.js
deleted file mode 100644
index be920d8..0000000
--- a/chrome/content/code/redirector.prototype.js
+++ /dev/null
@@ -1,343 +0,0 @@
-//// $Id$
-
-Redirector.prototype = {
-	
-	//rdIRedirector implementation
-	get enabled() {
-		return this._prefs && this._prefs.enabled;	
-	},
-	
-	set enabled(value) {
-		if (this._prefs) {
-			this._prefs.enabled = value;
-		}
-	},
-
-	get redirectCount() {
-		return this._list.length;
-	},
-	
-	addRedirect : function(redirect) {
-		//This runaround is necessary because the redirect
-		//that was created somewhere up in the GUI doesn't
-		//have the Redirect function in scope.
-		var rx = new Redirect();
-		rx.copyValues(redirect);
-		this._list.push(rx);
-		this.save();
-	},
-
-	debug : function(msg) {
-		if (this._prefs.debugEnabled) {
-			this._cout.logStringMessage('REDIRECTOR: ' + msg);
-		}
-	},
-	
-	deleteRedirectAt : function(index) {
-		this._list.splice(index, 1);
-		this.save();
-	},
-	
-	exportRedirects : function(file) {
-		var fileStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
-		const PR_WRONLY 	 = 0x02;
-		const PR_CREATE_FILE = 0x08;
-		const PR_TRUNCATE	 = 0x20;
-
-		fileStream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 0644, 0);
-		var stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
-		stream.init(fileStream, "UTF-8", 16384, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
-		stream.writeString(this._redirectsAsString('\n'));
-		stream.close();
-	},
-	
-	getRedirectAt : function(index) {
-		return this._list[index];	
-	},
-	
-	//Get the redirect url for the given url. This will not check if we are enabled, and
-	//not do any verification on the url, just assume that it is a good string url that is for http/s
-	getRedirectUrl : function(url) {
-		this.debug("Checking " + url);
-		
-		for each (var redirect in this._list) {
-			var result = redirect.getMatch(url);
-			if (result.isExcludeMatch) {
-				this.debug(url + ' matched exclude pattern ' + redirect.excludePattern + ' so the redirect ' + redirect.includePattern + ' will not be used');
-			} else if (result.isDisabledMatch) {
-				this.debug(url + ' matched pattern ' + redirect.includePattern + ' but the redirect is disabled');
-			} else if (result.isMatch) {
-				redirectUrl = this._makeAbsoluteUrl(url, result.redirectTo);
-				
-				//check for loops...
-				result = redirect.getMatch(redirectUrl);
-				if (result.isMatch) {
-					var title = this._getString('invalidRedirectTitle');
-					var msg = this._getFormattedString('invalidRedirectText', [redirect.includePattern, url, redirectUrl]);
-					this.debug(msg);
-					redirect.disabled = true;
-					this.save();					
-					this._msgBox(title, msg);
-				} else {
-					this.debug('Redirecting ' + url + ' to ' + redirectUrl);
-					return redirectUrl;
-				}
-			}
-		}
-		return null;
-	},
-	
-	importRedirects : function(file) {
-		var fileStream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
-		fileStream.init(file, 0x01, 0444, 0); //TODO: Find the actual constants for these magic numbers
-
-		var stream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
-		stream.init(fileStream, "UTF-8", 16384, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
-		stream = stream.QueryInterface(Ci.nsIUnicharLineInputStream);
-
-		var importCount = 0, existsCount = 0;
-		var lines = [];
-		var line = {value: null};
-		stream.readLine(line);
-		while (line.value) {
-			var redirect = new Redirect();
-			redirect.deserialize(line.value.replace('\n', ''));
-			if (this._containsRedirect(redirect)) {
-				existsCount++;
-			} else {
-				this._list.push(redirect);
-				importCount++;
-			}
-			stream.readLine(line);
-		}
-		stream.close();
-		this.save();
-		return importCount | (existsCount << 16);
-	},
-	
-	reload : function() {
-		loader.loadSubScript('chrome://redirector/content/code/redirector.prototype.js');
-		loader.loadSubScript('chrome://redirector/content/code/redirect.js');
-		var oldEnabled = this.enabled;
-		for (var key in Redirector.prototype) {
-			if (key != 'redirectCount' && key != 'enabled') {
-				this[key] = Redirector.prototype[key];
-			}
-		}
-		this._init();
-		this.enabled = oldEnabled;
-	}, 
-	
-	save : function() {
-		this._prefs.redirects = this._redirectsAsString(':::');
-	},
-
-	switchItems : function(index1, index2) {
-		var item = this._list[index1];
-		this._list[index1] = this._list[index2];
-		this._list[index2] = item;
-		this.save();
-	},
-	
-	//End rdIRedirector    
-	
-	// nsIContentPolicy implementation
-	shouldLoad: function(contentType, contentLocation, requestOrigin, aContext, mimeTypeGuess, extra) {
-		rdump('nsIContentPolicy::ShouldLoad ' + contentLocation.spec);
-		try {
-			//This is also done in getRedirectUrl, but we want to exit as quickly as possible for performance
-			if (!this._prefs.enabled) {
-				return Ci.nsIContentPolicy.ACCEPT;
-			}
-			
-			if (contentType != Ci.nsIContentPolicy.TYPE_DOCUMENT) {
-				return Ci.nsIContentPolicy.ACCEPT;
-			}
-
-			if (contentLocation.scheme != "http" && contentLocation.scheme != "https") {
-				return Ci.nsIContentPolicy.ACCEPT;
-			}
-			
-			if (!aContext || !aContext.loadURI) {
-				return Ci.nsIContentPolicy.ACCEPT;
-			}
-			
-			var redirectUrl = this.getRedirectUrl(contentLocation.spec);
-
-			if (!redirectUrl) {
-				return Ci.nsIContentPolicy.ACCEPT;
-			}			
-
-			aContext.loadURI(redirectUrl, requestOrigin, null);
-			return Ci.nsIContentPolicy.REJECT_REQUEST;
-		} catch(e) {
-			this.debug(e);	 
-		}
-		
-	},
-	
-	shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra) {
-		return Ci.nsIContentPolicy.ACCEPT;
-	},
-	//end nsIContentPolicy
-
-	//nsIChannelEventSink implementation
-	
-	//For FF4.0. Got this from a thread about adblock plus, https://adblockplus.org/forum/viewtopic.php?t=5895
-	asyncOnChannelRedirect: function(oldChannel, newChannel, flags, redirectCallback) {
-		this.onChannelRedirect(oldChannel, newChannel, flags);
-		redirectCallback.onRedirectVerifyCallback(0);
-	},	
-	
-	onChannelRedirect: function(oldChannel, newChannel, flags)
-	{
-		try {
-			let newLocation = newChannel.URI.spec;
-			rdump('nsIChannelEventSink::onChannelRedirect ' + newLocation);
-
-			if (!(newChannel.loadFlags & Ci.nsIChannel.LOAD_DOCUMENT_URI)) {
-				//We only redirect documents...
-				return; 
-			}
-
-			if (!this._prefs.enabled) {
-				return;
-			}
-			
-			if (!newLocation) {
-				return;
-			}
-			let callbacks = [];
-			if (newChannel.notificationCallbacks) {
-				callbacks.push(newChannel.notificationCallbacks);
-			}
-			if (newChannel.loadGroup && newChannel.loadGroup.notificationCallbacks) {
-				callbacks.push(newChannel.loadGroup.notificationCallbacks);
-			}
-			var win;
-			var webNav;
-			for each (let callback in callbacks)
-			{
-				try {
-					win = callback.getInterface(Ci.nsILoadContext).associatedWindow;
-					webNav = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
-					break;
-				} catch(e) {}
-			}
-			if (!webNav) {
-				return; 
-			}
-			var redirectUrl = this.getRedirectUrl(newLocation);
-
-			if (redirectUrl) {
-				webNav.loadURI(redirectUrl,null,null,null,null);
-				throw Cr.NS_BASE_STREAM_WOULD_BLOCK; //Throw this because the real error we should throw shows up in console...
-			}			
-			
-		} catch (e if (e != Cr.NS_BASE_STREAM_WOULD_BLOCK)) {
-			// We shouldn't throw exceptions here - this will prevent the redirect.
-			dump("Redirector: Unexpected error in onChannelRedirect: " + e + "\n");
-		}
-	},
-	//end nsIChannelEventSink
-	
-	//Private members and methods
-			
-	_prefs : null,
-	_list : null,
-	_strings : null,
-	_cout : Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService),
-
-	_init : function() {
-		if (this._prefs) {
-			this._prefs.dispose();
-		}
-		this._cout.logStringMessage('REDIRECTOR CREATED');
-		this._prefs = new RedirectorPrefs();
-		//Check if we need to update existing redirects
-		var data = this._prefs.redirects;
-		var version = this._prefs.version;
-		this._loadStrings();
-		var currentVersion = '2.5';
-		//Here update checks are handled
-		if (version == 'undefined') { //Either a fresh install of Redirector, or first time install of v2.0
-			if (data) { //There is some data in redirects, we are upgrading from a previous version, need to upgrade data
-				var tempList = JSON.parse(data);
-				var arr;
-				var newArr = []
-				for each (arr in tempList) {
-					if (arr.length == 5) {
-						arr.push(''); //For those that don't have an exclude pattern. Backwards compatibility is a bitch!
-					}
-					arr.splice(3,1); //Remove the "only if link exists" data
-					newArr.push(arr.join(',,,'));
-				}
-				this._prefs.redirects = newArr.join(':::');
-			}
-			this._prefs.version = currentVersion;
-		} else if (version == '2.0' || version == '2.0.1' || version == '2.0.2') {
-			this._prefs.version = currentVersion;
-		}
-		//Update finished
-		
-		//Now get from the new format
-		data = this._prefs.redirects;
-		var arr;
-		this._list = [];
-		if (data != '') {
-			for each (redirectString in data.split(':::')) {
-				var redirect = new Redirect();
-				redirect.deserialize(redirectString);
-				this._list.push(redirect);
-			}
-		}
-	},
-	
-	_loadStrings : function() {
-		var src = 'chrome://redirector/locale/redirector.properties';
-		var localeService = Cc["@mozilla.org/intl/nslocaleservice;1"].getService(Ci.nsILocaleService);
-		var appLocale = localeService.getApplicationLocale();
-		var stringBundleService = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService);
-		this._strings = stringBundleService.createBundle(src, appLocale);	 
-	},	  
-
-	_redirectsAsString : function(seperator) {
-		return [r.serialize() for each (r in this._list)].join(seperator);
-	},
-	
-	
-	_containsRedirect : function(redirect) {
-		for each (var existing in this._list) {
-			if (existing.equals(redirect)) {
-				return true;
-			}	
-		}
-		return false;
-	},
-	
-	_getString : function(name) {
-		return this._strings.GetStringFromName(name);
-	},
-	
-	_getFormattedString : function(name, params) {
-		return this._strings.formatStringFromName(name, params, params.length);
-	},
-	
-	_msgBox : function(title, text) {
-		Cc["@mozilla.org/embedcomp/prompt-service;1"]
-			.getService(Ci.nsIPromptService)
-				.alert(null, title, text);
-	},
-
-	_makeAbsoluteUrl : function(currentUrl, relativeUrl) {
-		
-		if (relativeUrl.match(/https?:/)) {
-			return relativeUrl;
-		} 
-		
-		var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
-		var uri = ioService.newURI(currentUrl, null, null); 
-		
-		return uri.resolve(relativeUrl);
-	}
-};
diff --git a/chrome/ui/browserOverlay.xul b/chrome/ui/browserOverlay.xul
new file mode 100644
index 0000000..32bf1d5
--- /dev/null
+++ b/chrome/ui/browserOverlay.xul
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- $Id$ -->
+<!DOCTYPE overlay SYSTEM "chrome://redirector/locale/browserOverlay.xul.dtd">
+<overlay id="redirector-overlay"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script src="../code/redirect.js"/>
+  <script src="../code/redirectorprefs.js"/>
+  <script src="../code/browserOverlay.xul.js"/>
+
+  <stringbundleset id="stringbundleset">
+    <stringbundle id="redirector-strings" src="chrome://redirector/locale/redirector.properties"/>
+  </stringbundleset>
+
+  <menupopup id="menu_ToolsPopup">
+    <menuitem id="redirector-menuitem" label="&RedirectorMenuItem.label;"
+              accesskey="&RedirectorMenuItem.accesskey;"
+              oncommand="RedirectorOverlay.onMenuItemCommand(event);"/>
+  </menupopup>
+  <popup id="contentAreaContextMenu">
+    <menuitem id="redirector-context" label="&RedirectorContext.label;"
+              accesskey="&RedirectorContext.accesskey;"
+              insertafter="context-stop"
+              oncommand="RedirectorOverlay.onContextMenuCommand(event)"/>
+  </popup>
+  <statusbar id="status-bar">
+  <statusbarpanel id="redirector-status">
+      <image id="redirector-statusbar-img" src="chrome://redirector/skin/statusactive.png"
+             tooltiptext="Redirector is enabled;"
+             style="width:16px; height:16px;"
+             onclick="RedirectorOverlay.statusBarClick(event);" />
+  </statusbarpanel>
+  </statusbar>
+
+</overlay>
\ No newline at end of file
diff --git a/chrome/ui/editRedirect.xul b/chrome/ui/editRedirect.xul
new file mode 100644
index 0000000..6d7b133
--- /dev/null
+++ b/chrome/ui/editRedirect.xul
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- $Id$ -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://redirector/skin/redirector.css" type="text/css"?>
+<!DOCTYPE dialog SYSTEM "chrome://redirector/locale/editRedirect.xul.dtd">
+<dialog title="&redirectWindow.title;"
+        orient="vertical"
+        autostretch="always"
+        onload="EditRedirect.onLoad();"
+        buttons="accept,cancel"
+        ondialogaccept="return EditRedirect.onAccept();"
+        xmlns:nc="http://home.netscape.com/NC-rdf#"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script type="application/x-javascript" src="../code/redirect.js"/>
+  <script type="application/x-javascript" src="../code/editRedirect.xul.js"/>
+  <stringbundleset id="stringbundleset">
+    <stringbundle id="redirector-strings" src="chrome://redirector/locale/redirector.properties"/>
+  </stringbundleset>
+
+  <grid>
+    <rows class="editRedirects">
+      <row align="center">
+        <text value="&txtExampleUrl.label;" />
+        <textbox id="txtExampleUrl" />
+      </row>
+      <row align="center">
+        <text value="&txtIncludePattern.label;" />
+        <textbox id="txtIncludePattern" taborder="1"/>
+        <button id="btnTestPattern" label="&btnTestPattern.label;" onclick="EditRedirect.testPattern();" taborder="2"/>
+      </row>
+      <row align="center">
+        <text value="&txtExcludePattern.label;" />
+        <textbox id="txtExcludePattern" taborder="3"/>
+      </row>
+      <row align="center">
+        <text value="&txtRedirectUrl.label;" />
+        <textbox id="txtRedirectUrl" taborder="4"/>
+      </row>
+      <row align="center">
+        <text value="&rdoPatternTypes.label;"/>
+        <radiogroup>
+          <hbox>
+            <radio id="rdoWildcard" label="&rdoWildcard.label;" accesskey="&rdoWildcard.accessKey;" selected="true" taborder="5"/>
+            <radio id="rdoRegex" label="&rdoRegex.label;"  accesskey="&rdoRegex.accessKey;" taborder="6"/>
+          </hbox>
+        </radiogroup>
+      </row>
+      <row align="center">
+        <text value="&chkUnescapeMatches.label;" />
+        <hbox>
+          <checkbox id="chkUnescapeMatches" label="" taborder="7"/>
+          <spacer flex="1" />
+        </hbox>
+      </row>
+    </rows>
+  </grid>
+</dialog>
diff --git a/chrome/ui/help.html b/chrome/ui/help.html
new file mode 100644
index 0000000..4534d96
--- /dev/null
+++ b/chrome/ui/help.html
@@ -0,0 +1,182 @@
+<!-- $Id$ -->
+<html>
+	<head>
+		<title>Redirector Help</title>
+		<style type="text/css">
+			body { font-family: Verdana, Arial; color:black; background-color:white; font-size:0.9em;}
+			a { color:blue; }
+		</style>
+	</head>
+	<body>
+		<h1>Redirector Help</h1>
+		<h3>Table of contents</h3>
+		<ul>
+			<li><a href="#whatisredirector">What is Redirector?</a></li>
+			<li><a href="#basicusage">Basic usage</a>
+				<ul>
+					<li><a href="#exampleurl">Example url</a></li>
+					<li><a href="#includepattern">Include pattern</a></li>
+					<li><a href="#excludepattern">Exclude pattern</a></li>
+					<li><a href="#redirectto">Redirect to</a></li>
+					<li><a href="#patterntype">Pattern type</a></li>
+					<li><a href="#unescapematches">Unescape matches</a></li>
+				</ul>
+			</li>
+			<li><a href="#wildcards">Wildcards</a></li>
+			<li><a href="#regularexpressions">Regular expressions</a></li>
+			<li><a href="#examples">Examples</a>
+				<ol>
+					<li><a href="#ex1">Static redirect</a></li>
+					<li><a href="#ex2">Redirect using query string parameter and wildcards</a></li>
+					<li><a href="#ex3">Redirect using query string parameter and regular expressions</a></li>
+					<li><a href="#ex4">Redirect to a different folder using wildcards</a></li>
+					<li><a href="#ex5">Redirect http to https using wildcards</a></li>
+				</ol>
+			
+			</li>
+		</ul>
+		
+		
+		<a name="whatisredirector"></a>
+		<h4>What is Redirector?</h4>
+		
+		<p>Redirector is an extension for Firefox that allows you to automatically redirect from
+		one webpage to another. For example, every time you visit http://abc.com you will automatically
+		load http://def.com instead. This can be useful for instance to always redirect articles to printer friendly
+		versions, redirect http:// to https:// for sites that support both, bypass advertising pages that appear before
+		being able to view certain pages and more.</p>
+		
+		<a name="basicusage"></a>
+		<h4>Basic usage</h4>
+		<p>To add a new redirect you can go to the <em>Tools</em> menuitem and select <em>Redirector</em>. That will
+		open the <em>Redirector settings</em> window which shows all your redirects. The window can also be opened
+		by right clicking on the <strong>R</strong> icon in your statusbar. 
+		There you can press the <em>Add...</em> button and then you can enter the details for the new redirect. A redirect
+		consists of a few things:
+		<ul>
+			<li><a name="exampleurl"></a><strong>Example url:</strong> This is an example of an url you want to redirect. It is not really used for anything,
+			it's just there to show what types of urls you're targetting. You can leave this out, but then you can't use the <em>Test pattern</em> button.</li>
+			
+			<li><a name="includepattern"></a><strong>Include pattern:</strong> This is pattern for the urls you want to redirect. In the simplest case, where you just want
+			to redirect one specific url to another then this will just be the exact url you want to redirect. For instance, if you just want http://aaa.com to
+			redirect to http://bbb.com then <em>Include pattern</em> will just be http://aaa.com. For more complex patterns that match many
+			urls you can use either <a href="#wildcards">wildcards</a> or <a href="#regularexpressions">regular expressions</a>.</li>
+			
+            <li><a name="excludepattern"></a><strong>Exclude pattern:</strong> Urls that match this pattern will never be redirected. This is not necessary to
+            fill out, but can be useful when you want to redirect all urls that contain some text except if they contain some other text. 
+            Redirects like that can often be done with a complex regular expression, but using an exclude pattern makes it much simpler. The exclude
+            patterns can use wildcard characters or regular expressions like the include patterns.</li>
+
+            <li><a name="redirectto"></a><strong>Redirect to:</strong> This is the url that you will be redirected to when you open any page where the url matches the
+            include pattern. You can use the special signs $1, $2, $3 etc. in the url, they will be replaced by the results of captures with regular
+            expressions or stars with wildcards. For instance, if you have the include pattern <em>http://google.com/*</em>, redirect to <em>http://froogle.com/$1</em>
+            and you open the page http://google.com/foobar, then you will be redireced to http://froogle.com/foobar, since 'foobar' was what the star replaced. $1 is for the
+            first star in the pattern, $2 for the second and so on. For regular expression $1 is for the first parantheses, $2 for the second etc.</li>
+		
+            <li><a name="patterntype"></a><strong>Pattern type:</strong> This specifies how Redirector should interpret the patterns, either as
+            <a href="#wildcards">wildcards</a> or <a href="#regularexpressions#">regular expressions</a>.</li>
+
+            <li><a name="unescapematches"></a><strong>Unescape matches:</strong> A common usage of Redirector is to catch urls like
+            <em>http://foo.com/redirect.php?url=http%3A%2F%2Fbar%2Ecom%2Fpath</em> and try to catch the url parameter and redirect to it. A pattern
+            like <em>http://foo.com/redirect.php?url=*</em> might be used for that purpose. However, if the url parameter is <em>escaped</em> (also known
+            as <em>urlencoded</em>) then that won't work. In the url above we see that it starts with <em>http%3A%2F%2F</em> instead of <em>http://</em>, and Firefox
+            won't accept this as a new url to redirect to. So, in cases like these you can check the <em>Unescape matches</em> option and then all
+            matches will be unescaped (turned from e.g. <em>http%3A%2F%2Fbar%2Ecom</em> to <em>http://bar.com</em>) before being inserted into the target url.
+            </li>
+            
+		</ul>
+		</p>
+
+		<a name="wildcards"></a>
+		<h4>Wildcards</h4>
+		
+		<p>Wildcards are the simplest way to specify include and exclude patterns. When you create a wildcard pattern there
+		is just one special character, the asterisk *. An asterisk in your pattern will match zero or more characters and you can
+		have more than one star in your pattern. Some examples:
+			<ul>
+				<li><em>http://example.com/*</em> matches http://example.com/, http://example.com/foo, http://example.com/bar and all other urls that start with http://example.com/.</li>
+				<li><em>http://*.example.com</em> matches all subdomains of example.com, like http://www.example.com, http://mail.example.com.</li>
+				<li><em>http*://example.com</em> matches both http://example.com and https://example.com.</li>
+				<li><em>http://example.com/index.asp*</em> matches http://example.com/index.asp, http://example.com/index.asp?a=b&c=d.</li>
+			</ul>
+		$1, $2, $3 in the redirect urls will match the text that the stars matched. Examples:
+			<ul>
+				<li><em>http://example.com/*</em> matches http://example.com/foobar, $1 is foobar.</li>
+				<li><em>http://*.example.com/*</em> matches http://www.example.com/foobar, $1 is www, $2 is foobar.</li>
+			</ul>
+		</p>
+		
+		<a name="regularexpressions"></a>
+		<h4>Regular expressions</h4>
+		
+		<p>Regular expressions allow for more complicated patterns but they are a lot harder to learn than wildcards. I'm not gonna
+		create a regex tutorial here but normal javascript regex syntax works, look at <a href="http://regular-expressions.info" target="_blank">http://regular-expressions.info</a> for
+		an introduction to regular expressions. $1,$2 etc. can be used in the redirect url and will be replaced with contents of captures in
+		the regular expressions. Captures are specified with parantheses. Example: http://example.com/index.asp\?id=(\d+) will match the url
+		http://example.com/index.asp?id=12345 and $1 will be replaced by 12345. (A common mistake in regex patterns is to forget to escape
+		the ? sign in the querystring of the url. ? is a special character in regular expressions so if you want to match an url with a querystring
+		you should escape it as \?).</p>
+
+		<a name="examples"></a>
+		<h4>Examples</h4>
+		
+		<ol>
+			<li>
+				<strong><a name="ex1"></a>Static redirect</strong><br/>
+				Redirects from http://example.com/foo to http://example.com/bar
+				<p>
+					<strong>Include pattern:</strong> http://example.com/foo<br/>
+					<strong>Exclude pattern:</strong><br/>
+					<strong>Redirect to:</strong> http://example.com/bar<br/>
+					<strong>Pattern type:</strong> Wildcard<br />
+				</p>
+				
+			</li>
+			<li>
+				<strong><a name="ex2"></a>Redirect using query string parameter and wildcards</strong><br/>
+				Redirects from http://example.com/index.php?id=12345&a=b to http://example.com/printerfriendly.php?id=12345&a=b
+				where 12345 could be any number.
+				<p>
+					<strong>Include pattern:</strong> http://example.com/index.php?id=*&a=b<br/>
+					<strong>Exclude pattern:</strong><br/>
+					<strong>Redirect to:</strong> http://example.com/printerfriendly.com?id=$1&a=b<br/>
+					<strong>Pattern type:</strong> Wildcard<br />
+				</p>
+			</li>
+			<li>
+				<strong><a name="ex3"></a>Redirect using query string parameter and regular expressions</strong><br/>
+				Redirects from http://example.com/index.php?id=12345&a=b to http://example.com/printerfriendly.php?id=12345&a=b
+				where 12345 could be any number.
+				<p>
+					<strong>Include pattern:</strong> http://example.com/index.php\?id=(\d+)&a=b<br/>
+					<strong>Exclude pattern:</strong><br/>
+					<strong>Redirect to:</strong> http://example.com/printerfriendly.com?id=$1&a=b<br/>
+					<strong>Pattern type:</strong> Regular Expression<br />
+				</p>
+			</li>
+			<li>
+				<strong><a name="ex4"></a>Redirect to a different folder using wildcards</strong><br/>
+				Redirects from http://example.com/category/fish/index.php to http://example.com/category/cats/index.php
+				where fish could be any word. The exclude pattern makes sure that there is only one
+				folder there, so for instance http://example.com/category/fish/cat/mouse/index.php would not match.
+				<p>
+					<strong>Include pattern:</strong> http://example.com/category/*/index.php<br/>
+					<strong>Exclude pattern:</strong> http://example.com/category/*/*/index.php<br/>
+					<strong>Redirect to:</strong> http://example.com/category/cats/index.php<br/>
+					<strong>Pattern type:</strong> Wildcard<br />
+				</p>
+			</li>
+			<li>
+				<strong><a name="ex5"></a>Redirect http to https using wildcards</strong><br/>
+				Redirects from http://mail.google.com/randomcharacters to https://mail.google.com/randomcharacters
+				where randomcharacters could be anything.
+				<p>
+					<strong>Include pattern:</strong> http://mail.google.com*<br/>
+					<strong>Exclude pattern:</strong><br/>
+					<strong>Redirect to:</strong> https://mail.google.com$1<br/>
+					<strong>Pattern type:</strong> Wildcard<br />
+				</p>
+			</li>
+		</ol>
+	</body>
+</html>
\ No newline at end of file
diff --git a/chrome/ui/settings.xul b/chrome/ui/settings.xul
new file mode 100644
index 0000000..ccb9569
--- /dev/null
+++ b/chrome/ui/settings.xul
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- $Id$ -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://redirector/skin/redirector.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://redirector/locale/settings.xul.dtd">
+<window title="&window.title;"
+        orient="vertical"
+        onload="Settings.onLoad();"
+        onunload="Settings.onUnload();"
+        buttons="accept"
+        width="650px"
+        height="500px"
+        id="redirectorSettings"
+        windowtype="redirectorSettings"
+		xmlns:nc="http://home.netscape.com/NC-rdf#"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+	<script type="application/x-javascript" src="../code/redirectorprefs.js"/>
+	<script type="application/x-javascript" src="../code/redirect.js"/>
+	<script type="application/x-javascript" src="../code/settings.xul.js"/>
+	<stringbundleset id="stringbundleset">
+		<stringbundle id="redirector-strings" src="chrome://redirector/locale/redirector.properties"/>
+	</stringbundleset>
+	<tabbox flex="1" >
+		<tabs>
+			<tab label="&tabRedirects.label;" accesskey="&tabRedirects.accesskey;" />
+			<tab label="&tabPreferences.label;" accesskey="&tabPreferences.accesskey;" />
+			<tab label="&tabImportExport.label;" accesskey="&tabImportExport.accesskey;" />
+			<tab label="&tabHelp.label;" accesskey="&tabHelp.accesskey;" />
+		</tabs>
+		<tabpanels flex="1">
+			<tabpanel flex="1">
+				<vbox flex="1">
+					<richlistbox seltype="single" id="lstRedirects" flex="1" onkeypress="Settings.listKeypress(event);" ondblclick="Settings.editRedirect();" onselect="Settings.selectionChange();">
+						<richlistitem class="SettingsItem" selected="false">
+							<grid>
+								<cols />
+								<rows class="redirectRows">
+									<row>
+										<label value="&colIncludePattern.label;:" />
+										<description name="dscrIncludePattern" />
+									</row>
+									<row>
+										<label value="&colExcludePattern.label;:" />
+										<description name="dscrExcludePattern" />
+									</row>
+									<row>
+										<label value="&colRedirectTo.label;:" />
+										<description name="dscrRedirectTo" />
+									</row>
+									<row>
+										<label value="&colEnabled.label;:" />
+										<hbox><checkbox checked="false" name="chkEnabled" label="" /> <spacer flex="1"/></hbox>
+									</row>
+								</rows>
+							</grid>
+						</richlistitem>
+					</richlistbox>
+					<hbox>
+						<button id="btnAdd" oncommand="Settings.addRedirect();" accesskey="&btnAdd.accesskey;" label="&btnAdd.label;" tooltiptext="&btnAdd.tooltip;" disabled="false" />
+						<button id="btnEdit" oncommand="Settings.editRedirect();" accesskey="&btnEdit.accesskey;" label="&btnEdit.label;" tooltiptext="&btnEdit.tooltip;" disabled="true" />
+						<button id="btnDelete" oncommand="Settings.deleteRedirect();" accesskey="&btnDelete.accesskey;" label="&btnDelete.label;" tooltiptext="&btnDelete.tooltip;" disabled="true" />
+						<button id="btnUp" oncommand="Settings.moveUp();" tooltiptext="&btnUp.tooltip;" disabled="true" />
+						<button id="btnDown" oncommand="Settings.moveDown();" tooltiptext="&btnDown.tooltip;" disabled="true" />
+					</hbox>
+				</vbox>
+			</tabpanel>
+			<tabpanel>
+				<vbox flex="1">
+					<groupbox>
+						<caption label="&grpGeneralPreferences.label;" />
+						<hbox>
+							<checkbox id="chkEnableRedirector" label="&chkEnableRedirector.label;" oncommand="Settings.preferenceChange(event);" accesskey="&chkEnableRedirector.accesskey;" preference="enabled" />
+							<spacer flex="1" />
+						</hbox>
+						<hbox>
+							<checkbox id="chkShowStatusBarIcon" label="&chkShowStatusBarIcon.label;" oncommand="Settings.preferenceChange(event);" accesskey="&chkShowStatusBarIcon.accesskey;" preference="showStatusBarIcon" />
+							<spacer flex="1" />
+						</hbox>
+						<hbox>
+							<checkbox id="chkShowContextMenu" label="&chkShowContextMenu.label;" oncommand="Settings.preferenceChange(event);" accesskey="&chkShowContextMenu.accesskey;" preference="showContextMenu" />
+							<spacer flex="1" />
+						</hbox>
+					</groupbox>
+					<groupbox>
+						<caption label="&grpDebuggingPreferences.label;" />
+						<hbox>
+							<checkbox id="chkEnableDebugOutput" label="&chkEnableDebugOutput.label;" oncommand="Settings.preferenceChange(event);" accesskey="&chkEnableDebugOutput.accesskey;" preference="debugEnabled" />
+							<spacer flex="1" />
+						</hbox>
+					</groupbox>
+					<spacer flex="1" />
+				</vbox>
+			</tabpanel>
+			<tabpanel>
+				<groupbox flex="1" id="grpImportExport">
+					<vbox>
+						<hbox align="middle">
+							<button id="btnImport" accesskey="&btnImport.accesskey;" oncommand="Settings.import();" label="&btnImport.label;"/>
+							<label id="lblImport" value="&lblImport.label;" />
+							<spacer flex="1" />
+						</hbox>
+						<hbox>
+							<button id="btnExport" accesskey="&btnExport.accesskey;" oncommand="Settings.export();" label="&btnExport.label;"/>
+							<label id="lblExport" value="&lblExport.label;" />
+							<spacer flex="1" />
+						</hbox>
+						<spacer flex="1" />
+					</vbox>
+				</groupbox>
+			</tabpanel>
+			<tabpanel>
+				<browser type="content" src="chrome://redirector/content/ui/help.html" flex="1" />
+			</tabpanel>
+		</tabpanels>
+	</tabbox>
+</window>
diff --git a/chrome/ui/skin/movedown.png b/chrome/ui/skin/movedown.png
new file mode 100644
index 0000000..d32b79c
Binary files /dev/null and b/chrome/ui/skin/movedown.png differ
diff --git a/chrome/ui/skin/movedowndisabled.png b/chrome/ui/skin/movedowndisabled.png
new file mode 100644
index 0000000..afd8fc6
Binary files /dev/null and b/chrome/ui/skin/movedowndisabled.png differ
diff --git a/chrome/ui/skin/moveup.png b/chrome/ui/skin/moveup.png
new file mode 100644
index 0000000..3025378
Binary files /dev/null and b/chrome/ui/skin/moveup.png differ
diff --git a/chrome/ui/skin/moveupdisabled.png b/chrome/ui/skin/moveupdisabled.png
new file mode 100644
index 0000000..e526b29
Binary files /dev/null and b/chrome/ui/skin/moveupdisabled.png differ
diff --git a/chrome/ui/skin/redirector.css b/chrome/ui/skin/redirector.css
new file mode 100644
index 0000000..ff9762e
--- /dev/null
+++ b/chrome/ui/skin/redirector.css
@@ -0,0 +1,15 @@
+/* $Id$ */
+
+.disabledRedirect { color:grey; }
+#lstRedirects richlistitem { border-bottom:dotted 1px grey; padding:3px; }
+.redirectRows > row > label { font-weight:bold;}
+.editRedirects > row > textbox { width: 350px; }
+#redirectorSettings > tabbox { margin:4px; }
+#btnUp { list-style-image: url('chrome://redirector/skin/moveup.png'); }
+#btnDown { list-style-image: url('chrome://redirector/skin/movedown.png'); }
+#btnUp[disabled=true] { list-style-image: url('chrome://redirector/skin/moveupdisabled.png'); }
+#btnDown[disabled=true] { list-style-image: url('chrome://redirector/skin/movedowndisabled.png'); }
+
+#btnUp, #btnDown { width:25px; min-width:25px; }
+#lblExport, #lblImport { padding-top:5px; }
+#grpImportExport { padding-top:10px; padding-left:5px;}
\ No newline at end of file
diff --git a/chrome/ui/skin/redirector.png b/chrome/ui/skin/redirector.png
new file mode 100644
index 0000000..f8de12c
Binary files /dev/null and b/chrome/ui/skin/redirector.png differ
diff --git a/chrome/ui/skin/statusactive.png b/chrome/ui/skin/statusactive.png
new file mode 100644
index 0000000..06ce766
Binary files /dev/null and b/chrome/ui/skin/statusactive.png differ
diff --git a/chrome/ui/skin/statusinactive.png b/chrome/ui/skin/statusinactive.png
new file mode 100644
index 0000000..8b83562
Binary files /dev/null and b/chrome/ui/skin/statusinactive.png differ
-- 
cgit v1.2.3-70-g09d2