Flesh out the remainder of the manager + http changes and create a sample application...
authorMark Spencer <markster@digium.com>
Sat, 1 Apr 2006 08:49:54 +0000 (08:49 +0000)
committerMark Spencer <markster@digium.com>
Sat, 1 Apr 2006 08:49:54 +0000 (08:49 +0000)
demonstrate the capability of manager over http.

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@16850 65c4cc65-6c06-0410-ace0-fbb531ad65f3

Makefile
configs/http.conf.sample
configs/manager.conf.sample
doc/ajam.txt [new file with mode: 0644]
http.c
include/asterisk/http.h
manager.c
static-http/ajamdemo.html [new file with mode: 0644]
static-http/astman.css [new file with mode: 0644]
static-http/astman.js [new file with mode: 0644]
static-http/prototype.js [new file with mode: 0644]

index b4f722c..9c058fa 100644 (file)
--- 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 \
index aea9564..7135463 100644 (file)
@@ -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
 ;
index cdca9bc..1db1ef4 100644 (file)
 ; ---------------------------- 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 (file)
index 0000000..d3babd0
--- /dev/null
@@ -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 cdcde76..5fcdc8a 100644 (file)
--- a/http.c
+++ b/http.c
 #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);
 }
index ea580c3..9156db0 100644 (file)
@@ -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);
 
index 16605bd..b2d4964 100644 (file)
--- 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 (file)
index 0000000..687b590
--- /dev/null
@@ -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 (file)
index 0000000..fbf2b2c
--- /dev/null
@@ -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 (file)
index 0000000..1a69272
--- /dev/null
@@ -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 (file)
index 0000000..0e85338
--- /dev/null
@@ -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