From bfba044b5f2acc4d83f0d995a03f421ef9d826b7 Mon Sep 17 00:00:00 2001
From: Mark Spencer <markster@digium.com>
Date: Sat, 1 Apr 2006 08:49:54 +0000
Subject: [PATCH] Flesh out the remainder of the manager + http changes and
 create a sample application to partially demonstrate the capability of
 manager over http.

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@16850 65c4cc65-6c06-0410-ace0-fbb531ad65f3
---
 Makefile                    |    7 +
 configs/http.conf.sample    |   13 +-
 configs/manager.conf.sample |    9 +-
 doc/ajam.txt                |   91 ++
 http.c                      |  220 ++++-
 include/asterisk/http.h     |    2 +
 manager.c                   |  576 ++++++++++-
 static-http/ajamdemo.html   |  215 +++++
 static-http/astman.css      |   34 +
 static-http/astman.js       |  256 +++++
 static-http/prototype.js    | 1781 +++++++++++++++++++++++++++++++++++
 11 files changed, 3165 insertions(+), 39 deletions(-)
 create mode 100644 doc/ajam.txt
 create mode 100644 static-http/ajamdemo.html
 create mode 100644 static-http/astman.css
 create mode 100644 static-http/astman.js
 create mode 100644 static-http/prototype.js

diff --git a/Makefile b/Makefile
index b4f722c966..9c058fae4c 100644
--- a/Makefile
+++ b/Makefile
@@ -566,6 +566,13 @@ clean: clean-depend
 
 datafiles: all
 	if [ x`$(ID) -un` = xroot ]; then sh build_tools/mkpkgconfig $(DESTDIR)/usr/lib/pkgconfig; fi
+	# Should static HTTP be installed during make samples or even with its own target ala
+	# webvoicemail?  There are portions here that *could* be customized but might also be
+	# improved a lot.  I'll put it here for now.
+	mkdir -p $(DESTDIR)$(ASTVARLIBDIR)/static-http
+	for x in static-http/*; do \
+		install -m 644 $$x $(DESTDIR)$(ASTVARLIBDIR)/static-http ; \
+	done
 	mkdir -p $(DESTDIR)$(ASTVARLIBDIR)/sounds/digits
 	mkdir -p $(DESTDIR)$(ASTVARLIBDIR)/sounds/priv-callerintros
 	for x in sounds/digits/*.gsm; do \
diff --git a/configs/http.conf.sample b/configs/http.conf.sample
index aea95643f1..7135463d67 100644
--- a/configs/http.conf.sample
+++ b/configs/http.conf.sample
@@ -4,15 +4,20 @@
 ;
 [general]
 ;
-; Whether HTTP interface is enabled or not.
+; Whether HTTP interface is enabled or not.  Default is no.
 ;
-enabled=no
+;enabled=yes
 ;
-; Address to bind to
+; Whether Asterisk should serve static content from http-static
+; Default is no.
+;
+;enablestatic=yes
+;
+; Address to bind to.  Default is 0.0.0.0
 ;
 bindaddr=127.0.0.1
 ;
-; Port to bind to
+; Port to bind to (default is 8088)
 ;
 bindport=8088
 ;
diff --git a/configs/manager.conf.sample b/configs/manager.conf.sample
index cdca9bc631..1db1ef4cc7 100644
--- a/configs/manager.conf.sample
+++ b/configs/manager.conf.sample
@@ -13,11 +13,18 @@
 ; ---------------------------- SECURITY NOTE -------------------------------
 ; Note that you should not enable the AMI on a public IP address. If needed,
 ; block this TCP port with iptables (or another FW software) and reach it
-; with IPsec, SSH, or SSL vpn tunnel
+; with IPsec, SSH, or SSL vpn tunnel.  You can also make the manager 
+; interface available over http if Asterisk's http server is enabled in
+; http.conf and if both "enabled" and "webenabled" are set to yes in
+; this file.  Both default to no.  httptimeout provides the maximum 
+; timeout in seconds before a web based session is discarded.  The 
+; default is 60 seconds.
 ;
 [general]
 enabled = no
+;webenabled = yes
 port = 5038
+;httptimeout = 60
 bindaddr = 0.0.0.0
 ;displayconnects = yes
 ;
diff --git a/doc/ajam.txt b/doc/ajam.txt
new file mode 100644
index 0000000000..d3babd0c20
--- /dev/null
+++ b/doc/ajam.txt
@@ -0,0 +1,91 @@
+Asynchronous Javascript Asterisk Manger (AJAM)
+==============================================
+
+AJAM is a new technology which allows web browsers or other HTTP enabled 
+applications and web pages to directly access the Asterisk Manger 
+Interface (AMI) via HTTP.  Setting up your server to process AJAM 
+involves a few steps:
+
+Setup the Asterisk HTTP server
+------------------------------
+
+1) Uncomment the line "enabled=yes" in /etc/asterisk/http.conf to enable
+   Asterisk's builtin micro HTTP server.
+
+2) If you want Asterisk to actually deliver simple HTML pages, CSS, 
+   javascript, etc. you should uncomment "enablestatic=yes"
+
+3) Adjust your "bindaddr" and "bindport" settings as appropriate for 
+   your desired accessibility
+
+4) Adjust your "prefix" if appropriate, which must be the beginning of
+   any URI on the server to match.  The default is "asterisk" and the 
+   rest of these instructions assume that value.
+
+Allow Manager Access via HTTP
+-----------------------------
+
+1) Make sure you have both "enabled = yes" and "webenabled = yes" setup 
+   in /etc/asterisk/manager.conf
+
+2) You may also use "httptimeout" to set a default timeout for HTTP 
+   connections.
+
+3) Make sure you have a manager username/secret
+
+Once those configurations are complete you can reload or restart 
+Asterisk and you should be able to point your web browser to specific 
+URI's which will allow you to access various web functions.  A complete 
+list can be found by typing "show http" at the Asterisk CLI.
+
+examples:
+
+http://localhost:8088/asterisk/manager?action=login&username=foo&secret=bar
+
+This logs you into the manager interface's "HTML" view.  Once you're 
+logged in, Asterisk stores a cookie on your browser (valid for the 
+length of httptimeout) which is used to connect to the same session.  
+
+http://localhost:8088/asterisk/rawman?action=status
+
+Assuming you've already logged into manager, this URI will give you a 
+"raw" manager output for the "status" command.
+
+http://localhost:8088/asterisk/mxml?action=status
+
+This will give you the same status view but represented as AJAX data, 
+theoretically compatible with RICO (http://www.openrico.org).
+
+http://localhost:8088/asterisk/static/ajamdemo.html
+
+If you have enabled static content support and have done a make install, 
+Asterisk will serve up a demo page which presents a live, but very 
+basic, "astman" like interface.  You can login with your username/secret 
+for manager and have a basic view of channels as well as transfer and 
+hangup calls.  It's only tested in Firefox, but could probably be made
+to run in other browsers as well.
+
+A sample library (astman.js) is included to help ease the creation of 
+manager HTML interfaces.
+
+Note that for the demo, there is no need for *any* external web server.
+
+Integration with other web servers 
+---------------------------------- 
+
+Asterisk's micro HTTP server is *not* designed to replace a general 
+purpose web server and it is intentionally created to provide only the 
+minimal interfaces required.  Even without the addition of an external 
+web server, one can use Asterisk's interfaces to implement screen pops 
+and similar tools pulling data from other web servers using iframes, 
+div's etc.  If you want to integrate CGI's, databases, PHP, etc.  you 
+will likely need to use a more traditional web server like Apache and 
+link in your Asterisk micro HTTP server with something like this:
+
+ProxyPass /asterisk http://localhost:8088/asterisk
+
+This is a fairly new technology so I'd love to hear if it's useful for 
+you!
+
+Mark
+
diff --git a/http.c b/http.c
index cdcde76a88..5fcdc8ac91 100644
--- a/http.c
+++ b/http.c
@@ -33,16 +33,20 @@
 #include <netinet/in.h>
 #include <sys/time.h>
 #include <sys/socket.h>
+#include <sys/stat.h>
 #include <sys/signal.h>
 #include <arpa/inet.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <pthread.h>
 
+#include "asterisk.h"
 #include "asterisk/cli.h"
 #include "asterisk/http.h"
 #include "asterisk/utils.h"
 #include "asterisk/strings.h"
+#include "asterisk/options.h"
+#include "asterisk/config.h"
 
 #define MAX_PREFIX 80
 #define DEFAULT_PREFIX "asterisk"
@@ -61,6 +65,100 @@ static pthread_t master = AST_PTHREADT_NULL;
 static char prefix[MAX_PREFIX];
 static int prefix_len = 0;
 static struct sockaddr_in oldsin;
+static int enablestatic=0;
+
+/* Limit the kinds of files we're willing to serve up */
+static struct {
+	char *ext;
+	char *mtype;
+} mimetypes[] = {
+	{ "png", "image/png" },
+	{ "jpg", "image/jpeg" },
+	{ "js", "application/x-javascript" },
+	{ "wav", "audio/x-wav" },
+	{ "mp3", "audio/mpeg" },
+};
+
+static char *ftype2mtype(const char *ftype, char *wkspace, int wkspacelen)
+{
+	int x;
+	if (ftype) {
+		for (x=0;x<sizeof(mimetypes) / sizeof(mimetypes[0]); x++) {
+			if (!strcasecmp(ftype, mimetypes[x].ext))
+				return mimetypes[x].mtype;
+		}
+	}
+	snprintf(wkspace, wkspacelen, "text/%s", ftype ? ftype : "plain");
+	return wkspace;
+}
+
+static char *static_callback(struct sockaddr_in *req, const char *uri, struct ast_variable *vars, int *status, char **title, int *contentlength)
+{
+	char result[4096];
+	char *c=result;
+	char *path;
+	char *ftype, *mtype;
+	char wkspace[80];
+	struct stat st;
+	int len;
+	int fd;
+	void *blob;
+
+	/* Yuck.  I'm not really sold on this, but if you don't deliver static content it makes your configuration 
+	   substantially more challenging, but this seems like a rather irritating feature creep on Asterisk. */
+	if (!enablestatic || ast_strlen_zero(uri))
+		goto out403;
+	/* Disallow any funny filenames at all */
+	if ((uri[0] < 33) || strchr("./|~@#$%^&*() \t", uri[0]))
+		goto out403;
+	if (strstr(uri, "/.."))
+		goto out403;
+		
+	if ((ftype = strrchr(uri, '.')))
+		ftype++;
+	mtype=ftype2mtype(ftype, wkspace, sizeof(wkspace));
+	
+	/* Cap maximum length */
+	len = strlen(uri) + strlen(ast_config_AST_VAR_DIR) + strlen("/static-http/") + 5;
+	if (len > 1024)
+		goto out403;
+		
+	path = alloca(len);
+	sprintf(path, "%s/static-http/%s", ast_config_AST_VAR_DIR, uri);
+	if (stat(path, &st))
+		goto out404;
+	if (S_ISDIR(st.st_mode))
+		goto out404;
+	fd = open(path, O_RDONLY);
+	if (fd < 0)
+		goto out403;
+	
+	len = st.st_size + strlen(mtype) + 40;
+	
+	blob = malloc(len);
+	if (blob) {
+		c = blob;
+		sprintf(c, "Content-type: %s\r\n\r\n", mtype);
+		c += strlen(c);
+		*contentlength = read(fd, c, st.st_size);
+		if (*contentlength < 0) {
+			close(fd);
+			free(blob);
+			goto out403;
+		}
+	}
+	return blob;
+
+out404:
+	*status = 404;
+	*title = strdup("Not Found");
+	return ast_http_error(404, "Not Found", NULL, "Nothing to see here.  Move along.");
+
+out403:
+	*status = 403;
+	*title = strdup("Access Denied");
+	return ast_http_error(403, "Access Denied", NULL, "Sorry, I cannot let you do that, Dave.");
+}
 
 
 static char *httpstatus_callback(struct sockaddr_in *req, const char *uri, struct ast_variable *vars, int *status, char **title, int *contentlength)
@@ -86,7 +184,15 @@ static char *httpstatus_callback(struct sockaddr_in *req, const char *uri, struc
 	ast_build_string(&c, &reslen, "<tr><td colspan=\"2\"><hr></td></tr>\r\n");
 	v = vars;
 	while(v) {
-		ast_build_string(&c, &reslen, "<tr><td><i>Submitted Variable '%s'</i></td><td>%s</td></tr>\r\n", v->name, v->value);
+		if (strncasecmp(v->name, "cookie_", 7))
+			ast_build_string(&c, &reslen, "<tr><td><i>Submitted Variable '%s'</i></td><td>%s</td></tr>\r\n", v->name, v->value);
+		v = v->next;
+	}
+	ast_build_string(&c, &reslen, "<tr><td colspan=\"2\"><hr></td></tr>\r\n");
+	v = vars;
+	while(v) {
+		if (!strncasecmp(v->name, "cookie_", 7))
+			ast_build_string(&c, &reslen, "<tr><td><i>Cookie '%s'</i></td><td>%s</td></tr>\r\n", v->name, v->value);
 		v = v->next;
 	}
 	ast_build_string(&c, &reslen, "</table><center><font size=\"-1\"><i>Asterisk and Digium are registered trademarks of Digium, Inc.</i></font></center></body>\r\n");
@@ -100,6 +206,13 @@ static struct ast_http_uri statusuri = {
 	.has_subtree = 0,
 };
 	
+static struct ast_http_uri staticuri = {
+	.callback = static_callback,
+	.description = "Asterisk HTTP Static Delivery",
+	.uri = "static",
+	.has_subtree = 1,
+};
+	
 char *ast_http_error(int status, const char *title, const char *extra_header, const char *text)
 {
 	char *c = NULL;
@@ -153,7 +266,7 @@ void ast_http_uri_unlink(struct ast_http_uri *urih)
 	}
 }
 
