diff --git a/Makefile b/Makefile index b4f722c96..9c058fae4 100644 --- a/Makefile +++ b/Makefile @@ -566,6 +566,13 @@ clean: clean-depend datafiles: all if [ x`$(ID) -un` = xroot ]; then sh build_tools/mkpkgconfig $(DESTDIR)/usr/lib/pkgconfig; fi + # Should static HTTP be installed during make samples or even with its own target ala + # webvoicemail? There are portions here that *could* be customized but might also be + # improved a lot. I'll put it here for now. + mkdir -p $(DESTDIR)$(ASTVARLIBDIR)/static-http + for x in static-http/*; do \ + install -m 644 $$x $(DESTDIR)$(ASTVARLIBDIR)/static-http ; \ + done mkdir -p $(DESTDIR)$(ASTVARLIBDIR)/sounds/digits mkdir -p $(DESTDIR)$(ASTVARLIBDIR)/sounds/priv-callerintros for x in sounds/digits/*.gsm; do \ diff --git a/configs/http.conf.sample b/configs/http.conf.sample index aea95643f..7135463d6 100644 --- a/configs/http.conf.sample +++ b/configs/http.conf.sample @@ -4,15 +4,20 @@ ; [general] ; -; Whether HTTP interface is enabled or not. +; Whether HTTP interface is enabled or not. Default is no. ; -enabled=no +;enabled=yes ; -; Address to bind to +; Whether Asterisk should serve static content from http-static +; Default is no. +; +;enablestatic=yes +; +; Address to bind to. Default is 0.0.0.0 ; bindaddr=127.0.0.1 ; -; Port to bind to +; Port to bind to (default is 8088) ; bindport=8088 ; diff --git a/configs/manager.conf.sample b/configs/manager.conf.sample index cdca9bc63..1db1ef4cc 100644 --- a/configs/manager.conf.sample +++ b/configs/manager.conf.sample @@ -13,11 +13,18 @@ ; ---------------------------- SECURITY NOTE ------------------------------- ; Note that you should not enable the AMI on a public IP address. If needed, ; block this TCP port with iptables (or another FW software) and reach it -; with IPsec, SSH, or SSL vpn tunnel +; with IPsec, SSH, or SSL vpn tunnel. You can also make the manager +; interface available over http if Asterisk's http server is enabled in +; http.conf and if both "enabled" and "webenabled" are set to yes in +; this file. Both default to no. httptimeout provides the maximum +; timeout in seconds before a web based session is discarded. The +; default is 60 seconds. ; [general] enabled = no +;webenabled = yes port = 5038 +;httptimeout = 60 bindaddr = 0.0.0.0 ;displayconnects = yes ; diff --git a/doc/ajam.txt b/doc/ajam.txt new file mode 100644 index 000000000..d3babd0c2 --- /dev/null +++ b/doc/ajam.txt @@ -0,0 +1,91 @@ +Asynchronous Javascript Asterisk Manger (AJAM) +============================================== + +AJAM is a new technology which allows web browsers or other HTTP enabled +applications and web pages to directly access the Asterisk Manger +Interface (AMI) via HTTP. Setting up your server to process AJAM +involves a few steps: + +Setup the Asterisk HTTP server +------------------------------ + +1) Uncomment the line "enabled=yes" in /etc/asterisk/http.conf to enable + Asterisk's builtin micro HTTP server. + +2) If you want Asterisk to actually deliver simple HTML pages, CSS, + javascript, etc. you should uncomment "enablestatic=yes" + +3) Adjust your "bindaddr" and "bindport" settings as appropriate for + your desired accessibility + +4) Adjust your "prefix" if appropriate, which must be the beginning of + any URI on the server to match. The default is "asterisk" and the + rest of these instructions assume that value. + +Allow Manager Access via HTTP +----------------------------- + +1) Make sure you have both "enabled = yes" and "webenabled = yes" setup + in /etc/asterisk/manager.conf + +2) You may also use "httptimeout" to set a default timeout for HTTP + connections. + +3) Make sure you have a manager username/secret + +Once those configurations are complete you can reload or restart +Asterisk and you should be able to point your web browser to specific +URI's which will allow you to access various web functions. A complete +list can be found by typing "show http" at the Asterisk CLI. + +examples: + +http://localhost:8088/asterisk/manager?action=login&username=foo&secret=bar + +This logs you into the manager interface's "HTML" view. Once you're +logged in, Asterisk stores a cookie on your browser (valid for the +length of httptimeout) which is used to connect to the same session. + +http://localhost:8088/asterisk/rawman?action=status + +Assuming you've already logged into manager, this URI will give you a +"raw" manager output for the "status" command. + +http://localhost:8088/asterisk/mxml?action=status + +This will give you the same status view but represented as AJAX data, +theoretically compatible with RICO (http://www.openrico.org). + +http://localhost:8088/asterisk/static/ajamdemo.html + +If you have enabled static content support and have done a make install, +Asterisk will serve up a demo page which presents a live, but very +basic, "astman" like interface. You can login with your username/secret +for manager and have a basic view of channels as well as transfer and +hangup calls. It's only tested in Firefox, but could probably be made +to run in other browsers as well. + +A sample library (astman.js) is included to help ease the creation of +manager HTML interfaces. + +Note that for the demo, there is no need for *any* external web server. + +Integration with other web servers +---------------------------------- + +Asterisk's micro HTTP server is *not* designed to replace a general +purpose web server and it is intentionally created to provide only the +minimal interfaces required. Even without the addition of an external +web server, one can use Asterisk's interfaces to implement screen pops +and similar tools pulling data from other web servers using iframes, +div's etc. If you want to integrate CGI's, databases, PHP, etc. you +will likely need to use a more traditional web server like Apache and +link in your Asterisk micro HTTP server with something like this: + +ProxyPass /asterisk http://localhost:8088/asterisk + +This is a fairly new technology so I'd love to hear if it's useful for +you! + +Mark + diff --git a/http.c b/http.c index cdcde76a8..5fcdc8ac9 100644 --- a/http.c +++ b/http.c @@ -33,16 +33,20 @@ #include #include #include +#include #include #include #include #include #include +#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 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, "
\r\n"); v = vars; while(v) { - ast_build_string(&c, &reslen, "Submitted Variable '%s'%s\r\n", v->name, v->value); + if (strncasecmp(v->name, "cookie_", 7)) + ast_build_string(&c, &reslen, "Submitted Variable '%s'%s\r\n", v->name, v->value); + v = v->next; + } + ast_build_string(&c, &reslen, "
\r\n"); + v = vars; + while(v) { + if (!strncasecmp(v->name, "cookie_", 7)) + ast_build_string(&c, &reslen, "Cookie '%s'%s\r\n", v->name, v->value); v = v->next; } ast_build_string(&c, &reslen, "
Asterisk and Digium are registered trademarks of Digium, Inc.
\r\n"); @@ -100,6 +206,13 @@ static struct ast_http_uri statusuri = { .has_subtree = 0, }; +static struct ast_http_uri staticuri = { + .callback = static_callback, + .description = "Asterisk HTTP Static Delivery", + .uri = "static", + .has_subtree = 1, +}; + char *ast_http_error(int status, const char *title, const char *extra_header, const char *text) { char *c = NULL; @@ -153,7 +266,7 @@ void ast_http_uri_unlink(struct ast_http_uri *urih) } } -static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char **title, int *contentlength) +static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char **title, int *contentlength, struct ast_variable **cookies) { char *c; char *turi; @@ -176,9 +289,9 @@ static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char ** if (val) { *val = '\0'; val++; + ast_uri_decode(val); } else val = ""; - ast_uri_decode(val); ast_uri_decode(var); if ((v = ast_variable_new(var, val))) { if (vars) @@ -189,6 +302,11 @@ static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char ** } } } + if (prev) + prev->next = *cookies; + else + vars = *cookies; + *cookies = NULL; ast_uri_decode(uri); if (!strncasecmp(uri, prefix, prefix_len)) { uri += prefix_len; @@ -227,9 +345,12 @@ static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char ** static void *ast_httpd_helper_thread(void *data) { char buf[4096]; + char cookie[4096]; char timebuf[256]; struct ast_http_server_instance *ser = data; + struct ast_variable *var, *prev=NULL, *vars=NULL; char *uri, *c, *title=NULL; + char *vname, *vval; int status = 200, contentlength = 0; time_t t; @@ -252,25 +373,68 @@ static void *ast_httpd_helper_thread(void *data) *c = '\0'; } } + + while (fgets(cookie, sizeof(cookie), ser->f)) { + /* Trim trailing characters */ + while(!ast_strlen_zero(cookie) && (cookie[strlen(cookie) - 1] < 33)) { + cookie[strlen(cookie) - 1] = '\0'; + } + if (ast_strlen_zero(cookie)) + break; + if (!strncasecmp(cookie, "Cookie: ", 8)) { + vname = cookie + 8; + vval = strchr(vname, '='); + if (vval) { + /* Ditch the = and the quotes */ + *vval = '\0'; + vval++; + if (*vval) + vval++; + if (strlen(vval)) + vval[strlen(vval) - 1] = '\0'; + var = ast_variable_new(vname, vval); + if (var) { + if (prev) + prev->next = var; + else + vars = var; + prev = var; + } + } + } + } + if (*uri) { if (!strcasecmp(buf, "get")) - c = handle_uri(&ser->requestor, uri, &status, &title, &contentlength); + c = handle_uri(&ser->requestor, uri, &status, &title, &contentlength, &vars); else c = ast_http_error(501, "Not Implemented", NULL, "Attempt to use unimplemented / unsupported method");\ } else c = ast_http_error(400, "Bad Request", NULL, "Invalid Request"); + + /* If they aren't mopped up already, clean up the cookies */ + if (vars) + ast_variables_destroy(vars); + if (!c) c = ast_http_error(500, "Internal Error", NULL, "Internal Server Error"); if (c) { time(&t); strftime(timebuf, sizeof(timebuf), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&t)); - ast_cli(ser->fd, "HTTP/1.1 GET %d %s\r\n", status, title ? title : "OK"); + ast_cli(ser->fd, "HTTP/1.1 %d %s\r\n", status, title ? title : "OK"); ast_cli(ser->fd, "Server: Asterisk\r\n"); ast_cli(ser->fd, "Date: %s\r\n", timebuf); - if (contentlength) - ast_cli(ser->fd, "Content-length: %d\r\n", contentlength); ast_cli(ser->fd, "Connection: close\r\n"); - ast_cli(ser->fd, "%s", c); + if (contentlength) { + char *tmp; + tmp = strstr(c, "\r\n\r\n"); + if (tmp) { + ast_cli(ser->fd, "Content-length: %d\r\n", contentlength); + write(ser->fd, c, (tmp + 4 - c)); + write(ser->fd, tmp + 4, contentlength); + } + } else + ast_cli(ser->fd, "%s", c); free(c); } if (title) @@ -297,19 +461,22 @@ static void *http_root(void *data) ast_log(LOG_WARNING, "Accept failed: %s\n", strerror(errno)); continue; } - if (!(ser = ast_calloc(1, sizeof(*ser)))) { - close(fd); - continue; - } - ser->fd = fd; - if ((ser->f = fdopen(ser->fd, "w+"))) { - if (ast_pthread_create(&launched, NULL, ast_httpd_helper_thread, ser)) { - ast_log(LOG_WARNING, "Unable to launch helper thread: %s\n", strerror(errno)); - fclose(ser->f); + ser = ast_calloc(1, sizeof(*ser)); + if (ser) { + ser->fd = fd; + memcpy(&ser->requestor, &sin, sizeof(ser->requestor)); + if ((ser->f = fdopen(ser->fd, "w+"))) { + if (ast_pthread_create(&launched, NULL, ast_httpd_helper_thread, ser)) { + ast_log(LOG_WARNING, "Unable to launch helper thread: %s\n", strerror(errno)); + fclose(ser->f); + free(ser); + } + } else { + ast_log(LOG_WARNING, "fdopen failed!\n"); + close(ser->fd); free(ser); } } else { - ast_log(LOG_WARNING, "fdopen failed!\n"); close(ser->fd); free(ser); } @@ -317,6 +484,18 @@ static void *http_root(void *data) return NULL; } +char *ast_http_setcookie(const char *var, const char *val, int expires, char *buf, int buflen) +{ + char *c; + c = buf; + ast_build_string(&c, &buflen, "Set-Cookie: %s=\"%s\"; Version=\"1\"", var, val); + if (expires) + ast_build_string(&c, &buflen, "; Max-Age=%d", expires); + ast_build_string(&c, &buflen, "\r\n"); + return buf; +} + + static void http_server_start(struct sockaddr_in *sin) { char iabuf[INET_ADDRSTRLEN]; @@ -383,6 +562,7 @@ static int __ast_http_load(int reload) struct ast_config *cfg; struct ast_variable *v; int enabled=0; + int newenablestatic=0; struct sockaddr_in sin; struct hostent *hp; struct ast_hostent ahp; @@ -396,6 +576,8 @@ static int __ast_http_load(int reload) while(v) { if (!strcasecmp(v->name, "enabled")) enabled = ast_true(v->value); + else if (!strcasecmp(v->name, "enablestatic")) + newenablestatic = ast_true(v->value); else if (!strcasecmp(v->name, "bindport")) sin.sin_port = ntohs(atoi(v->value)); else if (!strcasecmp(v->name, "bindaddr")) { @@ -416,6 +598,7 @@ static int __ast_http_load(int reload) ast_copy_string(prefix, newprefix, sizeof(prefix)); prefix_len = strlen(prefix); } + enablestatic = newenablestatic; http_server_start(&sin); return 0; } @@ -462,6 +645,7 @@ static struct ast_cli_entry http_cli[] = { int ast_http_init(void) { ast_http_uri_link(&statusuri); + ast_http_uri_link(&staticuri); ast_cli_register_multiple(http_cli, sizeof(http_cli) / sizeof(http_cli[0])); return __ast_http_load(0); } diff --git a/include/asterisk/http.h b/include/asterisk/http.h index ea580c3ca..9156db0c4 100644 --- a/include/asterisk/http.h +++ b/include/asterisk/http.h @@ -58,6 +58,8 @@ char *ast_http_error(int status, const char *title, const char *extra_header, co /* Destroy an HTTP server */ void ast_http_uri_unlink(struct ast_http_uri *urihandler); +char *ast_http_setcookie(const char *var, const char *val, int expires, char *buf, int buflen); + int ast_http_init(void); int ast_http_reload(void); diff --git a/manager.c b/manager.c index 16605bd40..b2d4964c5 100644 --- a/manager.c +++ b/manager.c @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -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, "<"); + (*dst) += 4; + *maxlen -= 4; + break; + case '>': + strcpy(*dst, ">"); + (*dst) += 4; + *maxlen -= 4; + break; + case '\"': + strcpy(*dst, """); + (*dst) += 6; + *maxlen -= 6; + break; + case '\'': + strcpy(*dst, "'"); + (*dst) += 6; + *maxlen -= 6; + break; + case '&': + strcpy(*dst, "&"); + (*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", "= 32)) in++; + if (*in) { + if ((count > 3) && inobj) { + ast_build_string(&tmp, &len, " />\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, "<%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, " />\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; /* , "
*/ + 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, "
\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, "%s%s\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__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 && (xnext) { + 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, "Asterisk™ Manager Test Interface"); + vars2msg(&m, params); + if (format == FORMAT_XML) { + ast_build_string(&c, &len, "\n"); + } else if (format == FORMAT_HTML) { + ast_build_string(&c, &len, "\r\n"); + ast_build_string(&c, &len, "\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, "\n"); + } else if (format == FORMAT_HTML) + ast_build_string(&c, &len, "

  Manager Tester

\r\n"); + } else { + *status = 500; + *title = strdup("Server Error"); + } + ast_mutex_lock(&s->__lock); + if (s->needdestroy) { + if (s->inuse == 1) { + ast_log(LOG_DEBUG, "Need destroy, doing it now!\n"); + blastaway = 1; + } else { + ast_log(LOG_DEBUG, "Need destroy, but can't do it yet!\n"); + if (s->waiting_thread != AST_PTHREADT_NULL) + pthread_kill(s->waiting_thread, SIGURG); + s->inuse--; + } + } else + s->inuse--; + ast_mutex_unlock(&s->__lock); + + if (blastaway) + destroy_session(s); + if (*status != 200) + return ast_http_error(500, "Server Error", NULL, "Internal Server Error (out of memory)\n"); + return retval; +} + +static char *manager_http_callback(struct sockaddr_in *requestor, const char *uri, struct ast_variable *params, int *status, char **title, int *contentlength) +{ + return generic_http_callback(FORMAT_HTML, requestor, uri, params, status, title, contentlength); +} + +static char *mxml_http_callback(struct sockaddr_in *requestor, const char *uri, struct ast_variable *params, int *status, char **title, int *contentlength) +{ + return generic_http_callback(FORMAT_XML, requestor, uri, params, status, title, contentlength); +} + +static char *rawman_http_callback(struct sockaddr_in *requestor, const char *uri, struct ast_variable *params, int *status, char **title, int *contentlength) +{ + return generic_http_callback(FORMAT_RAW, requestor, uri, params, status, title, contentlength); +} + +struct ast_http_uri rawmanuri = { + .description = "Raw HTTP Manager Event Interface", + .uri = "rawman", + .has_subtree = 0, + .callback = rawman_http_callback, +}; + +struct ast_http_uri manageruri = { + .description = "HTML Manager Event Interface", + .uri = "manager", + .has_subtree = 0, + .callback = manager_http_callback, +}; + +struct ast_http_uri managerxmluri = { + .description = "XML Manager Event Interface", + .uri = "mxml", + .has_subtree = 0, + .callback = mxml_http_callback, +}; + static int registered = 0; +static int webregged = 0; int init_manager(void) { @@ -1710,6 +2222,9 @@ int init_manager(void) int oldportno = portno; static struct sockaddr_in ba; int x = 1; + int flags; + int webenabled=0; + int newhttptimeout = 60; if (!registered) { /* Register default actions */ ast_manager_register2("Ping", 0, action_ping, "Keepalive command", mandescr_ping); @@ -1727,6 +2242,7 @@ int init_manager(void) ast_manager_register2("MailboxStatus", EVENT_FLAG_CALL, action_mailboxstatus, "Check Mailbox", mandescr_mailboxstatus ); ast_manager_register2("MailboxCount", EVENT_FLAG_CALL, action_mailboxcount, "Check Mailbox Message Count", mandescr_mailboxcount ); ast_manager_register2("ListCommands", 0, action_listcommands, "List available manager commands", mandescr_listcommands); + ast_manager_register2("WaitEvent", 0, action_waitevent, "Wait for an event to occur", mandescr_waitevent); ast_cli_register(&show_mancmd_cli); ast_cli_register(&show_mancmds_cli); @@ -1750,6 +2266,10 @@ int init_manager(void) if(val) block_sockets = ast_true(val); + val = ast_variable_retrieve(cfg, "general", "webenabled"); + if (val) + webenabled = ast_true(val); + if ((val = ast_variable_retrieve(cfg, "general", "port"))) { if (sscanf(val, "%d", &portno) != 1) { ast_log(LOG_WARNING, "Invalid port number '%s'\n", val); @@ -1762,6 +2282,9 @@ int init_manager(void) if ((val = ast_variable_retrieve(cfg, "general", "timestampevents"))) timestampevents = ast_true(val); + + if ((val = ast_variable_retrieve(cfg, "general", "httptimeout"))) + newhttptimeout = atoi(val); ba.sin_family = AF_INET; ba.sin_port = htons(portno); @@ -1785,6 +2308,25 @@ int init_manager(void) } ast_config_destroy(cfg); + if (webenabled && enabled) { + if (!webregged) { + ast_http_uri_link(&rawmanuri); + ast_http_uri_link(&manageruri); + ast_http_uri_link(&managerxmluri); + webregged = 1; + } + } else { + if (webregged) { + ast_http_uri_unlink(&rawmanuri); + ast_http_uri_unlink(&manageruri); + ast_http_uri_unlink(&managerxmluri); + webregged = 0; + } + } + + if (newhttptimeout > 0) + httptimeout = newhttptimeout; + /* If not enabled, do nothing */ if (!enabled) { return 0; @@ -1808,6 +2350,8 @@ int init_manager(void) asock = -1; return -1; } + flags = fcntl(asock, F_GETFL); + fcntl(asock, F_SETFL, flags | O_NONBLOCK); if (option_verbose) ast_verbose("Asterisk Management interface listening on port %d\n", portno); ast_pthread_create(&t, NULL, accept_thread, NULL); diff --git a/static-http/ajamdemo.html b/static-http/ajamdemo.html new file mode 100644 index 000000000..687b59044 --- /dev/null +++ b/static-http/ajamdemo.html @@ -0,0 +1,215 @@ + + + + + + +Asterisk™ AJAM Demo + + + + + +
+ + + + + + + + +

Asterisk™ AJAM Demo

Username:
Secret:
+
+   +
+
+
+ +
+This is a demo of the Asynchronous Javascript Asterisk Manager interface. You can login with a +valid, appropriately permissioned manager username and secret. +
+ + + + + +
+
+
+
+
+ + Copyright (C) 2006 Digium, Inc. Asterisk and Digium are trademarks of Digium, Inc. + +
+ diff --git a/static-http/astman.css b/static-http/astman.css new file mode 100644 index 000000000..fbf2b2cf9 --- /dev/null +++ b/static-http/astman.css @@ -0,0 +1,34 @@ +.chanlist { + border : 1px solid #1f669b; + height : 150px; + overflow : auto; + background-color : #f1f1f1; + width : 600; +} + +.chantable { + border : 0px; + background-color : #f1f1f1; + width : 100%; +} + +.labels { + background-color : #000000; + color : #ffffff; +} + +.chanlisteven { + background-color : #fff8e4; +} + +.chanlistodd { + background-color : #f0f5ff; +} + +.chanlistselected { + background-color : #ffb13d; +} + +.light { + color : #717171; +} diff --git a/static-http/astman.js b/static-http/astman.js new file mode 100644 index 000000000..1a6927263 --- /dev/null +++ b/static-http/astman.js @@ -0,0 +1,256 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Javascript routines or accessing manager routines over HTTP. + * + * Copyright (C) 1999 - 2006, Digium, Inc. + * + * Mark Spencer + * + * 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;xChannelStateCallerLocationLink"; + 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"; + s = s + "" + channels[x].channel + ""; + if (channels[x].state) + s = s + "" + channels[x].state + ""; + else + s = s + "unknown"; + if (channels[x].calleridname && channels[x].callerid && channels[x].calleridname != "") { + cid = channels[x].calleridname.escapeHTML() + " <" + channels[x].callerid.escapeHTML() + ">"; + } else if (channels[x].calleridname && (channels[x].calleridname != "")) { + cid = channels[x].calleridname.escapeHTML(); + } else if (channels[x].callerid) { + cid = channels[x].callerid.escapeHTML(); + } else { + cid = "Unknown"; + } + s = s + "" + cid + ""; + if (channels[x].extension) { + s = s + "" + channels[x].extension + "@" + channels[x].context + ":" + channels[x].priority + ""; + } else { + s = s + "None"; + } + if (channels[x].link) { + s = s + "" + channels[x].link + ""; + } else { + s = s + "None"; + } + s = s + "\n"; + found++; + } + } + if (!found) + s += "No active channels\n"; + s += "\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 + * + * 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: '(?:)((\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 '#'; + } +} + +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 '#'; + } +} + +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 = '' + this.content + '
'; + 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