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