-static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char **title, int *contentlength)
+static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char **title, int *contentlength, struct ast_variable **cookies)
 {
 	char *c;
 	char *turi;
@@ -176,9 +289,9 @@ static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char **
 			if (val) {
 				*val = '\0';
 				val++;
+				ast_uri_decode(val);
 			} else 
 				val = "";
-			ast_uri_decode(val);
 			ast_uri_decode(var);
 			if ((v = ast_variable_new(var, val))) {
 				if (vars)
@@ -189,6 +302,11 @@ static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char **
 			}
 		}
 	}
+	if (prev)
+		prev->next = *cookies;
+	else
+		vars = *cookies;
+	*cookies = NULL;
 	ast_uri_decode(uri);
 	if (!strncasecmp(uri, prefix, prefix_len)) {
 		uri += prefix_len;
@@ -227,9 +345,12 @@ static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char **
 static void *ast_httpd_helper_thread(void *data)
 {
 	char buf[4096];
+	char cookie[4096];
 	char timebuf[256];
 	struct ast_http_server_instance *ser = data;
+	struct ast_variable *var, *prev=NULL, *vars=NULL;
 	char *uri, *c, *title=NULL;
+	char *vname, *vval;
 	int status = 200, contentlength = 0;
 	time_t t;
 
@@ -252,25 +373,68 @@ static void *ast_httpd_helper_thread(void *data)
 				*c = '\0';
 			}
 		}
+
+		while (fgets(cookie, sizeof(cookie), ser->f)) {
+			/* Trim trailing characters */
+			while(!ast_strlen_zero(cookie) && (cookie[strlen(cookie) - 1] < 33)) {
+				cookie[strlen(cookie) - 1] = '\0';
+			}
+			if (ast_strlen_zero(cookie))
+				break;
+			if (!strncasecmp(cookie, "Cookie: ", 8)) {
+				vname = cookie + 8;
+				vval = strchr(vname, '=');
+				if (vval) {
+					/* Ditch the = and the quotes */
+					*vval = '\0';
+					vval++;
+					if (*vval)
+						vval++;
+					if (strlen(vval))
+						vval[strlen(vval) - 1] = '\0';
+					var = ast_variable_new(vname, vval);
+					if (var) {
+						if (prev)
+							prev->next = var;
+						else
+							vars = var;
+						prev = var;
+					}
+				}
+			}
+		}
+
 		if (*uri) {
 			if (!strcasecmp(buf, "get")) 
-				c = handle_uri(&ser->requestor, uri, &status, &title, &contentlength);
+				c = handle_uri(&ser->requestor, uri, &status, &title, &contentlength, &vars);
 			else 
 				c = ast_http_error(501, "Not Implemented", NULL, "Attempt to use unimplemented / unsupported method");\
 		} else 
 			c = ast_http_error(400, "Bad Request", NULL, "Invalid Request");
+
+		/* If they aren't mopped up already, clean up the cookies */
+		if (vars)
+			ast_variables_destroy(vars);
+
 		if (!c)
 			c = ast_http_error(500, "Internal Error", NULL, "Internal Server Error");
 		if (c) {
 			time(&t);
 			strftime(timebuf, sizeof(timebuf), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&t));
-			ast_cli(ser->fd, "HTTP/1.1 GET %d %s\r\n", status, title ? title : "OK");
+			ast_cli(ser->fd, "HTTP/1.1 %d %s\r\n", status, title ? title : "OK");
 			ast_cli(ser->fd, "Server: Asterisk\r\n");
 			ast_cli(ser->fd, "Date: %s\r\n", timebuf);
-			if (contentlength)
-				ast_cli(ser->fd, "Content-length: %d\r\n", contentlength);
 			ast_cli(ser->fd, "Connection: close\r\n");
-			ast_cli(ser->fd, "%s", c);
+			if (contentlength) {
+				char *tmp;
+				tmp = strstr(c, "\r\n\r\n");
+				if (tmp) {
+					ast_cli(ser->fd, "Content-length: %d\r\n", contentlength);
+					write(ser->fd, c, (tmp + 4 - c));
+					write(ser->fd, tmp + 4, contentlength);
+				}
+			} else
+				ast_cli(ser->fd, "%s", c);
 			free(c);
 		}
 		if (title)
@@ -297,19 +461,22 @@ static void *http_root(void *data)
 				ast_log(LOG_WARNING, "Accept failed: %s\n", strerror(errno));
 			continue;
 		}
-		if (!(ser = ast_calloc(1, sizeof(*ser)))) {
-			close(fd);
-			continue;
-		}
-		ser->fd = fd;
-		if ((ser->f = fdopen(ser->fd, "w+"))) {
-			if (ast_pthread_create(&launched, NULL, ast_httpd_helper_thread, ser)) {
-				ast_log(LOG_WARNING, "Unable to launch helper thread: %s\n", strerror(errno));
-				fclose(ser->f);
+		ser = ast_calloc(1, sizeof(*ser));
+		if (ser) {
+			ser->fd = fd;
+			memcpy(&ser->requestor, &sin, sizeof(ser->requestor));
+			if ((ser->f = fdopen(ser->fd, "w+"))) {
+				if (ast_pthread_create(&launched, NULL, ast_httpd_helper_thread, ser)) {
+					ast_log(LOG_WARNING, "Unable to launch helper thread: %s\n", strerror(errno));
+					fclose(ser->f);
+					free(ser);
+				}
+			} else {
+				ast_log(LOG_WARNING, "fdopen failed!\n");
+				close(ser->fd);
 				free(ser);
 			}
 		} else {
-			ast_log(LOG_WARNING, "fdopen failed!\n");
 			close(ser->fd);
 			free(ser);
 		}
@@ -317,6 +484,18 @@ static void *http_root(void *data)
 	return NULL;
 }
 
+char *ast_http_setcookie(const char *var, const char *val, int expires, char *buf, int buflen)
+{
+	char *c;
+	c = buf;
+	ast_build_string(&c, &buflen, "Set-Cookie: %s=\"%s\"; Version=\"1\"", var, val);
+	if (expires)
+		ast_build_string(&c, &buflen, "; Max-Age=%d", expires);
+	ast_build_string(&c, &buflen, "\r\n");
+	return buf;
+}
+
+
 static void http_server_start(struct sockaddr_in *sin)
 {
 	char iabuf[INET_ADDRSTRLEN];
@@ -383,6 +562,7 @@ static int __ast_http_load(int reload)
 	struct ast_config *cfg;
 	struct ast_variable *v;
 	int enabled=0;
+	int newenablestatic=0;
 	struct sockaddr_in sin;
 	struct hostent *hp;
 	struct ast_hostent ahp;
@@ -396,6 +576,8 @@ static int __ast_http_load(int reload)
 		while(v) {
 			if (!strcasecmp(v->name, "enabled"))
 				enabled = ast_true(v->value);
+			else if (!strcasecmp(v->name, "enablestatic"))
+				newenablestatic = ast_true(v->value);
 			else if (!strcasecmp(v->name, "bindport"))
 				sin.sin_port = ntohs(atoi(v->value));
 			else if (!strcasecmp(v->name, "bindaddr")) {
@@ -416,6 +598,7 @@ static int __ast_http_load(int reload)
 		ast_copy_string(prefix, newprefix, sizeof(prefix));
 		prefix_len = strlen(prefix);
 	}
+	enablestatic = newenablestatic;
 	http_server_start(&sin);
 	return 0;
 }
@@ -462,6 +645,7 @@ static struct ast_cli_entry http_cli[] = {
 int ast_http_init(void)
 {
 	ast_http_uri_link(&statusuri);
+	ast_http_uri_link(&staticuri);
 	ast_cli_register_multiple(http_cli, sizeof(http_cli) / sizeof(http_cli[0]));
 	return __ast_http_load(0);
 }
diff --git a/include/asterisk/http.h b/include/asterisk/http.h
index ea580c3ca8..9156db0c42 100644
--- a/include/asterisk/http.h
+++ b/include/asterisk/http.h
@@ -58,6 +58,8 @@ char *ast_http_error(int status, const char *title, const char *extra_header, co
 /* Destroy an HTTP server */
 void ast_http_uri_unlink(struct ast_http_uri *urihandler);
 
+char *ast_http_setcookie(const char *var, const char *val, int expires, char *buf, int buflen);
+
 int ast_http_init(void);
 int ast_http_reload(void);
 
diff --git a/manager.c b/manager.c
index 16605bd402..b2d4964c59 100644
--- a/manager.c
+++ b/manager.c
@@ -35,6 +35,7 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <ctype.h>
 #include <sys/time.h>
 #include <sys/types.h>
 #include <netdb.h>
@@ -64,6 +65,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 #include "asterisk/md5.h"
 #include "asterisk/acl.h"
 #include "asterisk/utils.h"
+#include "asterisk/http.h"
 
 struct fast_originate_helper {
 	char tech[AST_MAX_MANHEADER_LEN];
@@ -86,6 +88,7 @@ static int portno = DEFAULT_MANAGER_PORT;
 static int asock = -1;
 static int displayconnects = 1;
 static int timestampevents = 0;
+static int httptimeout = 60;
 
 static pthread_t t;
 AST_MUTEX_DEFINE_STATIC(sessionlock);
@@ -119,6 +122,18 @@ static struct mansession {
 	int busy;
 	/*! Whether or not we're "dead" */
 	int dead;
+	/*! Whether an HTTP manager is in use */
+	int inuse;
+	/*! Whether an HTTP session should be destroyed */
+	int needdestroy;
+	/*! Whether an HTTP session has someone waiting on events */
+	pthread_t waiting_thread;
+	/*! Unique manager identifer */
+	unsigned long managerid;
+	/*! Session timeout if HTTP */
+	time_t sessiontimeout;
+	/*! Output from manager interface */
+	char *outputstr;
 	/*! Logged in username */
 	char username[80];
 	/*! Authentication challenge */
@@ -212,11 +227,168 @@ static char *complete_show_mancmd(const char *line, const char *word, int pos, i
 	return ret;
 }
 
+static void xml_copy_escape(char **dst, int *maxlen, const char *src, int lower)
+{
+	while (*src && (*maxlen > 6)) {
+		switch(*src) {
+		case '<':
+			strcpy(*dst, "&lt;");
+			(*dst) += 4;
+			*maxlen -= 4;
+			break;
+		case '>':
+			strcpy(*dst, "&gt;");
+			(*dst) += 4;
+			*maxlen -= 4;
+			break;
+		case '\"':
+			strcpy(*dst, "&quot;");
+			(*dst) += 6;
+			*maxlen -= 6;
+			break;
+		case '\'':
+			strcpy(*dst, "&apos;");
+			(*dst) += 6;
+			*maxlen -= 6;
+			break;
+		case '&':
+			strcpy(*dst, "&amp;");
+			(*dst) += 4;
+			*maxlen -= 4;
+			break;		
+		default:
+			*(*dst)++ = lower ? tolower(*src) : *src;
+			(*maxlen)--;
+		}
+		src++;
+	}
+}
+static char *xml_translate(char *in, struct ast_variable *vars)
+{
+	struct ast_variable *v;
+	char *dest=NULL;
+	char *out, *tmp, *var, *val;
+	char *objtype=NULL;
+	int colons = 0;
+	int breaks = 0;
+	int len;
+	int count = 1;
+	int escaped = 0;
+	int inobj = 0;
+	int x;
+	v = vars;
+	while(v) {
+		if (!dest && !strcasecmp(v->name, "ajaxdest"))
+			dest = v->value;
+		else if (!objtype && !strcasecmp(v->name, "ajaxobjtype")) 
+			objtype = v->value;
+		v = v->next;
+	}
+	if (!dest)
+		dest = "unknown";
+	if (!objtype)
+		objtype = "generic";
+	for (x=0;in[x];x++) {
+		if (in[x] == ':')
+			colons++;
+		else if (in[x] == '\n')
+			breaks++;
+		else if (strchr("&\"<>", in[x]))
+			escaped++;
+	}
+	len = strlen(in) + colons * 5 + breaks * (40 + strlen(dest) + strlen(objtype)) + escaped * 10; /* foo="bar", "<response type=\"object\" id=\"dest\"", "&amp;" */
+	out = malloc(len);
+	if (!out)
+		return 0;
+	tmp = out;
+	while(*in) {
+		var = in;
+		while (*in && (*in >= 32)) in++;
+		if (*in) {
+			if ((count > 3) && inobj) {
+				ast_build_string(&tmp, &len, " /></response>\n");
+				inobj = 0;
+			}
+			count = 0;
+			while (*in && (*in < 32)) {
+				*in = '\0';
+				in++;
+				count++;
+			}
+			val = strchr(var, ':');
+			if (val) {
+				*val = '\0';
+				val++;
+				if (*val == ' ')
+					val++;
+				if (!inobj) {
+					ast_build_string(&tmp, &len, "<response type='object' id='%s'><%s", dest, objtype);
+					inobj = 1;
+				}
+				ast_build_string(&tmp, &len, " ");				
+				xml_copy_escape(&tmp, &len, var, 1);
+				ast_build_string(&tmp, &len, "='");
+				xml_copy_escape(&tmp, &len, val, 0);
+				ast_build_string(&tmp, &len, "'");
+			}
+		}
+	}
+	if (inobj)
+		ast_build_string(&tmp, &len, " /></response>\n");
+	return out;
+}
+
+static char *html_translate(char *in)
+{
+	int x;
+	int colons = 0;
+	int breaks = 0;
+	int len;
+	int count=1;
+	char *tmp, *var, *val, *out;
+	for (x=0;in[x];x++) {
+		if (in[x] == ':')
+			colons++;
+		if (in[x] == '\n')
+			breaks++;
+	}
+	len = strlen(in) + colons * 40 + breaks * 40; /* <tr><td></td><td></td></tr>, "<tr><td colspan=\"2\"><hr></td></tr> */
+	out = malloc(len);
+	if (!out)
+		return 0;
+	tmp = out;
+	while(*in) {
+		var = in;
+		while (*in && (*in >= 32)) in++;
+		if (*in) {
+			if ((count % 4) == 0){
+				ast_build_string(&tmp, &len, "<tr><td colspan=\"2\"><hr></td></tr>\r\n");
+			}
+			count = 0;
+			while (*in && (*in < 32)) {
+				*in = '\0';
+				in++;
+				count++;
+			}
+			val = strchr(var, ':');
+			if (val) {
+				*val = '\0';
+				val++;
+				if (*val == ' ')
+					val++;
+				ast_build_string(&tmp, &len, "<tr><td>%s</td><td>%s</td></tr>\r\n", var, val);
+			}
+		}
+	}
+	return out;
+}
+
 void astman_append(struct mansession *s, const char *fmt, ...)
 {
 	char *stuff;
 	int res;
 	va_list ap;
+	char *tmp;
 
 	va_start(ap, fmt);
 	res = vasprintf(&stuff, fmt, ap);
@@ -224,7 +396,17 @@ void astman_append(struct mansession *s, const char *fmt, ...)
 	if (res == -1) {
 		ast_log(LOG_ERROR, "Memory allocation failure\n");
 	} else {
-		ast_carefulwrite(s->fd, stuff, strlen(stuff), 100);
+		if (s->fd > -1)
+			ast_carefulwrite(s->fd, stuff, strlen(stuff), 100);
+		else {
+			tmp = realloc(s->outputstr, (s->outputstr ? strlen(s->outputstr) : 0) + strlen(stuff) + 1);
+			if (tmp) {
+				if (!s->outputstr)
+					tmp[0] = '\0';
+				s->outputstr = tmp;
+				strcat(s->outputstr, stuff);
+			}
+		}
 		free(stuff);
 	}
 }
@@ -320,6 +502,8 @@ static void free_session(struct mansession *s)
 	struct eventqent *eqe;
 	if (s->fd > -1)
 		close(s->fd);
+	if (s->outputstr)
+		free(s->outputstr);
 	ast_mutex_destroy(&s->__lock);
 	while(s->eventq) {
 		eqe = s->eventq;
@@ -606,7 +790,7 @@ static int authenticate(struct mansession *s, struct message *m)
 							return -1;
 						}
 					}
-				} else if (password && !strcasecmp(password, pass)) {
+				} else if (password && !strcmp(password, pass)) {
 					break;
 				} else {
 					ast_log(LOG_NOTICE, "%s failed to authenticate as '%s'\n", ast_inet_ntoa(iabuf, sizeof(iabuf), s->sin.sin_addr), user);
@@ -633,7 +817,7 @@ static int authenticate(struct mansession *s, struct message *m)
 
 /*! \brief PING: Manager PING */
 static char mandescr_ping[] = 
-"Description: A 'Ping' action will ellicit a 'Pong' response.  Used to keep the "
+"Description: A 'Ping' action will ellicit a 'Pong' response.  Used to keep the\n"
 "  manager connection open.\n"
 "Variables: NONE\n";
 
@@ -643,6 +827,94 @@ static int action_ping(struct mansession *s, struct message *m)
 	return 0;
 }
 
+/*! \brief WAITEVENT: Manager WAITEVENT */
+static char mandescr_waitevent[] = 
+"Description: A 'WaitEvent' action will ellicit a 'Success' response.  Whenever\n"
+"a manager event is queued.  Once WaitEvent has been called on an HTTP manager\n"
+"session, events will be generated and queued.\n"
+"Variables: \n"
+"   Timeout: Maximum time to wait for events\n";
+
+static int action_waitevent(struct mansession *s, struct message *m)
+{
+	char *timeouts = astman_get_header(m, "Timeout");
+	int timeout = -1, max;
+	int x;
+	int needexit = 0;
+	time_t now;
+	struct eventqent *eqe;
+	char *id = astman_get_header(m,"ActionID");
+	char idText[256]="";
+
+	if (!ast_strlen_zero(id))
+		snprintf(idText, sizeof(idText), "ActionID: %s\r\n", id);
+
+	if (!ast_strlen_zero(timeouts)) {
+		sscanf(timeouts, "%i", &timeout);
+	}
+	
+	ast_mutex_lock(&s->__lock);
+	if (s->waiting_thread != AST_PTHREADT_NULL) {
+		pthread_kill(s->waiting_thread, SIGURG);
+	}
+	if (s->sessiontimeout) {
+		time(&now);
+		max = s->sessiontimeout - now - 10;
+		if (max < 0)
+			max = 0;
+		if ((timeout < 0) || (timeout > max))
+			timeout = max;
+		if (!s->send_events)
+			s->send_events = -1;
+		/* Once waitevent is called, always queue events from now on */
+		if (s->busy == 1)
+			s->busy = 2;
+	}
+	ast_mutex_unlock(&s->__lock);
+	s->waiting_thread = pthread_self();
+
+	ast_log(LOG_DEBUG, "Starting waiting for an event!\n");
+	for (x=0;((x<timeout) || (timeout < 0)); x++) {
+		ast_mutex_lock(&s->__lock);
+		if (s->eventq)
+			needexit = 1;
+		if (s->waiting_thread != pthread_self())
+			needexit = 1;
+		if (s->needdestroy)
+			needexit = 1;
+		ast_mutex_unlock(&s->__lock);
+		if (needexit)
+			break;
+		if (s->fd > 0) {
+			if (ast_wait_for_input(s->fd, 1000))
+				break;
+		} else {
+			sleep(1);
+		}
+	}
+	ast_log(LOG_DEBUG, "Finished waiting for an event!\n");
+	ast_mutex_lock(&s->__lock);
+	if (s->waiting_thread == pthread_self()) {
+		astman_send_response(s, m, "Success", "Waiting for Event...");
+		/* Only show events if we're the most recent waiter */
+		while(s->eventq) {
+			astman_append(s, "%s", s->eventq->eventdata);
+			eqe = s->eventq;
+			s->eventq = s->eventq->next;
+			free(eqe);
+		}
+		astman_append(s,
+			"Event: WaitEventComplete\r\n"
+			"%s"
+			"\r\n",idText);
+		s->waiting_thread = AST_PTHREADT_NULL;
+	} else {
+		ast_log(LOG_DEBUG, "Abandoning event request!\n");
+	}
+	ast_mutex_unlock(&s->__lock);
+	return 0;
+}
+
 static char mandescr_listcommands[] = 
 "Description: Returns the action name and synopsis for every\n"
 "  action that is available to the user\n"
@@ -1338,10 +1610,10 @@ static int process_message(struct mansession *s, struct message *m)
 				s->authenticated = 1;
 				if (option_verbose > 1) {
 					if ( displayconnects ) {
-						ast_verbose(VERBOSE_PREFIX_2 "Manager '%s' logged on from %s\n", s->username, ast_inet_ntoa(iabuf, sizeof(iabuf), s->sin.sin_addr));
+						ast_verbose(VERBOSE_PREFIX_2 "%sManager '%s' logged on from %s\n", (s->sessiontimeout ? "HTTP " : ""), s->username, ast_inet_ntoa(iabuf, sizeof(iabuf), s->sin.sin_addr));
 					}
 				}
-				ast_log(LOG_EVENT, "Manager '%s' logged on from %s\n", s->username, ast_inet_ntoa(iabuf, sizeof(iabuf), s->sin.sin_addr));
+				ast_log(LOG_EVENT, "%sManager '%s' logged on from %s\n", (s->sessiontimeout ? "HTTP " : ""), s->username, ast_inet_ntoa(iabuf, sizeof(iabuf), s->sin.sin_addr));
 				astman_send_ack(s, m, "Authentication accepted");
 			}
 		} else if (!strcasecmp(action, "Logoff")) {
@@ -1353,7 +1625,7 @@ static int process_message(struct mansession *s, struct message *m)
 		int ret=0;
 		struct eventqent *eqe;
 		ast_mutex_lock(&s->__lock);
-		s->busy = 1;
+		s->busy++;
 		ast_mutex_unlock(&s->__lock);
 		while( tmp ) { 		
 			if (!strcasecmp(action, tmp->action)) {
@@ -1370,15 +1642,17 @@ static int process_message(struct mansession *s, struct message *m)
 		if (!tmp)
 			astman_send_error(s, m, "Invalid/unknown command");
 		ast_mutex_lock(&s->__lock);
-		s->busy = 0;
-		while(s->eventq) {
-			if (ast_carefulwrite(s->fd, s->eventq->eventdata, strlen(s->eventq->eventdata), s->writetimeout) < 0) {
-				ret = -1;
-				break;
+		if (s->fd > -1) {
+			s->busy--;
+			while(s->eventq) {
+				if (ast_carefulwrite(s->fd, s->eventq->eventdata, strlen(s->eventq->eventdata), s->writetimeout) < 0) {
+					ret = -1;
+					break;
+				}
+				eqe = s->eventq;
+				s->eventq = s->eventq->next;
+				free(eqe);
 			}
-			eqe = s->eventq;
-			s->eventq = s->eventq->next;
-			free(eqe);
 		}
 		ast_mutex_unlock(&s->__lock);
 		return ret;
@@ -1484,17 +1758,48 @@ static void *accept_thread(void *ignore)
 	int as;
 	struct sockaddr_in sin;
 	socklen_t sinlen;
-	struct mansession *s;
+	struct mansession *s, *prev=NULL, *next;
 	struct protoent *p;
 	int arg = 1;
 	int flags;
 	pthread_attr_t attr;
+	time_t now;
+	struct pollfd pfds[1];
+	char iabuf[INET_ADDRSTRLEN];
 
 	pthread_attr_init(&attr);
 	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
 
 	for (;;) {
+		time(&now);
+		ast_mutex_lock(&sessionlock);
+		prev = NULL;
+		s = sessions;
+		while(s) {
+			next = s->next;
+			if (s->sessiontimeout && (now > s->sessiontimeout) && !s->inuse) {
+				if (prev)
+					prev->next = next;
+				else
+					sessions = next;
+				if (s->authenticated && (option_verbose > 1) && displayconnects) {
+					ast_verbose(VERBOSE_PREFIX_2 "HTTP Manager '%s' timed out from %s\n",
+						s->username, ast_inet_ntoa(iabuf, sizeof(iabuf), s->sin.sin_addr));
+				}
+				free_session(s);
+			} else
+				prev = s;
+			s = next;
+		}
+		ast_mutex_unlock(&sessionlock);
+
 		sinlen = sizeof(sin);
+		pfds[0].fd = asock;
+		pfds[0].events = POLLIN;
+		/* Wait for something to happen, but timeout every few seconds so
+		   we can ditch any old manager sessions */
+		if (poll(pfds, 1, 5000) < 1)
+			continue;
 		as = accept(asock, (struct sockaddr *)&sin, &sinlen);
 		if (as < 0) {
 			ast_log(LOG_NOTICE, "Accept returned -1: %s\n", strerror(errno));
@@ -1514,6 +1819,7 @@ static void *accept_thread(void *ignore)
 		memset(s, 0, sizeof(struct mansession));
 		memcpy(&s->sin, &sin, sizeof(sin));
 		s->writetimeout = 100;
+		s->waiting_thread = AST_PTHREADT_NULL;
 
 		if(! block_sockets) {
 			/* For safety, make sure socket is non-blocking */
@@ -1593,7 +1899,9 @@ int manager_event(int category, const char *event, const char *fmt, ...)
 		ast_mutex_lock(&s->__lock);
 		if (s->busy) {
 			append_event(s, tmp);
-		} else if (!s->dead) {
+			if (s->waiting_thread != AST_PTHREADT_NULL)
+				pthread_kill(s->waiting_thread, SIGURG);
+		} else if (!s->dead && !s->sessiontimeout) {
 			if (ast_carefulwrite(s->fd, tmp, tmp_next - tmp, s->writetimeout) < 0) {
 				ast_log(LOG_WARNING, "Disconnecting slow (or gone) manager session!\n");
 				s->dead = 1;
@@ -1701,7 +2009,211 @@ int ast_manager_register2(const char *action, int auth, int (*func)(struct manse
 /*! @}
  END Doxygen group */
 
+static struct mansession *find_session(unsigned long ident)
+{
+	struct mansession *s;
+	ast_mutex_lock(&sessionlock);
+	s = sessions;
+	while(s) {
+		ast_mutex_lock(&s->__lock);
+		if (s->sessiontimeout && (s->managerid == ident) && !s->needdestroy) {
+			s->inuse++;
+			break;
+		}
+		ast_mutex_unlock(&s->__lock);
+		s = s->next;
+	}
+	ast_mutex_unlock(&sessionlock);
+	return s;
+}
+
+
+static void vars2msg(struct message *m, struct ast_variable *vars)
+{
+	int x;
+	for (x=0;vars && (x<AST_MAX_MANHEADERS);x++,vars = vars->next) {
+		if (!vars)
+			break;
+		m->hdrcount = x + 1;
+		snprintf(m->headers[x], sizeof(m->headers[x]), "%s: %s", vars->name, vars->value);
+	}
+}
+
+#define FORMAT_RAW	0
+#define FORMAT_HTML	1
+#define FORMAT_XML	2
+
+static char *contenttype[] = { "plain", "html", "xml" };
+
+static char *generic_http_callback(int format, struct sockaddr_in *requestor, const char *uri, struct ast_variable *params, int *status, char **title, int *contentlength)
+{
+	struct mansession *s=NULL;
+	unsigned long ident=0;
+	char workspace[256];
+	char cookie[128];
+	char iabuf[INET_ADDRSTRLEN];
+	int len = sizeof(workspace);
+	int blastaway = 0;
+	char *c = workspace;
+	char *retval=NULL;
+	struct message m;
+	struct ast_variable *v;
+	
+	v = params;
+	while(v) {
+		if (!strcasecmp(v->name, "mansession_id")) {
+			sscanf(v->value, "%lx", &ident);
+			break;
+		}
+		v = v->next;
+	}
+	s = find_session(ident);
+
+	if (!s) {
+		/* Create new session */
+		s = calloc(1, sizeof(struct mansession));
+		memcpy(&s->sin, requestor, sizeof(s->sin));
+		s->fd = -1;
+		s->waiting_thread = AST_PTHREADT_NULL;
+		s->send_events = 0;
+		ast_mutex_init(&s->__lock);
+		ast_mutex_lock(&s->__lock);
+		ast_mutex_lock(&sessionlock);
+		s->inuse = 1;
+		s->managerid = rand() | (unsigned long)s;
+		s->next = sessions;
+		sessions = s;
+		ast_mutex_unlock(&sessionlock);
+	}
+
+	/* Reset HTTP timeout */
+	time(&s->sessiontimeout);
+	s->sessiontimeout += httptimeout;
+	ast_mutex_unlock(&s->__lock);
+	
+	memset(&m, 0, sizeof(m));
+	if (s) {
+		char tmp[80];
+		ast_build_string(&c, &len, "Content-type: text/%s\n", contenttype[format]);
+		sprintf(tmp, "%08lx", s->managerid);
+		ast_build_string(&c, &len, "%s\r\n", ast_http_setcookie("mansession_id", tmp, httptimeout, cookie, sizeof(cookie)));
+		if (format == FORMAT_HTML)
+			ast_build_string(&c, &len, "<title>Asterisk&trade; Manager Test Interface</title>");
+		vars2msg(&m, params);
+		if (format == FORMAT_XML) {
+			ast_build_string(&c, &len, "<ajax-response>\n");
+		} else if (format == FORMAT_HTML) {
+			ast_build_string(&c, &len, "<body bgcolor=\"#ffffff\"><table align=center bgcolor=\"#f1f1f1\" width=\"500\">\r\n");
+			ast_build_string(&c, &len, "<tr><td colspan=\"2\" bgcolor=\"#f1f1ff\"><h1>&nbsp;&nbsp;Manager Tester</h1></td></tr>\r\n");
+		}
+		if (process_message(s, &m)) {
+			if (s->authenticated) {
+				if (option_verbose > 1) {
+					if (displayconnects) 
+						ast_verbose(VERBOSE_PREFIX_2 "HTTP Manager '%s' logged off from %s\n", s->username, ast_inet_ntoa(iabuf, sizeof(iabuf), s->sin.sin_addr));    
+				}
+				ast_log(LOG_EVENT, "HTTP Manager '%s' logged off from %s\n", s->username, ast_inet_ntoa(iabuf, sizeof(iabuf), s->sin.sin_addr));
+			} else {
+				if (option_verbose > 1) {
+					if (displayconnects)
+						ast_verbose(VERBOSE_PREFIX_2 "HTTP Connect attempt from '%s' unable to authenticate\n", ast_inet_ntoa(iabuf, sizeof(iabuf), s->sin.sin_addr));
+				}
+				ast_log(LOG_EVENT, "HTTP Failed attempt from %s\n", ast_inet_ntoa(iabuf, sizeof(iabuf), s->sin.sin_addr));
+			}
+			s->needdestroy = 1;
+		}
+		if (s->outputstr) {
+			char *tmp;
+			if (format == FORMAT_XML)
+				tmp = xml_translate(s->outputstr, params);
+			else if (format == FORMAT_HTML)
+				tmp = html_translate(s->outputstr);
+			else
+				tmp = s->outputstr;
+			if (tmp) {
+				retval = malloc(strlen(workspace) + strlen(tmp) + 128);
+				if (retval) {
+					strcpy(retval, workspace);
+					strcpy(retval + strlen(retval), tmp);
+					c = retval + strlen(retval);
+					len = 120;
+				}
+				free(tmp);
+			}
+			if (tmp != s->outputstr)
+				free(s->outputstr);
+			s->outputstr = NULL;
+		}
+		/* Still okay because c would safely be pointing to workspace even
+		   if retval failed to allocate above */
+		if (format == FORMAT_XML) {
+			ast_build_string(&c, &len, "</ajax-response>\n");
+		} else if (format == FORMAT_HTML)
+			ast_build_string(&c, &len, "</table></body>\r\n");
+	} else {
+		*status = 500;
+		*title = strdup("Server Error");
+	}
+	ast_mutex_lock(&s->__lock);
+	if (s->needdestroy) {
+		if (s->inuse == 1) {
+			ast_log(LOG_DEBUG, "Need destroy, doing it now!\n");
+			blastaway = 1;
+		} else {
+			ast_log(LOG_DEBUG, "Need destroy, but can't do it yet!\n");
+			if (s->waiting_thread != AST_PTHREADT_NULL)
+				pthread_kill(s->waiting_thread, SIGURG);
+			s->inuse--;
+		}
+	} else
+		s->inuse--;
+	ast_mutex_unlock(&s->__lock);
+	
+	if (blastaway)
+		destroy_session(s);
+	if (*status != 200)
+		return ast_http_error(500, "Server Error", NULL, "Internal Server Error (out of memory)\n"); 
+	return retval;
+}
+
+static char *manager_http_callback(struct sockaddr_in *requestor, const char *uri, struct ast_variable *params, int *status, char **title, int *contentlength)
+{
+	return generic_http_callback(FORMAT_HTML, requestor, uri, params, status, title, contentlength);
+}
+
+static char *mxml_http_callback(struct sockaddr_in *requestor, const char *uri, struct ast_variable *params, int *status, char **title, int *contentlength)
+{
+	return generic_http_callback(FORMAT_XML, requestor, uri, params, status, title, contentlength);
+}
+
+static char *rawman_http_callback(struct sockaddr_in *requestor, const char *uri, struct ast_variable *params, int *status, char **title, int *contentlength)
+{
+	return generic_http_callback(FORMAT_RAW, requestor, uri, params, status, title, contentlength);
+}
+
+struct ast_http_uri rawmanuri = {
+	.description = "Raw HTTP Manager Event Interface",
+	.uri = "rawman",
+	.has_subtree = 0,
+	.callback = rawman_http_callback,
+};
+
+struct ast_http_uri manageruri = {
+	.description = "HTML Manager Event Interface",
+	.uri = "manager",
+	.has_subtree = 0,
+	.callback = manager_http_callback,
+};
+
+struct ast_http_uri managerxmluri = {
+	.description = "XML Manager Event Interface",
+	.uri = "mxml",
+	.has_subtree = 0,
+	.callback = mxml_http_callback,
+};
+
 static int registered = 0;
+static int webregged = 0;
 
 int init_manager(void)
 {
@@ -1710,6 +2222,9 @@ int init_manager(void)
 	int oldportno = portno;
 	static struct sockaddr_in ba;
 	int x = 1;
+	int flags;
+	int webenabled=0;
+	int newhttptimeout = 60;
 	if (!registered) {
 		/* Register default actions */
 		ast_manager_register2("Ping", 0, action_ping, "Keepalive command", mandescr_ping);
@@ -1727,6 +2242,7 @@ int init_manager(void)
 		ast_manager_register2("MailboxStatus", EVENT_FLAG_CALL, action_mailboxstatus, "Check Mailbox", mandescr_mailboxstatus );
 		ast_manager_register2("MailboxCount", EVENT_FLAG_CALL, action_mailboxcount, "Check Mailbox Message Count", mandescr_mailboxcount );
 		ast_manager_register2("ListCommands", 0, action_listcommands, "List available manager commands", mandescr_listcommands);
+		ast_manager_register2("WaitEvent", 0, action_waitevent, "Wait for an event to occur", mandescr_waitevent);
 
 		ast_cli_register(&show_mancmd_cli);
 		ast_cli_register(&show_mancmds_cli);
@@ -1750,6 +2266,10 @@ int init_manager(void)
 	if(val)
 		block_sockets = ast_true(val);
 
+	val = ast_variable_retrieve(cfg, "general", "webenabled");
+	if (val)
+		webenabled = ast_true(val);
+
 	if ((val = ast_variable_retrieve(cfg, "general", "port"))) {
 		if (sscanf(val, "%d", &portno) != 1) {
 			ast_log(LOG_WARNING, "Invalid port number '%s'\n", val);
@@ -1762,6 +2282,9 @@ int init_manager(void)
 
 	if ((val = ast_variable_retrieve(cfg, "general", "timestampevents")))
 		timestampevents = ast_true(val);
+
+	if ((val = ast_variable_retrieve(cfg, "general", "httptimeout")))
+		newhttptimeout = atoi(val);
 	
 	ba.sin_family = AF_INET;
 	ba.sin_port = htons(portno);
@@ -1785,6 +2308,25 @@ int init_manager(void)
 	}
 	ast_config_destroy(cfg);
 	
+	if (webenabled && enabled) {
+		if (!webregged) {
+			ast_http_uri_link(&rawmanuri);
+			ast_http_uri_link(&manageruri);
+			ast_http_uri_link(&managerxmluri);
+			webregged = 1;
+		}
+	} else {
+		if (webregged) {
+			ast_http_uri_unlink(&rawmanuri);
+			ast_http_uri_unlink(&manageruri);
+			ast_http_uri_unlink(&managerxmluri);
+			webregged = 0;
+		}
+	}
+
+	if (newhttptimeout > 0)
+		httptimeout = newhttptimeout;
+	
 	/* If not enabled, do nothing */
 	if (!enabled) {
 		return 0;
@@ -1808,6 +2350,8 @@ int init_manager(void)
 			asock = -1;
 			return -1;
 		}
+		flags = fcntl(asock, F_GETFL);
+		fcntl(asock, F_SETFL, flags | O_NONBLOCK);
 		if (option_verbose)
 			ast_verbose("Asterisk Management interface listening on port %d\n", portno);
 		ast_pthread_create(&t, NULL, accept_thread, NULL);
diff --git a/static-http/ajamdemo.html b/static-http/ajamdemo.html
new file mode 100644
index 0000000000..687b590442
--- /dev/null
+++ b/static-http/ajamdemo.html
@@ -0,0 +1,215 @@
+<script src="prototype.js"></script>
+<script src="astman.js"></script>
+<link href="astman.css" media="all" rel="Stylesheet" type="text/css" />
+
+<script>
+	var logins = new Object;
+	var logoffs = new Object;
+	var channels = new Object;
+	var pongs = new Object;
+	var loggedon = 0;
+	var selectedchan = null;
+	var hungupchan = "";
+	var transferedchan = "";
+	
+	var demo = new Object;
+	
+	function loggedOn() {
+		if (loggedon)
+			return;
+		loggedon = 1;
+		updateButtons();
+		$('statusbar').innerHTML = "<i>Retrieving channel status...</i>";
+		astmanEngine.pollEvents();
+		astmanEngine.sendRequest('action=status', demo.channels);
+	}
+	
+	function clearChannelList() {
+		$('channellist').innerHTML = "<i class='light'>Not connected</i>";
+	}
+
+	function loggedOff() {
+		if (!loggedon)
+			return;
+		loggedon = 0;
+		selectedchan = null;
+		updateButtons();
+		astmanEngine.channelClear();
+	 	clearChannelList();
+	}
+	
+	function updateButtons()
+	{
+		if ($(selectedchan)) {
+			$('transfer').disabled = 0;
+			$('hangup').disabled = 0;
+		} else {
+			$('transfer').disabled = 1;
+			$('hangup').disabled = 1;
+			selectedchan = null;
+		}
+		if (loggedon) {
+			$('logoff').disabled = 0;
+			$('login').disabled = 1;
+			$('refresh').disabled = 0;
+		} else {
+			$('logoff').disabled = 1;
+			$('login').disabled = 0;
+			$('refresh').disabled = 1;
+		}
+	}
+	
+	demo.channelCallback = function(target) {
+		selectedchan = target;
+		updateButtons();
+	}
+	
+	demo.channels = function(msgs) {
+		resp = msgs[0].headers['response'];
+		if (resp == "Success") {
+			loggedOn();
+		} else
+			loggedOff();
+
+		for (i=1;i<msgs.length - 1;i++) 
+			astmanEngine.channelUpdate(msgs[i]);
+		$('channellist').innerHTML = astmanEngine.channelTable(demo.channelCallback);
+		$('statusbar').innerHTML = "Ready";
+	}
+
+	demo.logins = function(msgs) {
+		$('statusbar').innerHTML = msgs[0].headers['message'];
+		resp = msgs[0].headers['response'];
+		if (resp == "Success")
+			loggedOn();
+		else
+			loggedOff();
+	};
+	
+	
+	demo.logoffs = function(msgs) {
+		$('statusbar').innerHTML = msgs[0].headers['message'];
+		loggedOff();
+	};
+
+	demo.hungup = function(msgs) {
+		$('statusbar').innerHTML = "Hungup " + hungupchan;
+	}
+	
+	demo.transferred = function(msgs) {
+		$('statusbar').innerHTML = "Transferred " + transferredchan;
+	}
+
+	function doHangup() {
+		hungupchan = selectedchan;
+		astmanEngine.sendRequest('action=hangup&channel=' + selectedchan, demo.hungup);
+	}
+
+	function doStatus() {
+		$('statusbar').innerHTML = "<i>Updating channel status...</i>";
+		astmanEngine.channelClear();
+		astmanEngine.sendRequest('action=status', demo.channels);
+	}	
+		
+	function doLogin() {
+		$('statusbar').innerHTML = "<i>Logging in...</i>";
+		astmanEngine.sendRequest('action=login&username=' + $('username').value + "&secret=" + $('secret').value, demo.logins);
+	}
+	
+	function doTransfer() {
+		var channel = astmanEngine.channelInfo(selectedchan);
+		var exten = prompt("Enter new extension for " + selectedchan);
+		var altchan;
+		if (exten) {
+			if (channel.link) {
+				if (confirm("Transfer " + channel.link + " too?"))
+					altchan = channel.link;
+			}
+			if (altchan) {
+				transferredchan = selectedchan + " and " + altchan + " to " + exten;
+				astmanEngine.sendRequest('action=redirect&channel=' + selectedchan + "&priority=1&extrachannel=" + altchan + "&exten=" + exten, demo.transferred);
+			} else {
+				transferredchan = selectedchan + " to " + exten;
+				astmanEngine.sendRequest('action=redirect&channel=' + selectedchan + "&priority=1&exten=" + exten, demo.transferred);
+			}
+		}
+	}
+	
+	function doLogoff() {
+		$('statusbar').innerHTML = "<i>Logging off...</i>";
+		astmanEngine.sendRequest('action=logoff', demo.logoffs);
+	}
+	
+	demo.pongs  = function(msgs) {
+		resp = msgs[0].headers['response'];
+		if (resp == "Pong") {
+			$('statusbar').innerHTML = "<i>Already connected...</i>";
+			loggedOn();
+		} else {
+			$('statusbar').innerHTML = "<i>Please login...</i>";
+			loggedOff();
+		}
+	}
+	
+	demo.eventcb = function(msgs) {
+		var x;
+		if (loggedon) {
+			for (i=1;i<msgs.length - 1;i++) {
+				astmanEngine.channelUpdate(msgs[i]);
+			}
+			$('channellist').innerHTML = astmanEngine.channelTable(demo.channelCallback);
+			astmanEngine.pollEvents();
+		}
+		updateButtons();
+	}
+	
+	function localajaminit() {
+		astmanEngine.setURL('../rawman');
+		astmanEngine.setEventCallback(demo.eventcb);
+		//astmanEngine.setDebug($('ditto'));
+		clearChannelList();
+		astmanEngine.sendRequest('action=ping', demo.pongs);
+	}
+</script>
+
+<title>Asterisk&trade; AJAM Demo</title>
+<body onload="localajaminit()">
+<table align="center" width=600>
+<tr valign="top"><td>
+<table align="left">
+<tr><td colspan="2"><h2>Asterisk&trade; AJAM Demo</h2></td>
+<tr><td>Username:</td><td><input id="username"></td></tr>
+<tr><td>Secret:</td><td><input type="password" id="secret"></td></tr>
+	<tr><td colspan=2 align="center">
+	  <div id="statusbar">
+		<span style="margin-left: 4px;font-weight:bold">&nbsp;</span>
+	  </div>
+	</td></tr>
+
+	<tr><td><input type="submit" id="login" value="Login" onClick="doLogin()"></td>
+	<td><input type="submit" id="logoff" value="Logoff" disabled=1 onClick="doLogoff()"></td></tr>
+</table>
+</td><td valign='bottom'>
+<table>
+<div style="margin-left:10;margin-right:50;margin-top:10;margin-bottom:20">
+<i>This is a demo of the Asynchronous Javascript Asterisk Manager interface.  You can login with a
+valid, appropriately permissioned manager username and secret.</i>
+</div>
+<tr>
+	<td><input type="submit" onClick="doStatus()" id="refresh" value="Refresh"></td>
+	<td><input type="submit" onClick="doTransfer()" id="transfer" value="Transfer..."></td>
+	<td><input type="submit" onClick="doHangup()" id="hangup" value="Hangup"></td>
+</tr>
+</table>
+</td></tr>
+<tr><td colspan=2>
+		<div id="channellist" class="chanlist">
+		</div>
+	</td></tr>
+<tr><td align="center" colspan=2>
+	<font size=-1><i>
+		Copyright (C) 2006 Digium, Inc.  Asterisk and Digium are trademarks of Digium, Inc.
+	</i></font>
+</td></tr>
+</table>
+</body>
diff --git a/static-http/astman.css b/static-http/astman.css
new file mode 100644
index 0000000000..fbf2b2cf9c
--- /dev/null
+++ b/static-http/astman.css
@@ -0,0 +1,34 @@
+.chanlist {
+	border           : 1px solid #1f669b;
+	height			: 150px;
+	overflow		: auto;
+	background-color : #f1f1f1;
+	width			: 600;
+}
+
+.chantable {
+	border           : 0px;
+	background-color : #f1f1f1;
+	width			: 100%;
+}
+
+.labels {
+	background-color : #000000;
+	color : #ffffff;
+}
+
+.chanlisteven {
+	background-color : #fff8e4;
+}
+
+.chanlistodd {
+	background-color : #f0f5ff;
+}	
+
+.chanlistselected {
+	background-color : #ffb13d;
+}
+
+.light {
+	color : #717171;
+}
diff --git a/static-http/astman.js b/static-http/astman.js
new file mode 100644
index 0000000000..1a6927263c
--- /dev/null
+++ b/static-http/astman.js
@@ -0,0 +1,256 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Javascript routines or accessing manager routines over HTTP.
+ *
+ * Copyright (C) 1999 - 2006, Digium, Inc.
+ *
+ * Mark Spencer <markster@digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ *
+ */
+
+
+function Astman() {
+	var me = this;
+	var channels = new Array;
+	var lastselect;
+	var selecttarget;
+	this.setURL = function(url) {
+		this.url = url;
+	};
+	this.setEventCallback = function(callback) {
+		this.eventcallback = callback;
+	};
+	this.setDebug = function(debug) {
+		this.debug = debug;
+	};
+	this.clickChannel = function(ev) {
+		var target = ev.target;
+		// XXX This is icky, we statically use astmanEngine to call the callback XXX 
+		if (me.selecttarget)
+			me.restoreTarget(me.selecttarget);
+		while(!target.id || !target.id.length)
+			target=target.parentNode;
+		me.selecttarget = target.id;
+		target.className = "chanlistselected";
+		me.chancallback(target.id);
+	};
+	this.restoreTarget = function(targetname) {
+		var other;
+		target = $(targetname);
+		if (!target)
+			return;
+		if (target.previousSibling) {
+			other = target.previousSibling.previousSibling.className;
+		} else if (target.nextSibling) {
+			other = target.nextSibling.nextSibling.className;
+		}
+		if (other) {
+			if (other == "chanlisteven") 
+				target.className = "chanlistodd";
+			else
+				target.className = "chanlisteven";
+		} else
+				target.className = "chanlistodd";
+	};
+	this.channelUpdate = function(msg, channame) {
+		var fields = new Array("callerid", "calleridname", "context", "extension", "priority", "account", "state", "link", "uniqueid" );
+
+		if (!channame || !channame.length)
+			channame = msg.headers['channel'];
+
+		if (!channels[channame])
+			channels[channame] = new Array();
+			
+		if (msg.headers.event) {
+			if (msg.headers.event == "Hangup") {
+				delete channels[channame];
+			} else if (msg.headers.event == "Link") {
+				var chan1 = msg.headers.channel1;
+				var chan2 = msg.headers.channel2;
+				if (chan1 && channels[chan1])
+					channels[chan1].link = chan2;
+				if (chan2 && channels[chan2])
+					channels[chan2].link = chan1;
+			} else if (msg.headers.event == "Unlink") {
+				var chan1 = msg.headers.channel1;
+				var chan2 = msg.headers.channel2;
+				if (chan1 && channels[chan1])
+					delete channels[chan1].link;
+				if (chan2 && channels[chan2])
+					delete channels[chan2].link;
+			} else if (msg.headers.event == "Rename") {
+				var oldname = msg.headers.oldname;
+				var newname = msg.headers.newname;
+				if (oldname && channels[oldname]) {
+					channels[newname] = channels[oldname];
+					delete channels[oldname];
+				}
+			} else {
+				channels[channame]['channel'] = channame;
+				for (x=0;x<fields.length;x++) {
+					if (msg.headers[fields[x]])
+						channels[channame][fields[x]] = msg.headers[fields[x]];
+				}
+			}
+		} else {
+			channels[channame]['channel'] = channame;
+			for (x=0;x<fields.length;x++) {
+				if (msg.headers[fields[x]])
+					channels[channame][fields[x]] = msg.headers[fields[x]];
+			}
+		}
+	};
+	this.channelClear = function() {
+		channels = new Array;
+	}
+	this.channelInfo = function(channame) {
+		return channels[channame];
+	};
+	this.channelTable = function(callback) {
+		var s, x;
+		var cclass, count=0;
+		var found = 0;
+		var fieldlist = new Array("channel", "callerid", "calleridname", "context", "extension", "priority");
+
+		me.chancallback = callback;
+		s = "<table class='chantable' align='center'>\n";
+		s = s + "\t<tr class='labels' id='labels'><td>Channel</td><td>State</td><td>Caller</td><td>Location</td><td>Link</td></tr>";
+		count=0;
+		for (x in channels) {
+			if (channels[x].channel) {
+				if (count % 2)
+					cclass = "chanlistodd";
+				else
+					cclass = "chanlisteven";
+				if (me.selecttarget && (me.selecttarget == x))
+					cclass = "chanlistselected";
+				count++;
+				s = s + "\t<tr class='" + cclass + "' id='" + channels[x].channel + "' onClick='astmanEngine.clickChannel(event)'>";
+				s = s + "<td>" + channels[x].channel + "</td>";
+				if (channels[x].state)
+					s = s + "<td>" + channels[x].state + "</td>";
+				else
+					s = s + "<td><i class='light'>unknown</i></td>";
+				if (channels[x].calleridname && channels[x].callerid && channels[x].calleridname != "<unknown>") {
+					cid = channels[x].calleridname.escapeHTML() + " &lt;" + channels[x].callerid.escapeHTML() + "&gt;";
+				} else if (channels[x].calleridname && (channels[x].calleridname != "<unknown>")) {
+					cid = channels[x].calleridname.escapeHTML();
+				} else if (channels[x].callerid) {
+					cid = channels[x].callerid.escapeHTML();
+				} else {
+					cid = "<i class='light'>Unknown</i>";
+				}
+				s = s + "<td>" + cid + "</td>";
+				if (channels[x].extension) {
+					s = s + "<td>" + channels[x].extension + "@" + channels[x].context + ":" + channels[x].priority + "</td>";
+				} else {
+					s = s + "<td><i class='light'>None</i></td>";
+				}
+				if (channels[x].link) {
+					s = s + "<td>" + channels[x].link + "</td>";
+				} else {
+					s = s + "<td><i class='light'>None</i></td>";
+				}
+				s = s + "</tr>\n";
+				found++;
+			}
+		}
+		if (!found)
+			s += "<tr><td colspan=" + fieldlist.length + "><i class='light'>No active channels</i></td>\n";
+		s += "</table>\n";
+		return s;
+	};
+	this.parseResponse = function(t, callback) {
+		var msgs = new Array();
+		var inmsg = 0;
+		var msgnum = 0;
+		var x,y;
+		var s = t.responseText;
+		var allheaders = s.split('\r\n');
+		if (me.debug) 
+			me.debug.value = "\n";
+		for (x=0;x<allheaders.length;x++) {
+			if (allheaders[x].length) {
+				var fields = allheaders[x].split(': ');
+				if (!inmsg) {
+					msgs[msgnum] = new Object();
+					msgs[msgnum].headers = new Array();
+					msgs[msgnum].names = new Array();
+					y=0;
+				}
+				msgs[msgnum].headers[fields[0].toLowerCase()] = fields[1];
+				msgs[msgnum].names[y++] = fields[0].toLowerCase();
+				if (me.debug)
+					me.debug.value = me.debug.value + "field " + fields[0] + "/" + fields[1] + "\n";
+				inmsg=1;
+			} else {
+				if (inmsg) {
+					if (me.debug)
+						me.debug.value = me.debug.value + " new message\n";
+					inmsg = 0;
+					msgnum++;
+				}
+			}
+		}
+		if (me.debug) {
+			me.debug.value = me.debug.value + "msgnum is " + msgnum + " and array length is " + msgs.length + "\n";
+			me.debug.value = me.debug.value + "length is " + msgs.length + "\n";
+			var x, y;
+			for (x=0;x<msgs.length;x++) {
+				for (y=0;y<msgs[x].names.length;y++)  {
+					me.debug.value = me.debug.value + "msg "+ (x + 1) + "/" + msgs[x].names[y] + "/" + msgs[x].headers[msgs[x].names[y]] + "\n";
+				}
+			}
+		}
+		callback(msgs);
+	};
+	this.managerResponse = function(t) {
+		me.parseResponse(t, me.callback);
+	};
+	this.doEvents = function(msgs) {
+		me.eventcallback(msgs);
+	};
+	this.eventResponse = function(t) {
+		me.parseResponse(t, me.doEvents);
+	};
+	this.sendRequest = function(request, callback) {
+		var tmp;
+		var opt = {
+			method: 'get',
+			asynchronous: true,
+			onSuccess: this.managerResponse,
+			onFailure: function(t) {
+				alert("Error: " + t.status + ": " + t.statusText);
+			},
+		};
+		me.callback = callback;
+		opt.parameters = request;
+		tmp = new Ajax.Request(this.url, opt);
+	};
+	this.pollEvents = function() {
+		var tmp;
+		var opt = {
+			method: 'get',
+			asynchronous: true,
+			onSuccess: this.eventResponse,
+			onFailure: function(t) {
+				alert("Event Error: " + t.status + ": " + t.statusText);
+			},
+		};
+		opt.parameters="action=waitevent";
+		tmp = new Ajax.Request(this.url, opt);
+	};
+};
+
+astmanEngine = new Astman();
diff --git a/static-http/prototype.js b/static-http/prototype.js
new file mode 100644
index 0000000000..0e85338bab
--- /dev/null
+++ b/static-http/prototype.js
@@ -0,0 +1,1781 @@
+/*  Prototype JavaScript framework, version 1.4.0
+ *  (c) 2005 Sam Stephenson <sam@conio.net>
+ *
+ *  Prototype is freely distributable under the terms of an MIT-style license.
+ *  For details, see the Prototype web site: http://prototype.conio.net/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+  Version: '1.4.0',
+  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
+
+  emptyFunction: function() {},
+  K: function(x) {return x}
+}
+
+var Class = {
+  create: function() {
+    return function() {
+      this.initialize.apply(this, arguments);
+    }
+  }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+  for (property in source) {
+    destination[property] = source[property];
+  }
+  return destination;
+}
+
+Object.inspect = function(object) {
+  try {
+    if (object == undefined) return 'undefined';
+    if (object == null) return 'null';
+    return object.inspect ? object.inspect() : object.toString();
+  } catch (e) {
+    if (e instanceof RangeError) return '...';
+    throw e;
+  }
+}
+
+Function.prototype.bind = function() {
+  var __method = this, args = $A(arguments), object = args.shift();
+  return function() {
+    return __method.apply(object, args.concat($A(arguments)));
+  }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+  var __method = this;
+  return function(event) {
+    return __method.call(object, event || window.event);
+  }
+}
+
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    var digits = this.toString(16);
+    if (this < 16) return '0' + digits;
+    return digits;
+  },
+
+  succ: function() {
+    return this + 1;
+  },
+
+  times: function(iterator) {
+    $R(0, this, true).each(iterator);
+    return this;
+  }
+});
+
+var Try = {
+  these: function() {
+    var returnValue;
+
+    for (var i = 0; i < arguments.length; i++) {
+      var lambda = arguments[i];
+      try {
+        returnValue = lambda();
+        break;
+      } catch (e) {}
+    }
+
+    return returnValue;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create();
+PeriodicalExecuter.prototype = {
+  initialize: function(callback, frequency) {
+    this.callback = callback;
+    this.frequency = frequency;
+    this.currentlyExecuting = false;
+
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    if (!this.currentlyExecuting) {
+      try {
+        this.currentlyExecuting = true;
+        this.callback();
+      } finally {
+        this.currentlyExecuting = false;
+      }
+    }
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+function $() {
+  var elements = new Array();
+
+  for (var i = 0; i < arguments.length; i++) {
+    var element = arguments[i];
+    if (typeof element == 'string')
+      element = document.getElementById(element);
+
+    if (arguments.length == 1)
+      return element;
+
+    elements.push(element);
+  }
+
+  return elements;
+}
+Object.extend(String.prototype, {
+  stripTags: function() {
+    return this.replace(/<\/?[^>]+>/gi, '');
+  },
+
+  stripScripts: function() {
+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+  },
+
+  extractScripts: function() {
+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+    return (this.match(matchAll) || []).map(function(scriptTag) {
+      return (scriptTag.match(matchOne) || ['', ''])[1];
+    });
+  },
+
+  evalScripts: function() {
+    return this.extractScripts().map(eval);
+  },
+
+  escapeHTML: function() {
+    var div = document.createElement('div');
+    var text = document.createTextNode(this);
+    div.appendChild(text);
+    return div.innerHTML;
+  },
+
+  unescapeHTML: function() {
+    var div = document.createElement('div');
+    div.innerHTML = this.stripTags();
+    return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
+  },
+
+  toQueryParams: function() {
+    var pairs = this.match(/^\??(.*)$/)[1].split('&');
+    return pairs.inject({}, function(params, pairString) {
+      var pair = pairString.split('=');
+      params[pair[0]] = pair[1];
+      return params;
+    });
+  },
+
+  toArray: function() {
+    return this.split('');
+  },
+
+  camelize: function() {
+    var oStringList = this.split('-');
+    if (oStringList.length == 1) return oStringList[0];
+
+    var camelizedString = this.indexOf('-') == 0
+      ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
+      : oStringList[0];
+
+    for (var i = 1, len = oStringList.length; i < len; i++) {
+      var s = oStringList[i];
+      camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
+    }
+
+    return camelizedString;
+  },
+
+  inspect: function() {
+    return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'";
+  }
+});
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+var $break    = new Object();
+var $continue = new Object();
+
+var Enumerable = {
+  each: function(iterator) {
+    var index = 0;
+    try {
+      this._each(function(value) {
+        try {
+          iterator(value, index++);
+        } catch (e) {
+          if (e != $continue) throw e;
+        }
+      });
+    } catch (e) {
+      if (e != $break) throw e;
+    }
+  },
+
+  all: function(iterator) {
+    var result = true;
+    this.each(function(value, index) {
+      result = result && !!(iterator || Prototype.K)(value, index);
+      if (!result) throw $break;
+    });
+    return result;
+  },
+
+  any: function(iterator) {
+    var result = true;
+    this.each(function(value, index) {
+      if (result = !!(iterator || Prototype.K)(value, index))
+        throw $break;
+    });
+    return result;
+  },
+
+  collect: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push(iterator(value, index));
+    });
+    return results;
+  },
+
+  detect: function (iterator) {
+    var result;
+    this.each(function(value, index) {
+      if (iterator(value, index)) {
+        result = value;
+        throw $break;
+      }
+    });
+    return result;
+  },
+
+  findAll: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  grep: function(pattern, iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      var stringValue = value.toString();
+      if (stringValue.match(pattern))
+        results.push((iterator || Prototype.K)(value, index));
+    })
+    return results;
+  },
+
+  include: function(object) {
+    var found = false;
+    this.each(function(value) {
+      if (value == object) {
+        found = true;
+        throw $break;
+      }
+    });
+    return found;
+  },
+
+  inject: function(memo, iterator) {
+    this.each(function(value, index) {
+      memo = iterator(memo, value, index);
+    });
+    return memo;
+  },
+
+  invoke: function(method) {
+    var args = $A(arguments).slice(1);
+    return this.collect(function(value) {
+      return value[method].apply(value, args);
+    });
+  },
+
+  max: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (value >= (result || value))
+        result = value;
+    });
+    return result;
+  },
+
+  min: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (value <= (result || value))
+        result = value;
+    });
+    return result;
+  },
+
+  partition: function(iterator) {
+    var trues = [], falses = [];
+    this.each(function(value, index) {
+      ((iterator || Prototype.K)(value, index) ?
+        trues : falses).push(value);
+    });
+    return [trues, falses];
+  },
+
+  pluck: function(property) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push(value[property]);
+    });
+    return results;
+  },
+
+  reject: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (!iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  sortBy: function(iterator) {
+    return this.collect(function(value, index) {
+      return {value: value, criteria: iterator(value, index)};
+    }).sort(function(left, right) {
+      var a = left.criteria, b = right.criteria;
+      return a < b ? -1 : a > b ? 1 : 0;
+    }).pluck('value');
+  },
+
+  toArray: function() {
+    return this.collect(Prototype.K);
+  },
+
+  zip: function() {
+    var iterator = Prototype.K, args = $A(arguments);
+    if (typeof args.last() == 'function')
+      iterator = args.pop();
+
+    var collections = [this].concat(args).map($A);
+    return this.map(function(value, index) {
+      iterator(value = collections.pluck(index));
+      return value;
+    });
+  },
+
+  inspect: function() {
+    return '#<Enumerable:' + this.toArray().inspect() + '>';
+  }
+}
+
+Object.extend(Enumerable, {
+  map:     Enumerable.collect,
+  find:    Enumerable.detect,
+  select:  Enumerable.findAll,
+  member:  Enumerable.include,
+  entries: Enumerable.toArray
+});
+var $A = Array.from = function(iterable) {
+  if (!iterable) return [];
+  if (iterable.toArray) {
+    return iterable.toArray();
+  } else {
+    var results = [];
+    for (var i = 0; i < iterable.length; i++)
+      results.push(iterable[i]);
+    return results;
+  }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+  _each: function(iterator) {
+    for (var i = 0; i < this.length; i++)
+      iterator(this[i]);
+  },
+
+  clear: function() {
+    this.length = 0;
+    return this;
+  },
+
+  first: function() {
+    return this[0];
+  },
+
+  last: function() {
+    return this[this.length - 1];
+  },
+
+  compact: function() {
+    return this.select(function(value) {
+      return value != undefined || value != null;
+    });
+  },
+
+  flatten: function() {
+    return this.inject([], function(array, value) {
+      return array.concat(value.constructor == Array ?
+        value.flatten() : [value]);
+    });
+  },
+
+  without: function() {
+    var values = $A(arguments);
+    return this.select(function(value) {
+      return !values.include(value);
+    });
+  },
+
+  indexOf: function(object) {
+    for (var i = 0; i < this.length; i++)
+      if (this[i] == object) return i;
+    return -1;
+  },
+
+  reverse: function(inline) {
+    return (inline !== false ? this : this.toArray())._reverse();
+  },
+
+  shift: function() {
+    var result = this[0];
+    for (var i = 0; i < this.length - 1; i++)
+      this[i] = this[i + 1];
+    this.length--;
+    return result;
+  },
+
+  inspect: function() {
+    return '[' + this.map(Object.inspect).join(', ') + ']';
+  }
+});
+var Hash = {
+  _each: function(iterator) {
+    for (key in this) {
+      var value = this[key];
+      if (typeof value == 'function') continue;
+
+      var pair = [key, value];
+      pair.key = key;
+      pair.value = value;
+      iterator(pair);
+    }
+  },
+
+  keys: function() {
+    return this.pluck('key');
+  },
+
+  values: function() {
+    return this.pluck('value');
+  },
+
+  merge: function(hash) {
+    return $H(hash).inject($H(this), function(mergedHash, pair) {
+      mergedHash[pair.key] = pair.value;
+      return mergedHash;
+    });
+  },
+
+  toQueryString: function() {
+    return this.map(function(pair) {
+      return pair.map(encodeURIComponent).join('=');
+    }).join('&');
+  },
+
+  inspect: function() {
+    return '#<Hash:{' + this.map(function(pair) {
+      return pair.map(Object.inspect).join(': ');
+    }).join(', ') + '}>';
+  }
+}
+
+function $H(object) {
+  var hash = Object.extend({}, object || {});
+  Object.extend(hash, Enumerable);
+  Object.extend(hash, Hash);
+  return hash;
+}
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
+  initialize: function(start, end, exclusive) {
+    this.start = start;
+    this.end = end;
+    this.exclusive = exclusive;
+  },
+
+  _each: function(iterator) {
+    var value = this.start;
+    do {
+      iterator(value);
+      value = value.succ();
+    } while (this.include(value));
+  },
+
+  include: function(value) {
+    if (value < this.start)
+      return false;
+    if (this.exclusive)
+      return value < this.end;
+    return value <= this.end;
+  }
+});
+
+var $R = function(start, end, exclusive) {
+  return new ObjectRange(start, end, exclusive);
+}
+
+var Ajax = {
+  getTransport: function() {
+    return Try.these(
+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')},
+      function() {return new XMLHttpRequest()}
+    ) || false;
+  },
+
+  activeRequestCount: 0
+}
+
+Ajax.Responders = {
+  responders: [],
+
+  _each: function(iterator) {
+    this.responders._each(iterator);
+  },
+
+  register: function(responderToAdd) {
+    if (!this.include(responderToAdd))
+      this.responders.push(responderToAdd);
+  },
+
+  unregister: function(responderToRemove) {
+    this.responders = this.responders.without(responderToRemove);
+  },
+
+  dispatch: function(callback, request, transport, json) {
+    this.each(function(responder) {
+      if (responder[callback] && typeof responder[callback] == 'function') {
+        try {
+          responder[callback].apply(responder, [request, transport, json]);
+        } catch (e) {}
+      }
+    });
+  }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+  onCreate: function() {
+    Ajax.activeRequestCount++;
+  },
+
+  onComplete: function() {
+    Ajax.activeRequestCount--;
+  }
+});
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+  setOptions: function(options) {
+    this.options = {
+      method:       'post',
+      asynchronous: true,
+      parameters:   ''
+    }
+    Object.extend(this.options, options || {});
+  },
+
+  responseIsSuccess: function() {
+    return this.transport.status == undefined
+        || this.transport.status == 0
+        || (this.transport.status >= 200 && this.transport.status < 300);
+  },
+
+  responseIsFailure: function() {
+    return !this.responseIsSuccess();
+  }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+  initialize: function(url, options) {
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+    this.request(url);
+  },
+
+  request: function(url) {
+    var parameters = this.options.parameters || '';
+    if (parameters.length > 0) parameters += '&_=';
+
+    try {
+      this.url = url;
+      if (this.options.method == 'get' && parameters.length > 0)
+        this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;
+
+      Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+      this.transport.open(this.options.method, this.url,
+        this.options.asynchronous);
+
+      if (this.options.asynchronous) {
+        this.transport.onreadystatechange = this.onStateChange.bind(this);
+        setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);
+      }
+
+      this.setRequestHeaders();
+
+      var body = this.options.postBody ? this.options.postBody : parameters;
+      this.transport.send(this.options.method == 'post' ? body : null);
+
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  setRequestHeaders: function() {
+    var requestHeaders =
+      ['X-Requested-With', 'XMLHttpRequest',
+       'X-Prototype-Version', Prototype.Version];
+
+    if (this.options.method == 'post') {
+      requestHeaders.push('Content-type',
+        'application/x-www-form-urlencoded');
+
+      /* Force "Connection: close" for Mozilla browsers to work around
+       * a bug where XMLHttpReqeuest sends an incorrect Content-length
+       * header. See Mozilla Bugzilla #246651.
+       */
+      if (this.transport.overrideMimeType)
+        requestHeaders.push('Connection', 'close');
+    }
+
+    if (this.options.requestHeaders)
+      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
+
+    for (var i = 0; i < requestHeaders.length; i += 2)
+      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
+  },
+
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState != 1)
+      this.respondToReadyState(this.transport.readyState);
+  },
+
+  header: function(name) {
+    try {
+      return this.transport.getResponseHeader(name);
+    } catch (e) {}
+  },
+
+  evalJSON: function() {
+    try {
+      return eval(this.header('X-JSON'));
+    } catch (e) {}
+  },
+
+  evalResponse: function() {
+    try {
+      return eval(this.transport.responseText);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  respondToReadyState: function(readyState) {
+    var event = Ajax.Request.Events[readyState];
+    var transport = this.transport, json = this.evalJSON();
+
+    if (event == 'Complete') {
+      try {
+        (this.options['on' + this.transport.status]
+         || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(transport, json);
+      } catch (e) {
+        this.dispatchException(e);
+      }
+
+      if ((this.header('Content-type') || '').match(/^text\/javascript/i))
+        this.evalResponse();
+    }
+
+    try {
+      (this.options['on' + event] || Prototype.emptyFunction)(transport, json);
+      Ajax.Responders.dispatch('on' + event, this, transport, json);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+
+    /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
+    if (event == 'Complete')
+      this.transport.onreadystatechange = Prototype.emptyFunction;
+  },
+
+  dispatchException: function(exception) {
+    (this.options.onException || Prototype.emptyFunction)(this, exception);
+    Ajax.Responders.dispatch('onException', this, exception);
+  }
+});
+
+Ajax.Updater = Class.create();
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
+  initialize: function(container, url, options) {
+    this.containers = {
+      success: container.success ? $(container.success) : $(container),
+      failure: container.failure ? $(container.failure) :
+        (container.success ? null : $(container))
+    }
+
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+
+    var onComplete = this.options.onComplete || Prototype.emptyFunction;
+    this.options.onComplete = (function(transport, object) {
+      this.updateContent();
+      onComplete(transport, object);
+    }).bind(this);
+
+    this.request(url);
+  },
+
+  updateContent: function() {
+    var receiver = this.responseIsSuccess() ?
+      this.containers.success : this.containers.failure;
+    var response = this.transport.responseText;
+
+    if (!this.options.evalScripts)
+      response = response.stripScripts();
+
+    if (receiver) {
+      if (this.options.insertion) {
+        new this.options.insertion(receiver, response);
+      } else {
+        Element.update(receiver, response);
+      }
+    }
+
+    if (this.responseIsSuccess()) {
+      if (this.onComplete)
+        setTimeout(this.onComplete.bind(this), 10);
+    }
+  }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+  initialize: function(container, url, options) {
+    this.setOptions(options);
+    this.onComplete = this.options.onComplete;
+
+    this.frequency = (this.options.frequency || 2);
+    this.decay = (this.options.decay || 1);
+
+    this.updater = {};
+    this.container = container;
+    this.url = url;
+
+    this.start();
+  },
+
+  start: function() {
+    this.options.onComplete = this.updateComplete.bind(this);
+    this.onTimerEvent();
+  },
+
+  stop: function() {
+    this.updater.onComplete = undefined;
+    clearTimeout(this.timer);
+    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+  },
+
+  updateComplete: function(request) {
+    if (this.options.decay) {
+      this.decay = (request.responseText == this.lastText ?
+        this.decay * this.options.decay : 1);
+
+      this.lastText = request.responseText;
+    }
+    this.timer = setTimeout(this.onTimerEvent.bind(this),
+      this.decay * this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    this.updater = new Ajax.Updater(this.container, this.url, this.options);
+  }
+});
+document.getElementsByClassName = function(className, parentElement) {
+  var children = ($(parentElement) || document.body).getElementsByTagName('*');
+  return $A(children).inject([], function(elements, child) {
+    if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
+      elements.push(child);
+    return elements;
+  });
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element) {
+  var Element = new Object();
+}
+
+Object.extend(Element, {
+  visible: function(element) {
+    return $(element).style.display != 'none';
+  },
+
+  toggle: function() {
+    for (var i = 0; i < arguments.length; i++) {
+      var element = $(arguments[i]);
+      Element[Element.visible(element) ? 'hide' : 'show'](element);
+    }
+  },
+
+  hide: function() {
+    for (var i = 0; i < arguments.length; i++) {
+      var element = $(arguments[i]);
+      element.style.display = 'none';
+    }
+  },
+
+  show: function() {
+    for (var i = 0; i < arguments.length; i++) {
+      var element = $(arguments[i]);
+      element.style.display = '';
+    }
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+  },
+
+  update: function(element, html) {
+    $(element).innerHTML = html.stripScripts();
+    setTimeout(function() {html.evalScripts()}, 10);
+  },
+
+  getHeight: function(element) {
+    element = $(element);
+    return element.offsetHeight;
+  },
+
+  classNames: function(element) {
+    return new Element.ClassNames(element);
+  },
+
+  hasClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    return Element.classNames(element).include(className);
+  },
+
+  addClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    return Element.classNames(element).add(className);
+  },
+
+  removeClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    return Element.classNames(element).remove(className);
+  },
+
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    element = $(element);
+    for (var i = 0; i < element.childNodes.length; i++) {
+      var node = element.childNodes[i];
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+        Element.remove(node);
+    }
+  },
+
+  empty: function(element) {
+    return $(element).innerHTML.match(/^\s*$/);
+  },
+
+  scrollTo: function(element) {
+    element = $(element);
+    var x = element.x ? element.x : element.offsetLeft,
+        y = element.y ? element.y : element.offsetTop;
+    window.scrollTo(x, y);
+  },
+
+  getStyle: function(element, style) {
+    element = $(element);
+    var value = element.style[style.camelize()];
+    if (!value) {
+      if (document.defaultView && document.defaultView.getComputedStyle) {
+        var css = document.defaultView.getComputedStyle(element, null);
+        value = css ? css.getPropertyValue(style) : null;
+      } else if (element.currentStyle) {
+        value = element.currentStyle[style.camelize()];
+      }
+    }
+
+    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
+      if (Element.getStyle(element, 'position') == 'static') value = 'auto';
+
+    return value == 'auto' ? null : value;
+  },
+
+  setStyle: function(element, style) {
+    element = $(element);
+    for (name in style)
+      element.style[name.camelize()] = style[name];
+  },
+
+  getDimensions: function(element) {
+    element = $(element);
+    if (Element.getStyle(element, 'display') != 'none')
+      return {width: element.offsetWidth, height: element.offsetHeight};
+
+    // All *Width and *Height properties give 0 on elements with display none,
+    // so enable the element temporarily
+    var els = element.style;
+    var originalVisibility = els.visibility;
+    var originalPosition = els.position;
+    els.visibility = 'hidden';
+    els.position = 'absolute';
+    els.display = '';
+    var originalWidth = element.clientWidth;
+    var originalHeight = element.clientHeight;
+    els.display = 'none';
+    els.position = originalPosition;
+    els.visibility = originalVisibility;
+    return {width: originalWidth, height: originalHeight};
+  },
+
+  makePositioned: function(element) {
+    element = $(element);
+    var pos = Element.getStyle(element, 'position');
+    if (pos == 'static' || !pos) {
+      element._madePositioned = true;
+      element.style.position = 'relative';
+      // Opera returns the offset relative to the positioning context, when an
+      // element is position relative but top and left have not been defined
+      if (window.opera) {
+        element.style.top = 0;
+        element.style.left = 0;
+      }
+    }
+  },
+
+  undoPositioned: function(element) {
+    element = $(element);
+    if (element._madePositioned) {
+      element._madePositioned = undefined;
+      element.style.position =
+        element.style.top =
+        element.style.left =
+        element.style.bottom =
+        element.style.right = '';
+    }
+  },
+
+  makeClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return;
+    element._overflow = element.style.overflow;
+    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+      element.style.overflow = 'hidden';
+  },
+
+  undoClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return;
+    element.style.overflow = element._overflow;
+    element._overflow = undefined;
+  }
+});
+
+var Toggle = new Object();
+Toggle.display = Element.toggle;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+  this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+  initialize: function(element, content) {
+    this.element = $(element);
+    this.content = content.stripScripts();
+
+    if (this.adjacency && this.element.insertAdjacentHTML) {
+      try {
+        this.element.insertAdjacentHTML(this.adjacency, this.content);
+      } catch (e) {
+        if (this.element.tagName.toLowerCase() == 'tbody') {
+          this.insertContent(this.contentFromAnonymousTable());
+        } else {
+          throw e;
+        }
+      }
+    } else {
+      this.range = this.element.ownerDocument.createRange();
+      if (this.initializeRange) this.initializeRange();
+      this.insertContent([this.range.createContextualFragment(this.content)]);
+    }
+
+    setTimeout(function() {content.evalScripts()}, 10);
+  },
+
+  contentFromAnonymousTable: function() {
+    var div = document.createElement('div');
+    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
+    return $A(div.childNodes[0].childNodes[0].childNodes);
+  }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+  initializeRange: function() {
+    this.range.setStartBefore(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment, this.element);
+    }).bind(this));
+  }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(true);
+  },
+
+  insertContent: function(fragments) {
+    fragments.reverse(false).each((function(fragment) {
+      this.element.insertBefore(fragment, this.element.firstChild);
+    }).bind(this));
+  }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.appendChild(fragment);
+    }).bind(this));
+  }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+  initializeRange: function() {
+    this.range.setStartAfter(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment,
+        this.element.nextSibling);
+    }).bind(this));
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
+
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length > 0;
+    })._each(iterator);
+  },
+
+  set: function(className) {
+    this.element.className = className;
+  },
+
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set(this.toArray().concat(classNameToAdd).join(' '));
+  },
+
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set(this.select(function(className) {
+      return className != classNameToRemove;
+    }).join(' '));
+  },
+
+  toString: function() {
+    return this.toArray().join(' ');
+  }
+}
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+var Field = {
+  clear: function() {
+    for (var i = 0; i < arguments.length; i++)
+      $(arguments[i]).value = '';
+  },
+
+  focus: function(element) {
+    $(element).focus();
+  },
+
+  present: function() {
+    for (var i = 0; i < arguments.length; i++)
+      if ($(arguments[i]).value == '') return false;
+    return true;
+  },
+
+  select: function(element) {
+    $(element).select();
+  },
+
+  activate: function(element) {
+    element = $(element);
+    element.focus();
+    if (element.select)
+      element.select();
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Form = {
+  serialize: function(form) {
+    var elements = Form.getElements($(form));
+    var queryComponents = new Array();
+
+    for (var i = 0; i < elements.length; i++) {
+      var queryComponent = Form.Element.serialize(elements[i]);
+      if (queryComponent)
+        queryComponents.push(queryComponent);
+    }
+
+    return queryComponents.join('&');
+  },
+
+  getElements: function(form) {
+    form = $(form);
+    var elements = new Array();
+
+    for (tagName in Form.Element.Serializers) {
+      var tagElements = form.getElementsByTagName(tagName);
+      for (var j = 0; j < tagElements.length; j++)
+        elements.push(tagElements[j]);
+    }
+    return elements;
+  },
+
+  getInputs: function(form, typeName, name) {
+    form = $(form);
+    var inputs = form.getElementsByTagName('input');
+
+    if (!typeName && !name)
+      return inputs;
+
+    var matchingInputs = new Array();
+    for (var i = 0; i < inputs.length; i++) {
+      var input = inputs[i];
+      if ((typeName && input.type != typeName) ||
+          (name && input.name != name))
+        continue;
+      matchingInputs.push(input);
+    }
+
+    return matchingInputs;
+  },
+
+  disable: function(form) {
+    var elements = Form.getElements(form);
+    for (var i = 0; i < elements.length; i++) {
+      var element = elements[i];
+      element.blur();
+      element.disabled = 'true';
+    }
+  },
+
+  enable: function(form) {
+    var elements = Form.getElements(form);
+    for (var i = 0; i < elements.length; i++) {
+      var element = elements[i];
+      element.disabled = '';
+    }
+  },
+
+  findFirstElement: function(form) {
+    return Form.getElements(form).find(function(element) {
+      return element.type != 'hidden' && !element.disabled &&
+        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    });
+  },
+
+  focusFirstElement: function(form) {
+    Field.activate(Form.findFirstElement(form));
+  },
+
+  reset: function(form) {
+    $(form).reset();
+  }
+}
+
+Form.Element = {
+  serialize: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    var parameter = Form.Element.Serializers[method](element);
+
+    if (parameter) {
+      var key = encodeURIComponent(parameter[0]);
+      if (key.length == 0) return;
+
+      if (parameter[1].constructor != Array)
+        parameter[1] = [parameter[1]];
+
+      return parameter[1].map(function(value) {
+        return key + '=' + encodeURIComponent(value);
+      }).join('&');
+    }
+  },
+
+  getValue: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    var parameter = Form.Element.Serializers[method](element);
+
+    if (parameter)
+      return parameter[1];
+  }
+}
+
+Form.Element.Serializers = {
+  input: function(element) {
+    switch (element.type.toLowerCase()) {
+      case 'submit':
+      case 'hidden':
+      case 'password':
+      case 'text':
+        return Form.Element.Serializers.textarea(element);
+      case 'checkbox':
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element);
+    }
+    return false;
+  },
+
+  inputSelector: function(element) {
+    if (element.checked)
+      return [element.name, element.value];
+  },
+
+  textarea: function(element) {
+    return [element.name, element.value];
+  },
+
+  select: function(element) {
+    return Form.Element.Serializers[element.type == 'select-one' ?
+      'selectOne' : 'selectMany'](element);
+  },
+
+  selectOne: function(element) {
+    var value = '', opt, index = element.selectedIndex;
+    if (index >= 0) {
+      opt = element.options[index];
+      value = opt.value;
+      if (!value && !('value' in opt))
+        value = opt.text;
+    }
+    return [element.name, value];
+  },
+
+  selectMany: function(element) {
+    var value = new Array();
+    for (var i = 0; i < element.length; i++) {
+      var opt = element.options[i];
+      if (opt.selected) {
+        var optValue = opt.value;
+        if (!optValue && !('value' in opt))
+          optValue = opt.text;
+        value.push(optValue);
+      }
+    }
+    return [element.name, value];
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var $F = Form.Element.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = function() {}
+Abstract.TimedObserver.prototype = {
+  initialize: function(element, frequency, callback) {
+    this.frequency = frequency;
+    this.element   = $(element);
+    this.callback  = callback;
+
+    this.lastValue = this.getValue();
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+  initialize: function(element, callback) {
+    this.element  = $(element);
+    this.callback = callback;
+
+    this.lastValue = this.getValue();
+    if (this.element.tagName.toLowerCase() == 'form')
+      this.registerFormCallbacks();
+    else
+      this.registerCallback(this.element);
+  },
+
+  onElementEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  },
+
+  registerFormCallbacks: function() {
+    var elements = Form.getElements(this.element);
+    for (var i = 0; i < elements.length; i++)
+      this.registerCallback(elements[i]);
+  },
+
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':
+        case 'radio':
+          Event.observe(element, 'click', this.onElementEvent.bind(this));
+          break;
+        case 'password':
+        case 'text':
+        case 'textarea':
+        case 'select-one':
+        case 'select-multiple':
+          Event.observe(element, 'change', this.onElementEvent.bind(this));
+          break;
+      }
+    }
+  }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+if (!window.Event) {
+  var Event = new Object();
+}
+
+Object.extend(Event, {
+  KEY_BACKSPACE: 8,
+  KEY_TAB:       9,
+  KEY_RETURN:   13,
+  KEY_ESC:      27,
+  KEY_LEFT:     37,
+  KEY_UP:       38,
+  KEY_RIGHT:    39,
+  KEY_DOWN:     40,
+  KEY_DELETE:   46,
+
+  element: function(event) {
+    return event.target || event.srcElement;
+  },
+
+  isLeftClick: function(event) {
+    return (((event.which) && (event.which == 1)) ||
+            ((event.button) && (event.button == 1)));
+  },
+
+  pointerX: function(event) {
+    return event.pageX || (event.clientX +
+      (document.documentElement.scrollLeft || document.body.scrollLeft));
+  },
+
+  pointerY: function(event) {
+    return event.pageY || (event.clientY +
+      (document.documentElement.scrollTop || document.body.scrollTop));
+  },
+
+  stop: function(event) {
+    if (event.preventDefault) {
+      event.preventDefault();
+      event.stopPropagation();
+    } else {
+      event.returnValue = false;
+      event.cancelBubble = true;
+    }
+  },
+
+  // find the first node with the given tagName, starting from the
+  // node the event was triggered on; traverses the DOM upwards
+  findElement: function(event, tagName) {
+    var element = Event.element(event);
+    while (element.parentNode && (!element.tagName ||
+        (element.tagName.toUpperCase() != tagName.toUpperCase())))
+      element = element.parentNode;
+    return element;
+  },
+
+  observers: false,
+
+  _observeAndCache: function(element, name, observer, useCapture) {
+    if (!this.observers) this.observers = [];
+    if (element.addEventListener) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.addEventListener(name, observer, useCapture);
+    } else if (element.attachEvent) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.attachEvent('on' + name, observer);
+    }
+  },
+
+  unloadCache: function() {
+    if (!Event.observers) return;
+    for (var i = 0; i < Event.observers.length; i++) {
+      Event.stopObserving.apply(this, Event.observers[i]);
+      Event.observers[i][0] = null;
+    }
+    Event.observers = false;
+  },
+
+  observe: function(element, name, observer, useCapture) {
+    var element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+        || element.attachEvent))
+      name = 'keydown';
+
+    this._observeAndCache(element, name, observer, useCapture);
+  },
+
+  stopObserving: function(element, name, observer, useCapture) {
+    var element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+        || element.detachEvent))
+      name = 'keydown';
+
+    if (element.removeEventListener) {
+      element.removeEventListener(name, observer, useCapture);
+    } else if (element.detachEvent) {
+      element.detachEvent('on' + name, observer);
+    }
+  }
+});
+
+/* prevent memory leaks in IE */
+Event.observe(window, 'unload', Event.unloadCache, false);
+var Position = {
+  // set to true if needed, warning: firefox performance problems
+  // NOT neeeded for page scrolling, only if draggable contained in
+  // scrollable elements
+  includeScrollOffsets: false,
+
+  // must be called before calling withinIncludingScrolloffset, every time the
+  // page is scrolled
+  prepare: function() {
+    this.deltaX =  window.pageXOffset
+                || document.documentElement.scrollLeft
+                || document.body.scrollLeft
+                || 0;
+    this.deltaY =  window.pageYOffset
+                || document.documentElement.scrollTop
+                || document.body.scrollTop
+                || 0;
+  },
+
+  realOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.scrollTop  || 0;
+      valueL += element.scrollLeft || 0;
+      element = element.parentNode;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  cumulativeOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  positionedOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+      if (element) {
+        p = Element.getStyle(element, 'position');
+        if (p == 'relative' || p == 'absolute') break;
+      }
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  offsetParent: function(element) {
+    if (element.offsetParent) return element.offsetParent;
+    if (element == document.body) return element;
+
+    while ((element = element.parentNode) && element != document.body)
+      if (Element.getStyle(element, 'position') != 'static')
+        return element;
+
+    return document.body;
+  },
+
+  // caches x/y coordinate pair to use with overlap
+  within: function(element, x, y) {
+    if (this.includeScrollOffsets)
+      return this.withinIncludingScrolloffsets(element, x, y);
+    this.xcomp = x;
+    this.ycomp = y;
+    this.offset = this.cumulativeOffset(element);
+
+    return (y >= this.offset[1] &&
+            y <  this.offset[1] + element.offsetHeight &&
+            x >= this.offset[0] &&
+            x <  this.offset[0] + element.offsetWidth);
+  },
+
+  withinIncludingScrolloffsets: function(element, x, y) {
+    var offsetcache = this.realOffset(element);
+
+    this.xcomp = x + offsetcache[0] - this.deltaX;
+    this.ycomp = y + offsetcache[1] - this.deltaY;
+    this.offset = this.cumulativeOffset(element);
+
+    return (this.ycomp >= this.offset[1] &&
+            this.ycomp <  this.offset[1] + element.offsetHeight &&
+            this.xcomp >= this.offset[0] &&
+            this.xcomp <  this.offset[0] + element.offsetWidth);
+  },
+
+  // within must be called directly before
+  overlap: function(mode, element) {
+    if (!mode) return 0;
+    if (mode == 'vertical')
+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+        element.offsetHeight;
+    if (mode == 'horizontal')
+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+        element.offsetWidth;
+  },
+
+  clone: function(source, target) {
+    source = $(source);
+    target = $(target);
+    target.style.position = 'absolute';
+    var offsets = this.cumulativeOffset(source);
+    target.style.top    = offsets[1] + 'px';
+    target.style.left   = offsets[0] + 'px';
+    target.style.width  = source.offsetWidth + 'px';
+    target.style.height = source.offsetHeight + 'px';
+  },
+
+  page: function(forElement) {
+    var valueT = 0, valueL = 0;
+
+    var element = forElement;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+
+      // Safari fix
+      if (element.offsetParent==document.body)
+        if (Element.getStyle(element,'position')=='absolute') break;
+
+    } while (element = element.offsetParent);
+
+    element = forElement;
+    do {
+      valueT -= element.scrollTop  || 0;
+      valueL -= element.scrollLeft || 0;
+    } while (element = element.parentNode);
+
+    return [valueL, valueT];
+  },
+
+  clone: function(source, target) {
+    var options = Object.extend({
+      setLeft:    true,
+      setTop:     true,
+      setWidth:   true,
+      setHeight:  true,
+      offsetTop:  0,
+      offsetLeft: 0
+    }, arguments[2] || {})
+
+    // find page position of source
+    source = $(source);
+    var p = Position.page(source);
+
+    // find coordinate system to use
+    target = $(target);
+    var delta = [0, 0];
+    var parent = null;
+    // delta [0,0] will do fine with position: fixed elements,
+    // position:absolute needs offsetParent deltas
+    if (Element.getStyle(target,'position') == 'absolute') {
+      parent = Position.offsetParent(target);
+      delta = Position.page(parent);
+    }
+
+    // correct by body offsets (fixes Safari)
+    if (parent == document.body) {
+      delta[0] -= document.body.offsetLeft;
+      delta[1] -= document.body.offsetTop;
+    }
+
+    // set position
+    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
+    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
+    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
+    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
+  },
+
+  absolutize: function(element) {
+    element = $(element);
+    if (element.style.position == 'absolute') return;
+    Position.prepare();
+
+    var offsets = Position.positionedOffset(element);
+    var top     = offsets[1];
+    var left    = offsets[0];
+    var width   = element.clientWidth;
+    var height  = element.clientHeight;
+
+    element._originalLeft   = left - parseFloat(element.style.left  || 0);
+    element._originalTop    = top  - parseFloat(element.style.top || 0);
+    element._originalWidth  = element.style.width;
+    element._originalHeight = element.style.height;
+
+    element.style.position = 'absolute';
+    element.style.top    = top + 'px';;
+    element.style.left   = left + 'px';;
+    element.style.width  = width + 'px';;
+    element.style.height = height + 'px';;
+  },
+
+  relativize: function(element) {
+    element = $(element);
+    if (element.style.position == 'relative') return;
+    Position.prepare();
+
+    element.style.position = 'relative';
+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.height = element._originalHeight;
+    element.style.width  = element._originalWidth;
+  }
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely
+// positioned.  For performance reasons, redefine Position.cumulativeOffset for
+// KHTML/WebKit only.
+if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+  Position.cumulativeOffset = function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      if (element.offsetParent == document.body)
+        if (Element.getStyle(element, 'position') == 'absolute') break;
+
+      element = element.offsetParent;
+    } while (element);
+
+    return [valueL, valueT];
+  }
+}
\ No newline at end of file
-- 
GitLab