Index: trunk/web/Makefile
===================================================================
--- trunk/web/Makefile	(revision 236)
+++ trunk/web/Makefile	(revision 236)
@@ -0,0 +1,13 @@
+TEMPLATES=$(wildcard *.tmpl)
+OUTPUTS=$(TEMPLATES:.tmpl=.py)
+
+all: ${OUTPUTS}
+
+%.py: %.tmpl
+	cheetah compile $<
+
+#${OUTPUTS}:${TEMPLATES}
+#	cheetah compile $^
+
+clean:
+	@rm -f ${OUTPUTS} *.pyo *.pyc *.py.bak
Index: trunk/web/controls.py
===================================================================
--- trunk/web/controls.py	(revision 236)
+++ trunk/web/controls.py	(revision 236)
@@ -0,0 +1,284 @@
+"""
+Functions to perform remctls.
+"""
+
+from sipb_xen_database import Machine, Disk, Type, NIC, CDROM, ctx, meta
+import validation
+from webcommon import CodeError, InvalidInput
+import random
+import subprocess
+import sys
+import time
+import re
+
+# ... and stolen from xend/uuid.py
+def randomUUID():
+    """Generate a random UUID."""
+
+    return [ random.randint(0, 255) for _ in range(0, 16) ]
+
+def uuidToString(u):
+    """Turn a numeric UUID to a hyphen-seperated one."""
+    return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
+                     "%02x" * 6]) % tuple(u)
+# end stolen code
+
+def kinit(username = 'daemon/sipb-xen.mit.edu', keytab = '/etc/sipb-xen.keytab'):
+    """Kinit with a given username and keytab"""
+
+    p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
+                         stderr=subprocess.PIPE)
+    e = p.wait()
+    if e:
+        raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
+
+def checkKinit():
+    """If we lack tickets, kinit."""
+    p = subprocess.Popen(['klist', '-s'])
+    if p.wait():
+        kinit()
+
+def remctl(*args, **kws):
+    """Perform a remctl and return the output.
+
+    kinits if necessary, and outputs errors to stderr.
+    """
+    checkKinit()
+    p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
+                         + list(args),
+                         stdout=subprocess.PIPE,
+                         stderr=subprocess.PIPE)
+    v = p.wait()
+    if kws.get('err'):
+        return p.stdout.read(), p.stderr.read()
+    if v:
+        print >> sys.stderr, 'Error', v, 'on remctl', args, ':'
+        print >> sys.stderr, p.stderr.read()
+        raise CodeError('ERROR on remctl')
+    return p.stdout.read()
+
+def lvcreate(machine, disk):
+    """Create a single disk for a machine"""
+    remctl('web', 'lvcreate', machine.name,
+           disk.guest_device_name, str(disk.size))
+    
+def makeDisks(machine):
+    """Update the lvm partitions to add a disk."""
+    for disk in machine.disks:
+        lvcreate(machine, disk)
+
+def bootMachine(machine, cdtype):
+    """Boot a machine with a given boot CD.
+
+    If cdtype is None, give no boot cd.  Otherwise, it is the string
+    id of the CD (e.g. 'gutsy_i386')
+    """
+    if cdtype is not None:
+        remctl('control', machine.name, 'create', 
+               cdtype)
+    else:
+        remctl('control', machine.name, 'create')
+
+def registerMachine(machine):
+    """Register a machine to be controlled by the web interface"""
+    remctl('web', 'register', machine.name)
+
+def unregisterMachine(machine):
+    """Unregister a machine to not be controlled by the web interface"""
+    remctl('web', 'unregister', machine.name)
+
+def createVm(owner, contact, name, memory, disk, is_hvm, cdrom):
+    """Create a VM and put it in the database"""
+    # put stuff in the table
+    transaction = ctx.current.create_transaction()
+    try:
+        validation.validMemory(owner, memory)
+        validation.validDisk(owner, disk  * 1. / 1024)
+        validation.validAddVm(owner)
+        res = meta.engine.execute('select nextval('
+                                  '\'"machines_machine_id_seq"\')')
+        id = res.fetchone()[0]
+        machine = Machine()
+        machine.machine_id = id
+        machine.name = name
+        machine.memory = memory
+        machine.owner = owner
+        machine.administrator = owner
+        machine.contact = contact
+        machine.uuid = uuidToString(randomUUID())
+        machine.boot_off_cd = True
+        machine_type = Type.get_by(hvm=is_hvm)
+        machine.type_id = machine_type.type_id
+        ctx.current.save(machine)
+        disk = Disk(machine.machine_id, 
+                    'hda', disk)
+        open_nics = NIC.select_by(machine_id=None)
+        if not open_nics: #No IPs left!
+            raise CodeError("No IP addresses left!  "
+                            "Contact sipb-xen-dev@mit.edu")
+        nic = open_nics[0]
+        nic.machine_id = machine.machine_id
+        nic.hostname = name
+        ctx.current.save(nic)    
+        ctx.current.save(disk)
+        transaction.commit()
+    except:
+        transaction.rollback()
+        raise
+    registerMachine(machine)
+    makeDisks(machine)
+    # tell it to boot with cdrom
+    bootMachine(machine, cdrom)
+    return machine
+
+def getUptimes(machines=None):
+    """Return a dictionary mapping machine names to uptime strings"""
+    value_string = remctl('web', 'listvms')
+    lines = value_string.splitlines()
+    d = {}
+    for line in lines:
+        lst = line.split()
+        name, id = lst[:2]
+        uptime = ' '.join(lst[2:])
+        d[name] = uptime
+    ans = {}
+    for m in machines:
+        ans[m] = d.get(m.name)
+    return ans
+
+def parseStatus(s):
+    """Parse a status string into nested tuples of strings.
+
+    s = output of xm list --long <machine_name>
+    """
+    values = re.split('([()])', s)
+    stack = [[]]
+    for v in values[2:-2]: #remove initial and final '()'
+        if not v:
+            continue
+        v = v.strip()
+        if v == '(':
+            stack.append([])
+        elif v == ')':
+            if len(stack[-1]) == 1:
+                stack[-1].append('')
+            stack[-2].append(stack[-1])
+            stack.pop()
+        else:
+            if not v:
+                continue
+            stack[-1].extend(v.split())
+    return stack[-1]
+
+def statusInfo(machine):
+    """Return the status list for a given machine.
+
+    Gets and parses xm list --long
+    """
+    value_string, err_string = remctl('control', machine.name, 'list-long', 
+                                      err=True)
+    if 'Unknown command' in err_string:
+        raise CodeError("ERROR in remctl list-long %s is not registered" % 
+                        (machine.name,))
+    elif 'does not exist' in err_string:
+        return None
+    elif err_string:
+        raise CodeError("ERROR in remctl list-long %s:  %s" % 
+                        (machine.name, err_string))
+    status = parseStatus(value_string)
+    return status
+
+def deleteVM(machine):
+    """Delete a VM."""
+    remctl('control', machine.name, 'destroy', err=True)
+    transaction = ctx.current.create_transaction()
+    delete_disk_pairs = [(machine.name, d.guest_device_name) 
+                         for d in machine.disks]
+    try:
+        for nic in machine.nics:
+            nic.machine_id = None
+            nic.hostname = None
+            ctx.current.save(nic)
+        for disk in machine.disks:
+            ctx.current.delete(disk)
+        ctx.current.delete(machine)
+        transaction.commit()
+    except:
+        transaction.rollback()
+        raise
+    for mname, dname in delete_disk_pairs:
+        remctl('web', 'lvremove', mname, dname)
+    unregisterMachine(machine)
+
+def commandResult(user, fields):
+    start_time = 0
+    print >> sys.stderr, time.time()-start_time
+    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
+    action = fields.getfirst('action')
+    cdrom = fields.getfirst('cdrom')
+    print >> sys.stderr, time.time()-start_time
+    if cdrom is not None and not CDROM.get(cdrom):
+        raise CodeError("Invalid cdrom type '%s'" % cdrom)    
+    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 
+                      'Delete VM'):
+        raise CodeError("Invalid action '%s'" % action)
+    if action == 'Reboot':
+        if cdrom is not None:
+            out, err = remctl('control', machine.name, 'reboot', cdrom,
+                              err=True)
+        else:
+            out, err = remctl('control', machine.name, 'reboot',
+                              err=True)
+        if err:
+            if re.match("Error: Domain '.*' does not exist.", err):
+                raise InvalidInput("action", "reboot", 
+                                   "Machine is not on")
+            else:
+                print >> sys.stderr, 'Error on reboot:'
+                print >> sys.stderr, err
+                raise CodeError('ERROR on remctl')
+                
+    elif action == 'Power on':
+        if validation.maxMemory(user, machine) < machine.memory:
+            raise InvalidInput('action', 'Power on',
+                               "You don't have enough free RAM quota "
+                               "to turn on this machine.")
+        bootMachine(machine, cdrom)
+    elif action == 'Power off':
+        out, err = remctl('control', machine.name, 'destroy', err=True)
+        if err:
+            if re.match("Error: Domain '.*' does not exist.", err):
+                raise InvalidInput("action", "Power off", 
+                                   "Machine is not on.")
+            else:
+                print >> sys.stderr, 'Error on power off:'
+                print >> sys.stderr, err
+                raise CodeError('ERROR on remctl')
+    elif action == 'Shutdown':
+        out, err = remctl('control', machine.name, 'shutdown', err=True)
+        if err:
+            if re.match("Error: Domain '.*' does not exist.", err):
+                raise InvalidInput("action", "Shutdown", 
+                                   "Machine is not on.")
+            else:
+                print >> sys.stderr, 'Error on Shutdown:'
+                print >> sys.stderr, err
+                raise CodeError('ERROR on remctl')
+    elif action == 'Delete VM':
+        deleteVM(machine)
+    print >> sys.stderr, time.time()-start_time
+
+    d = dict(user=user,
+             command=action,
+             machine=machine)
+    return d
+
+def resizeDisk(machine_name, disk_name, new_size):
+    remctl("web", "lvresize", machine_name, disk_name, new_size)
+
+def renameMachine(machine, old_name, new_name):
+    for disk in machine.disks:
+        remctl("web", "lvrename", old_name, 
+               disk.guest_device_name, new_name)
+    remctl("web", "moveregister", old_name, new_name)
+    
Index: trunk/web/getafsgroups.py
===================================================================
--- trunk/web/getafsgroups.py	(revision 236)
+++ trunk/web/getafsgroups.py	(revision 236)
@@ -0,0 +1,96 @@
+#!/usr/bin/python
+import pprint
+import subprocess
+
+# import ldap
+# l = ldap.open("W92-130-LDAP-2.mit.edu")
+# # ldap.mit.edu is 1/2 broken right now so we're going to the working backend
+# l.simple_bind_s("", "")
+
+# def getLdapGroups(user):
+#     """
+#     getLdapGroups(user): returns a generator for the list of LDAP groups containing user
+#     """
+#     for user_data in l.search_s("ou=affiliates,dc=mit,dc=edu", ldap.SCOPE_ONELEVEL, "uid=" + user, []):
+#         for group_data in l.search_s("ou=groups,dc=mit,dc=edu", ldap.SCOPE_ONELEVEL, "uniqueMember="+user_data[0], ['cn']):
+#             yield group_data[1]['cn'][0]
+
+# def checkLdapGroups(user, group):
+#     """
+#     checkLdapGroups(user, group): returns True if and only if user is in LDAP group group
+#     """
+#     for result_data in l.search_s("ou=affiliates,dc=mit,dc=edu", ldap.SCOPE_ONELEVEL, "uid=" + user, []):
+#         if l.search_s("ou=groups,dc=mit,dc=edu", ldap.SCOPE_ONELEVEL, "(&(cn=" + group + ")(uniqueMember="+result_data[0] + "))", []) != []:
+#             return True
+#     return False
+
+class MyException(Exception):
+    pass
+
+def getAfsGroupMembers(group, cell):
+    p = subprocess.Popen(["pts", "membership", group, '-c', cell], 
+                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    if p.wait():
+        return []
+    return [line.strip() for line in p.stdout.readlines()[1:]]
+
+def checkAfsGroup(user, group, cell):
+    """
+    checkAfsGroup(user, group) returns True if and only if user is in AFS group group in cell cell
+    """
+    return user in getAfsGroupMembers(group, cell)
+
+def getCell(locker):
+    p = subprocess.Popen(["fs", "whichcell", "/mit/" + locker], 
+                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    if p.wait():
+        raise MyException(p.stderr.read())
+    return p.stdout.read().split()[-1][1:-1]
+
+def getLockerAcl(locker):
+    p = subprocess.Popen(["fs", "listacl", "/mit/" + locker], 
+                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    if p.wait():
+        raise MyException(p.stderr.read())
+    lines = p.stdout.readlines()
+    values = []
+    for line in lines[1:]:
+        fields = line.split()
+        if fields[0] == 'Negative':
+            break
+        if 'rlidwka' in fields[1]:
+            values.append(fields[0])
+    return values
+
+def notLockerOwner(user, locker):
+    """
+    notLockerOwner(user, locker) returns false if and only if user administers locker.
+
+    If the user does not own the locker, returns the string reason for
+    the failure.
+    """
+    try:
+        cell = getCell(locker)
+        values = getLockerAcl(locker)
+    except MyException, e:
+        return str(e)
+
+    for entry in values:
+        if entry[0] == user or (entry[0][0:6] == "system" and 
+                                checkAfsGroup(user, entry[0], cell)):
+            return False
+    return "You don't have admin bits on /mit/" + locker
+
+
+if __name__ == "__main__":
+#    print list(getldapgroups("tabbott"))
+    print checkAfsGroup("tabbott", "system:debathena", 'athena.mit.edu')
+    print checkAfsGroup("tabbott", "system:debathena", 'sipb.mit.edu')
+    print checkAfsGroup("tabbott", "system:debathena-root", 'athena.mit.edu')
+    print checkAfsGroup("tabbott", "system:hmmt-request", 'athena.mit.edu')
+    print notLockerOwner("tabbott", "tabbott")
+    print notLockerOwner("tabbott", "debathena")
+    print notLockerOwner("tabbott", "sipb")
+    print notLockerOwner("tabbott", "lsc")
+    print notLockerOwner("tabbott", "scripts")
+    print notLockerOwner("ecprice", "hmmt")
Index: trunk/web/main.py
===================================================================
--- trunk/web/main.py	(revision 236)
+++ trunk/web/main.py	(revision 236)
@@ -0,0 +1,643 @@
+#!/usr/bin/python
+"""Main CGI script for web interface"""
+
+import base64
+import cPickle
+import cgi
+import datetime
+import hmac
+import os
+import sha
+import simplejson
+import sys
+import time
+from StringIO import StringIO
+
+
+def revertStandardError():
+    """Move stderr to stdout, and return the contents of the old stderr."""
+    errio = sys.stderr
+    if not isinstance(errio, StringIO):
+        return None
+    sys.stderr = sys.stdout
+    errio.seek(0)
+    return errio.read()
+
+def printError():
+    """Revert stderr to stdout, and print the contents of stderr"""
+    if isinstance(sys.stderr, StringIO):
+        print revertStandardError()
+
+if __name__ == '__main__':
+    import atexit
+    atexit.register(printError)
+    sys.stderr = StringIO()
+
+sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
+
+import templates
+from Cheetah.Template import Template
+from sipb_xen_database import Machine, CDROM, ctx, connect
+import validation
+from webcommon import InvalidInput, CodeError, g
+import controls
+
+class Checkpoint:
+    def __init__(self):
+        self.start_time = time.time()
+        self.checkpoints = []
+
+    def checkpoint(self, s):
+        self.checkpoints.append((s, time.time()))
+
+    def __str__(self):
+        return ('Timing info:\n%s\n' %
+                '\n'.join(['%s: %s' % (d, t - self.start_time) for
+                           (d, t) in self.checkpoints]))
+
+checkpoint = Checkpoint()
+
+
+def helppopup(subj):
+    """Return HTML code for a (?) link to a specified help topic"""
+    return ('<span class="helplink"><a href="help?subject=' + subj + 
+            '&amp;simple=true" target="_blank" ' + 
+            'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
+
+def makeErrorPre(old, addition):
+    if addition is None:
+        return
+    if old:
+        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
+    else:
+        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
+
+Template.helppopup = staticmethod(helppopup)
+Template.err = None
+
+class JsonDict:
+    """Class to store a dictionary that will be converted to JSON"""
+    def __init__(self, **kws):
+        self.data = kws
+        if 'err' in kws:
+            err = kws['err']
+            del kws['err']
+            self.addError(err)
+
+    def __str__(self):
+        return simplejson.dumps(self.data)
+
+    def addError(self, text):
+        """Add stderr text to be displayed on the website."""
+        self.data['err'] = \
+            makeErrorPre(self.data.get('err'), text)
+
+class Defaults:
+    """Class to store default values for fields."""
+    memory = 256
+    disk = 4.0
+    cdrom = ''
+    name = ''
+    vmtype = 'hvm'
+    def __init__(self, max_memory=None, max_disk=None, **kws):
+        if max_memory is not None:
+            self.memory = min(self.memory, max_memory)
+        if max_disk is not None:
+            self.max_disk = min(self.disk, max_disk)
+        for key in kws:
+            setattr(self, key, kws[key])
+
+
+
+DEFAULT_HEADERS = {'Content-Type': 'text/html'}
+
+def error(op, user, fields, err, emsg):
+    """Print an error page when a CodeError occurs"""
+    d = dict(op=op, user=user, errorMessage=str(err),
+             stderr=emsg)
+    return templates.error(searchList=[d])
+
+def invalidInput(op, user, fields, err, emsg):
+    """Print an error page when an InvalidInput exception occurs"""
+    d = dict(op=op, user=user, err_field=err.err_field,
+             err_value=str(err.err_value), stderr=emsg,
+             errorMessage=str(err))
+    return templates.invalid(searchList=[d])
+
+def hasVnc(status):
+    """Does the machine with a given status list support VNC?"""
+    if status is None:
+        return False
+    for l in status:
+        if l[0] == 'device' and l[1][0] == 'vfb':
+            d = dict(l[1][1:])
+            return 'location' in d
+    return False
+
+def parseCreate(user, fields):
+    name = fields.getfirst('name')
+    if not validation.validMachineName(name):
+        raise InvalidInput('name', name, 'You must provide a machine name.')
+    name = name.lower()
+
+    if Machine.get_by(name=name):
+        raise InvalidInput('name', name,
+                           "Name already exists.")
+    
+    owner = validation.testOwner(user, fields.getfirst('owner'))
+
+    memory = fields.getfirst('memory')
+    memory = validation.validMemory(user, memory, on=True)
+    
+    disk = fields.getfirst('disk')
+    disk = validation.validDisk(user, disk)
+
+    vm_type = fields.getfirst('vmtype')
+    if vm_type not in ('hvm', 'paravm'):
+        raise CodeError("Invalid vm type '%s'"  % vm_type)    
+    is_hvm = (vm_type == 'hvm')
+
+    cdrom = fields.getfirst('cdrom')
+    if cdrom is not None and not CDROM.get(cdrom):
+        raise CodeError("Invalid cdrom type '%s'" % cdrom)
+    return dict(contact=user, name=name, memory=memory, disk=disk,
+                owner=owner, is_hvm=is_hvm, cdrom=cdrom)
+
+def create(user, fields):
+    """Handler for create requests."""
+    try:
+        parsed_fields = parseCreate(user, fields)
+        machine = controls.createVm(**parsed_fields)
+    except InvalidInput, err:
+        pass
+    else:
+        err = None
+    g.clear() #Changed global state
+    d = getListDict(user)
+    d['err'] = err
+    if err:
+        for field in fields.keys():
+            setattr(d['defaults'], field, fields.getfirst(field))
+    else:
+        d['new_machine'] = parsed_fields['name']
+    return templates.list(searchList=[d])
+
+
+def getListDict(user):
+    machines = [m for m in Machine.select() 
+                if validation.haveAccess(user, m)]    
+    checkpoint.checkpoint('Got my machines')
+    on = {}
+    has_vnc = {}
+    on = g.uptimes
+    checkpoint.checkpoint('Got uptimes')
+    for m in machines:
+        m.uptime = g.uptimes.get(m)
+        if not on[m]:
+            has_vnc[m] = 'Off'
+        elif m.type.hvm:
+            has_vnc[m] = True
+        else:
+            has_vnc[m] = "ParaVM"+helppopup("paravm_console")
+    max_memory = validation.maxMemory(user)
+    max_disk = validation.maxDisk(user)
+    checkpoint.checkpoint('Got max mem/disk')
+    defaults = Defaults(max_memory=max_memory,
+                        max_disk=max_disk,
+                        owner=user,
+                        cdrom='gutsy-i386')
+    checkpoint.checkpoint('Got defaults')
+    d = dict(user=user,
+             cant_add_vm=validation.cantAddVm(user),
+             max_memory=max_memory,
+             max_disk=max_disk,
+             defaults=defaults,
+             machines=machines,
+             has_vnc=has_vnc,
+             uptimes=g.uptimes,
+             cdroms=CDROM.select())
+    return d
+
+def listVms(user, fields):
+    """Handler for list requests."""
+    checkpoint.checkpoint('Getting list dict')
+    d = getListDict(user)
+    checkpoint.checkpoint('Got list dict')
+    return templates.list(searchList=[d])
+            
+def vnc(user, fields):
+    """VNC applet page.
+
+    Note that due to same-domain restrictions, the applet connects to
+    the webserver, which needs to forward those requests to the xen
+    server.  The Xen server runs another proxy that (1) authenticates
+    and (2) finds the correct port for the VM.
+
+    You might want iptables like:
+
+    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
+      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 
+    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
+      --dport 10003 -j SNAT --to-source 18.187.7.142 
+    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
+      --dport 10003 -j ACCEPT
+
+    Remember to enable iptables!
+    echo 1 > /proc/sys/net/ipv4/ip_forward
+    """
+    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
+    
+    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
+
+    data = {}
+    data["user"] = user
+    data["machine"] = machine.name
+    data["expires"] = time.time()+(5*60)
+    pickled_data = cPickle.dumps(data)
+    m = hmac.new(TOKEN_KEY, digestmod=sha)
+    m.update(pickled_data)
+    token = {'data': pickled_data, 'digest': m.digest()}
+    token = cPickle.dumps(token)
+    token = base64.urlsafe_b64encode(token)
+    
+    status = controls.statusInfo(machine)
+    has_vnc = hasVnc(status)
+    
+    d = dict(user=user,
+             on=status,
+             has_vnc=has_vnc,
+             machine=machine,
+             hostname=os.environ.get('SERVER_NAME', 'localhost'),
+             authtoken=token)
+    return templates.vnc(searchList=[d])
+
+def getNicInfo(data_dict, machine):
+    """Helper function for info, get data on nics for a machine.
+
+    Modifies data_dict to include the relevant data, and returns a list
+    of (key, name) pairs to display "name: data_dict[key]" to the user.
+    """
+    data_dict['num_nics'] = len(machine.nics)
+    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
+                           ('nic%s_mac', 'NIC %s MAC Addr'),
+                           ('nic%s_ip', 'NIC %s IP'),
+                           ]
+    nic_fields = []
+    for i in range(len(machine.nics)):
+        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
+        if not i:
+            data_dict['nic%s_hostname' % i] = (machine.name + 
+                                               '.servers.csail.mit.edu')
+        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
+        data_dict['nic%s_ip' % i] = machine.nics[i].ip
+    if len(machine.nics) == 1:
+        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
+    return nic_fields
+
+def getDiskInfo(data_dict, machine):
+    """Helper function for info, get data on disks for a machine.
+
+    Modifies data_dict to include the relevant data, and returns a list
+    of (key, name) pairs to display "name: data_dict[key]" to the user.
+    """
+    data_dict['num_disks'] = len(machine.disks)
+    disk_fields_template = [('%s_size', '%s size')]
+    disk_fields = []
+    for disk in machine.disks:
+        name = disk.guest_device_name
+        disk_fields.extend([(x % name, y % name) for x, y in 
+                            disk_fields_template])
+        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
+    return disk_fields
+
+def command(user, fields):
+    """Handler for running commands like boot and delete on a VM."""
+    back = fields.getfirst('back')
+    try:
+        d = controls.commandResult(user, fields)
+        if d['command'] == 'Delete VM':
+            back = 'list'
+    except InvalidInput, err:
+        if not back:
+            raise
+        print >> sys.stderr, err
+        result = None
+    else:
+        result = 'Success!'
+        if not back:
+            return templates.command(searchList=[d])
+    if back == 'list':
+        g.clear() #Changed global state
+        d = getListDict(user)
+        d['result'] = result
+        return templates.list(searchList=[d])
+    elif back == 'info':
+        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
+        d = infoDict(user, machine)
+        d['result'] = result
+        return templates.info(searchList=[d])
+    else:
+        raise InvalidInput
+    ('back', back, 'Not a known back page.')
+
+def modifyDict(user, fields):
+    olddisk = {}
+    transaction = ctx.current.create_transaction()
+    try:
+        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
+        owner = validation.testOwner(user, fields.getfirst('owner'), machine)
+        admin = validation.testAdmin(user, fields.getfirst('administrator'),
+                                     machine)
+        contact = validation.testContact(user, fields.getfirst('contact'),
+                                         machine)
+        name = validation.testName(user, fields.getfirst('name'), machine)
+        oldname = machine.name
+        command = "modify"
+
+        memory = fields.getfirst('memory')
+        if memory is not None:
+            memory = validation.validMemory(user, memory, machine, on=False)
+            machine.memory = memory
+ 
+        disksize = validation.testDisk(user, fields.getfirst('disk'))
+        if disksize is not None:
+            disksize = validation.validDisk(user, disksize, machine)
+            disk = machine.disks[0]
+            if disk.size != disksize:
+                olddisk[disk.guest_device_name] = disksize
+                disk.size = disksize
+                ctx.current.save(disk)
+        
+        if owner is not None:
+            machine.owner = owner
+        if name is not None:
+            machine.name = name
+        if admin is not None:
+            machine.administrator = admin
+        if contact is not None:
+            machine.contact = contact
+            
+        ctx.current.save(machine)
+        transaction.commit()
+    except:
+        transaction.rollback()
+        raise
+    for diskname in olddisk:
+        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
+    if name is not None:
+        controls.renameMachine(machine, oldname, name)
+    return dict(user=user,
+                command=command,
+                machine=machine)
+    
+def modify(user, fields):
+    """Handler for modifying attributes of a machine."""
+    try:
+        modify_dict = modifyDict(user, fields)
+    except InvalidInput, err:
+        result = None
+        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
+    else:
+        machine = modify_dict['machine']
+        result = 'Success!'
+        err = None
+    info_dict = infoDict(user, machine)
+    info_dict['err'] = err
+    if err:
+        for field in fields.keys():
+            setattr(info_dict['defaults'], field, fields.getfirst(field))
+    info_dict['result'] = result
+    return templates.info(searchList=[info_dict])
+    
+
+def helpHandler(user, fields):
+    """Handler for help messages."""
+    simple = fields.getfirst('simple')
+    subjects = fields.getlist('subject')
+    
+    help_mapping = dict(paravm_console="""
+ParaVM machines do not support console access over VNC.  To access
+these machines, you either need to boot with a liveCD and ssh in or
+hope that the sipb-xen maintainers add support for serial consoles.""",
+                        hvm_paravm="""
+HVM machines use the virtualization features of the processor, while
+ParaVM machines use Xen's emulation of virtualization features.  You
+want an HVM virtualized machine.""",
+                        cpu_weight="""
+Don't ask us!  We're as mystified as you are.""",
+                        owner="""
+The owner field is used to determine <a
+href="help?subject=quotas">quotas</a>.  It must be the name of a
+locker that you are an AFS administrator of.  In particular, you or an
+AFS group you are a member of must have AFS rlidwka bits on the
+locker.  You can check see who administers the LOCKER locker using the
+command 'fs la /mit/LOCKER' on Athena.)  See also <a
+href="help?subject=administrator">administrator</a>.""",
+                        administrator="""
+The administrator field determines who can access the console and
+power on and off the machine.  This can be either a user or a moira
+group.""",
+                        quotas="""
+Quotas are determined on a per-locker basis.  Each quota may have a
+maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
+active machines."""
+                   )
+    
+    if not subjects:
+        subjects = sorted(help_mapping.keys())
+        
+    d = dict(user=user,
+             simple=simple,
+             subjects=subjects,
+             mapping=help_mapping)
+    
+    return templates.help(searchList=[d])
+    
+
+def badOperation(u, e):
+    raise CodeError("Unknown operation")
+
+def infoDict(user, machine):
+    status = controls.statusInfo(machine)
+    checkpoint.checkpoint('Getting status info')
+    has_vnc = hasVnc(status)
+    if status is None:
+        main_status = dict(name=machine.name,
+                           memory=str(machine.memory))
+        uptime = None
+        cputime = None
+    else:
+        main_status = dict(status[1:])
+        start_time = float(main_status.get('start_time', 0))
+        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
+        cpu_time_float = float(main_status.get('cpu_time', 0))
+        cputime = datetime.timedelta(seconds=int(cpu_time_float))
+    checkpoint.checkpoint('Status')
+    display_fields = """name uptime memory state cpu_weight on_reboot 
+     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
+    display_fields = [('name', 'Name'),
+                      ('owner', 'Owner'),
+                      ('administrator', 'Administrator'),
+                      ('contact', 'Contact'),
+                      ('type', 'Type'),
+                      'NIC_INFO',
+                      ('uptime', 'uptime'),
+                      ('cputime', 'CPU usage'),
+                      ('memory', 'RAM'),
+                      'DISK_INFO',
+                      ('state', 'state (xen format)'),
+                      ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
+                      ('on_reboot', 'Action on VM reboot'),
+                      ('on_poweroff', 'Action on VM poweroff'),
+                      ('on_crash', 'Action on VM crash'),
+                      ('on_xend_start', 'Action on Xen start'),
+                      ('on_xend_stop', 'Action on Xen stop'),
+                      ('bootloader', 'Bootloader options'),
+                      ]
+    fields = []
+    machine_info = {}
+    machine_info['name'] = machine.name
+    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
+    machine_info['owner'] = machine.owner
+    machine_info['administrator'] = machine.administrator
+    machine_info['contact'] = machine.contact
+
+    nic_fields = getNicInfo(machine_info, machine)
+    nic_point = display_fields.index('NIC_INFO')
+    display_fields = (display_fields[:nic_point] + nic_fields + 
+                      display_fields[nic_point+1:])
+
+    disk_fields = getDiskInfo(machine_info, machine)
+    disk_point = display_fields.index('DISK_INFO')
+    display_fields = (display_fields[:disk_point] + disk_fields + 
+                      display_fields[disk_point+1:])
+    
+    main_status['memory'] += ' MiB'
+    for field, disp in display_fields:
+        if field in ('uptime', 'cputime') and locals()[field] is not None:
+            fields.append((disp, locals()[field]))
+        elif field in machine_info:
+            fields.append((disp, machine_info[field]))
+        elif field in main_status:
+            fields.append((disp, main_status[field]))
+        else:
+            pass
+            #fields.append((disp, None))
+
+    checkpoint.checkpoint('Got fields')
+
+
+    max_mem = validation.maxMemory(user, machine, False)
+    checkpoint.checkpoint('Got mem')
+    max_disk = validation.maxDisk(user, machine)
+    defaults = Defaults()
+    for name in 'machine_id name administrator owner memory contact'.split():
+        setattr(defaults, name, getattr(machine, name))
+    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
+    checkpoint.checkpoint('Got defaults')
+    d = dict(user=user,
+             cdroms=CDROM.select(),
+             on=status is not None,
+             machine=machine,
+             defaults=defaults,
+             has_vnc=has_vnc,
+             uptime=str(uptime),
+             ram=machine.memory,
+             max_mem=max_mem,
+             max_disk=max_disk,
+             owner_help=helppopup("owner"),
+             fields = fields)
+    return d
+
+def info(user, fields):
+    """Handler for info on a single VM."""
+    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
+    d = infoDict(user, machine)
+    checkpoint.checkpoint('Got infodict')
+    return templates.info(searchList=[d])
+
+mapping = dict(list=listVms,
+               vnc=vnc,
+               command=command,
+               modify=modify,
+               info=info,
+               create=create,
+               help=helpHandler)
+
+def printHeaders(headers):
+    for key, value in headers.iteritems():
+        print '%s: %s' % (key, value)
+    print
+
+
+def getUser():
+    """Return the current user based on the SSL environment variables"""
+    if 'SSL_CLIENT_S_DN_Email' in os.environ:
+        username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
+        return username
+    else:
+        return 'moo'
+
+def main(operation, user, fields):    
+    start_time = time.time()
+    fun = mapping.get(operation, badOperation)
+
+    if fun not in (helpHandler, ):
+        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
+    try:
+        checkpoint.checkpoint('Before')
+        output = fun(u, fields)
+        checkpoint.checkpoint('After')
+
+        headers = dict(DEFAULT_HEADERS)
+        if isinstance(output, tuple):
+            new_headers, output = output
+            headers.update(new_headers)
+        e = revertStandardError()
+        if e:
+            output.addError(e)
+        printHeaders(headers)
+        output_string =  str(output)
+        checkpoint.checkpoint('output as a string')
+        print output_string
+        print '<pre>%s</pre>' % checkpoint
+    except Exception, err:
+        if not fields.has_key('js'):
+            if isinstance(err, CodeError):
+                print 'Content-Type: text/html\n'
+                e = revertStandardError()
+                print error(operation, u, fields, err, e)
+                sys.exit(1)
+            if isinstance(err, InvalidInput):
+                print 'Content-Type: text/html\n'
+                e = revertStandardError()
+                print invalidInput(operation, u, fields, err, e)
+                sys.exit(1)
+        print 'Content-Type: text/plain\n'
+        print 'Uh-oh!  We experienced an error.'
+        print 'Please email sipb-xen@mit.edu with the contents of this page.'
+        print '----'
+        e = revertStandardError()
+        print e
+        print '----'
+        raise
+
+if __name__ == '__main__':
+    fields = cgi.FieldStorage()
+    u = getUser()
+    g.user = u
+    operation = os.environ.get('PATH_INFO', '')
+    if not operation:
+        print "Status: 301 Moved Permanently"
+        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
+        sys.exit(0)
+
+    if operation.startswith('/'):
+        operation = operation[1:]
+    if not operation:
+        operation = 'list'
+
+    #main(operation, u, fields)
+    import profile
+    profile.run('main(operation, u, fields)', 'log-'+operation)
+
Index: trunk/web/static/VncViewer.jar
===================================================================
--- trunk/web/static/VncViewer.jar	(revision 236)
+++ trunk/web/static/VncViewer.jar	(revision 236)
@@ -0,0 +1,1 @@
+link ../../../vnc/vnc_javasrc/VncViewer.jar
Index: trunk/web/static/layout.css
===================================================================
--- trunk/web/static/layout.css	(revision 236)
+++ trunk/web/static/layout.css	(revision 236)
@@ -0,0 +1,3 @@
+html,body {
+	height: 100%;
+}
Index: trunk/web/static/prototype.js
===================================================================
--- trunk/web/static/prototype.js	(revision 236)
+++ trunk/web/static/prototype.js	(revision 236)
@@ -0,0 +1,3277 @@
+/*  Prototype JavaScript framework, version 1.5.1.1
+ *  (c) 2005-2007 Sam Stephenson
+ *
+ *  Prototype is freely distributable under the terms of an MIT-style license.
+ *  For details, see the Prototype web site: http://www.prototypejs.org/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+  Version: '1.5.1.1',
+
+  Browser: {
+    IE:     !!(window.attachEvent && !window.opera),
+    Opera:  !!window.opera,
+    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1
+  },
+
+  BrowserFeatures: {
+    XPath: !!document.evaluate,
+    ElementExtensions: !!window.HTMLElement,
+    SpecificElementExtensions:
+      (document.createElement('div').__proto__ !==
+       document.createElement('form').__proto__)
+  },
+
+  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+  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 (var property in source) {
+    destination[property] = source[property];
+  }
+  return destination;
+}
+
+Object.extend(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;
+    }
+  },
+
+  toJSON: function(object) {
+    var type = typeof object;
+    switch(type) {
+      case 'undefined':
+      case 'function':
+      case 'unknown': return;
+      case 'boolean': return object.toString();
+    }
+    if (object === null) return 'null';
+    if (object.toJSON) return object.toJSON();
+    if (object.ownerDocument === document) return;
+    var results = [];
+    for (var property in object) {
+      var value = Object.toJSON(object[property]);
+      if (value !== undefined)
+        results.push(property.toJSON() + ': ' + value);
+    }
+    return '{' + results.join(', ') + '}';
+  },
+
+  keys: function(object) {
+    var keys = [];
+    for (var property in object)
+      keys.push(property);
+    return keys;
+  },
+
+  values: function(object) {
+    var values = [];
+    for (var property in object)
+      values.push(object[property]);
+    return values;
+  },
+
+  clone: function(object) {
+    return Object.extend({}, object);
+  }
+});
+
+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, args = $A(arguments), object = args.shift();
+  return function(event) {
+    return __method.apply(object, [event || window.event].concat(args));
+  }
+}
+
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    return this.toPaddedString(2, 16);
+  },
+
+  succ: function() {
+    return this + 1;
+  },
+
+  times: function(iterator) {
+    $R(0, this, true).each(iterator);
+    return this;
+  },
+
+  toPaddedString: function(length, radix) {
+    var string = this.toString(radix || 10);
+    return '0'.times(length - string.length) + string;
+  },
+
+  toJSON: function() {
+    return isFinite(this) ? this.toString() : 'null';
+  }
+});
+
+Date.prototype.toJSON = function() {
+  return '"' + this.getFullYear() + '-' +
+    (this.getMonth() + 1).toPaddedString(2) + '-' +
+    this.getDate().toPaddedString(2) + 'T' +
+    this.getHours().toPaddedString(2) + ':' +
+    this.getMinutes().toPaddedString(2) + ':' +
+    this.getSeconds().toPaddedString(2) + '"';
+};
+
+var Try = {
+  these: function() {
+    var returnValue;
+
+    for (var i = 0, length = arguments.length; i < 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() {
+    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  stop: function() {
+    if (!this.timer) return;
+    clearInterval(this.timer);
+    this.timer = null;
+  },
+
+  onTimerEvent: function() {
+    if (!this.currentlyExecuting) {
+      try {
+        this.currentlyExecuting = true;
+        this.callback(this);
+      } finally {
+        this.currentlyExecuting = false;
+      }
+    }
+  }
+}
+Object.extend(String, {
+  interpret: function(value) {
+    return value == null ? '' : String(value);
+  },
+  specialChar: {
+    '\b': '\\b',
+    '\t': '\\t',
+    '\n': '\\n',
+    '\f': '\\f',
+    '\r': '\\r',
+    '\\': '\\\\'
+  }
+});
+
+Object.extend(String.prototype, {
+  gsub: function(pattern, replacement) {
+    var result = '', source = this, match;
+    replacement = arguments.callee.prepareReplacement(replacement);
+
+    while (source.length > 0) {
+      if (match = source.match(pattern)) {
+        result += source.slice(0, match.index);
+        result += String.interpret(replacement(match));
+        source  = source.slice(match.index + match[0].length);
+      } else {
+        result += source, source = '';
+      }
+    }
+    return result;
+  },
+
+  sub: function(pattern, replacement, count) {
+    replacement = this.gsub.prepareReplacement(replacement);
+    count = count === undefined ? 1 : count;
+
+    return this.gsub(pattern, function(match) {
+      if (--count < 0) return match[0];
+      return replacement(match);
+    });
+  },
+
+  scan: function(pattern, iterator) {
+    this.gsub(pattern, iterator);
+    return this;
+  },
+
+  truncate: function(length, truncation) {
+    length = length || 30;
+    truncation = truncation === undefined ? '...' : truncation;
+    return this.length > length ?
+      this.slice(0, length - truncation.length) + truncation : this;
+  },
+
+  strip: function() {
+    return this.replace(/^\s+/, '').replace(/\s+$/, '');
+  },
+
+  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(function(script) { return eval(script) });
+  },
+
+  escapeHTML: function() {
+    var self = arguments.callee;
+    self.text.data = this;
+    return self.div.innerHTML;
+  },
+
+  unescapeHTML: function() {
+    var div = document.createElement('div');
+    div.innerHTML = this.stripTags();
+    return div.childNodes[0] ? (div.childNodes.length > 1 ?
+      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
+      div.childNodes[0].nodeValue) : '';
+  },
+
+  toQueryParams: function(separator) {
+    var match = this.strip().match(/([^?#]*)(#.*)?$/);
+    if (!match) return {};
+
+    return match[1].split(separator || '&').inject({}, function(hash, pair) {
+      if ((pair = pair.split('='))[0]) {
+        var key = decodeURIComponent(pair.shift());
+        var value = pair.length > 1 ? pair.join('=') : pair[0];
+        if (value != undefined) value = decodeURIComponent(value);
+
+        if (key in hash) {
+          if (hash[key].constructor != Array) hash[key] = [hash[key]];
+          hash[key].push(value);
+        }
+        else hash[key] = value;
+      }
+      return hash;
+    });
+  },
+
+  toArray: function() {
+    return this.split('');
+  },
+
+  succ: function() {
+    return this.slice(0, this.length - 1) +
+      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+  },
+
+  times: function(count) {
+    var result = '';
+    for (var i = 0; i < count; i++) result += this;
+    return result;
+  },
+
+  camelize: function() {
+    var parts = this.split('-'), len = parts.length;
+    if (len == 1) return parts[0];
+
+    var camelized = this.charAt(0) == '-'
+      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+      : parts[0];
+
+    for (var i = 1; i < len; i++)
+      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+    return camelized;
+  },
+
+  capitalize: function() {
+    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+  },
+
+  underscore: function() {
+    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+  },
+
+  dasherize: function() {
+    return this.gsub(/_/,'-');
+  },
+
+  inspect: function(useDoubleQuotes) {
+    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+      var character = String.specialChar[match[0]];
+      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+    });
+    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+  },
+
+  toJSON: function() {
+    return this.inspect(true);
+  },
+
+  unfilterJSON: function(filter) {
+    return this.sub(filter || Prototype.JSONFilter, '#{1}');
+  },
+
+  isJSON: function() {
+    var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+  },
+
+  evalJSON: function(sanitize) {
+    var json = this.unfilterJSON();
+    try {
+      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+    } catch (e) { }
+    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+  },
+
+  include: function(pattern) {
+    return this.indexOf(pattern) > -1;
+  },
+
+  startsWith: function(pattern) {
+    return this.indexOf(pattern) === 0;
+  },
+
+  endsWith: function(pattern) {
+    var d = this.length - pattern.length;
+    return d >= 0 && this.lastIndexOf(pattern) === d;
+  },
+
+  empty: function() {
+    return this == '';
+  },
+
+  blank: function() {
+    return /^\s*$/.test(this);
+  }
+});
+
+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
+  escapeHTML: function() {
+    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+  },
+  unescapeHTML: function() {
+    return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
+  }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+  if (typeof replacement == 'function') return replacement;
+  var template = new Template(replacement);
+  return function(match) { return template.evaluate(match) };
+}
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+Object.extend(String.prototype.escapeHTML, {
+  div:  document.createElement('div'),
+  text: document.createTextNode('')
+});
+
+with (String.prototype.escapeHTML) div.appendChild(text);
+
+var Template = Class.create();
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+Template.prototype = {
+  initialize: function(template, pattern) {
+    this.template = template.toString();
+    this.pattern  = pattern || Template.Pattern;
+  },
+
+  evaluate: function(object) {
+    return this.template.gsub(this.pattern, function(match) {
+      var before = match[1];
+      if (before == '\\') return match[2];
+      return before + String.interpret(object[match[3]]);
+    });
+  }
+}
+
+var $break = {}, $continue = new Error('"throw $continue" is deprecated, use "return" instead');
+
+var Enumerable = {
+  each: function(iterator) {
+    var index = 0;
+    try {
+      this._each(function(value) {
+        iterator(value, index++);
+      });
+    } catch (e) {
+      if (e != $break) throw e;
+    }
+    return this;
+  },
+
+  eachSlice: function(number, iterator) {
+    var index = -number, slices = [], array = this.toArray();
+    while ((index += number) < array.length)
+      slices.push(array.slice(index, index+number));
+    return slices.map(iterator);
+  },
+
+  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 = false;
+    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 || Prototype.K)(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;
+  },
+
+  inGroupsOf: function(number, fillWith) {
+    fillWith = fillWith === undefined ? null : fillWith;
+    return this.eachSlice(number, function(slice) {
+      while(slice.length < number) slice.push(fillWith);
+      return slice;
+    });
+  },
+
+  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.map(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 (result == undefined || value >= result)
+        result = value;
+    });
+    return result;
+  },
+
+  min: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (result == undefined || value < result)
+        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.map(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.map();
+  },
+
+  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) {
+      return iterator(collections.pluck(index));
+    });
+  },
+
+  size: function() {
+    return this.toArray().length;
+  },
+
+  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, length = iterable.length; i < length; i++)
+      results.push(iterable[i]);
+    return results;
+  }
+}
+
+if (Prototype.Browser.WebKit) {
+  $A = Array.from = function(iterable) {
+    if (!iterable) return [];
+    if (!(typeof iterable == 'function' && iterable == '[object NodeList]') &&
+      iterable.toArray) {
+      return iterable.toArray();
+    } else {
+      var results = [];
+      for (var i = 0, length = iterable.length; i < length; i++)
+        results.push(iterable[i]);
+      return results;
+    }
+  }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse)
+  Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+  _each: function(iterator) {
+    for (var i = 0, length = this.length; i < 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 != null;
+    });
+  },
+
+  flatten: function() {
+    return this.inject([], function(array, value) {
+      return array.concat(value && 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, length = this.length; i < length; i++)
+      if (this[i] == object) return i;
+    return -1;
+  },
+
+  reverse: function(inline) {
+    return (inline !== false ? this : this.toArray())._reverse();
+  },
+
+  reduce: function() {
+    return this.length > 1 ? this : this[0];
+  },
+
+  uniq: function(sorted) {
+    return this.inject([], function(array, value, index) {
+      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+        array.push(value);
+      return array;
+    });
+  },
+
+  clone: function() {
+    return [].concat(this);
+  },
+
+  size: function() {
+    return this.length;
+  },
+
+  inspect: function() {
+    return '[' + this.map(Object.inspect).join(', ') + ']';
+  },
+
+  toJSON: function() {
+    var results = [];
+    this.each(function(object) {
+      var value = Object.toJSON(object);
+      if (value !== undefined) results.push(value);
+    });
+    return '[' + results.join(', ') + ']';
+  }
+});
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string) {
+  string = string.strip();
+  return string ? string.split(/\s+/) : [];
+}
+
+if (Prototype.Browser.Opera){
+  Array.prototype.concat = function() {
+    var array = [];
+    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+    for (var i = 0, length = arguments.length; i < length; i++) {
+      if (arguments[i].constructor == Array) {
+        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+          array.push(arguments[i][j]);
+      } else {
+        array.push(arguments[i]);
+      }
+    }
+    return array;
+  }
+}
+var Hash = function(object) {
+  if (object instanceof Hash) this.merge(object);
+  else Object.extend(this, object || {});
+};
+
+Object.extend(Hash, {
+  toQueryString: function(obj) {
+    var parts = [];
+    parts.add = arguments.callee.addPair;
+
+    this.prototype._each.call(obj, function(pair) {
+      if (!pair.key) return;
+      var value = pair.value;
+
+      if (value && typeof value == 'object') {
+        if (value.constructor == Array) value.each(function(value) {
+          parts.add(pair.key, value);
+        });
+        return;
+      }
+      parts.add(pair.key, value);
+    });
+
+    return parts.join('&');
+  },
+
+  toJSON: function(object) {
+    var results = [];
+    this.prototype._each.call(object, function(pair) {
+      var value = Object.toJSON(pair.value);
+      if (value !== undefined) results.push(pair.key.toJSON() + ': ' + value);
+    });
+    return '{' + results.join(', ') + '}';
+  }
+});
+
+Hash.toQueryString.addPair = function(key, value, prefix) {
+  key = encodeURIComponent(key);
+  if (value === undefined) this.push(key);
+  else this.push(key + '=' + (value == null ? '' : encodeURIComponent(value)));
+}
+
+Object.extend(Hash.prototype, Enumerable);
+Object.extend(Hash.prototype, {
+  _each: function(iterator) {
+    for (var key in this) {
+      var value = this[key];
+      if (value && value == Hash.prototype[key]) 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(this, function(mergedHash, pair) {
+      mergedHash[pair.key] = pair.value;
+      return mergedHash;
+    });
+  },
+
+  remove: function() {
+    var result;
+    for(var i = 0, length = arguments.length; i < length; i++) {
+      var value = this[arguments[i]];
+      if (value !== undefined){
+        if (result === undefined) result = value;
+        else {
+          if (result.constructor != Array) result = [result];
+          result.push(value)
+        }
+      }
+      delete this[arguments[i]];
+    }
+    return result;
+  },
+
+  toQueryString: function() {
+    return Hash.toQueryString(this);
+  },
+
+  inspect: function() {
+    return '#<Hash:{' + this.map(function(pair) {
+      return pair.map(Object.inspect).join(': ');
+    }).join(', ') + '}>';
+  },
+
+  toJSON: function() {
+    return Hash.toJSON(this);
+  }
+});
+
+function $H(object) {
+  if (object instanceof Hash) return object;
+  return new Hash(object);
+};
+
+// Safari iterates over shadowed properties
+if (function() {
+  var i = 0, Test = function(value) { this.key = value };
+  Test.prototype.key = 'foo';
+  for (var property in new Test('bar')) i++;
+  return i > 1;
+}()) Hash.prototype._each = function(iterator) {
+  var cache = [];
+  for (var key in this) {
+    var value = this[key];
+    if ((value && value == Hash.prototype[key]) || cache.include(key)) continue;
+    cache.push(key);
+    var pair = [key, value];
+    pair.key = key;
+    pair.value = value;
+    iterator(pair);
+  }
+};
+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;
+    while (this.include(value)) {
+      iterator(value);
+      value = value.succ();
+    }
+  },
+
+  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 XMLHttpRequest()},
+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+    ) || false;
+  },
+
+  activeRequestCount: 0
+}
+
+Ajax.Responders = {
+  responders: [],
+
+  _each: function(iterator) {
+    this.responders._each(iterator);
+  },
+
+  register: function(responder) {
+    if (!this.include(responder))
+      this.responders.push(responder);
+  },
+
+  unregister: function(responder) {
+    this.responders = this.responders.without(responder);
+  },
+
+  dispatch: function(callback, request, transport, json) {
+    this.each(function(responder) {
+      if (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,
+      contentType:  'application/x-www-form-urlencoded',
+      encoding:     'UTF-8',
+      parameters:   ''
+    }
+    Object.extend(this.options, options || {});
+
+    this.options.method = this.options.method.toLowerCase();
+    if (typeof this.options.parameters == 'string')
+      this.options.parameters = this.options.parameters.toQueryParams();
+  }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+  _complete: false,
+
+  initialize: function(url, options) {
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+    this.request(url);
+  },
+
+  request: function(url) {
+    this.url = url;
+    this.method = this.options.method;
+    var params = Object.clone(this.options.parameters);
+
+    if (!['get', 'post'].include(this.method)) {
+      // simulate other verbs over post
+      params['_method'] = this.method;
+      this.method = 'post';
+    }
+
+    this.parameters = params;
+
+    if (params = Hash.toQueryString(params)) {
+      // when GET, append parameters to URL
+      if (this.method == 'get')
+        this.url += (this.url.include('?') ? '&' : '?') + params;
+      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+        params += '&_=';
+    }
+
+    try {
+      if (this.options.onCreate) this.options.onCreate(this.transport);
+      Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+      this.transport.open(this.method.toUpperCase(), this.url,
+        this.options.asynchronous);
+
+      if (this.options.asynchronous)
+        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
+
+      this.transport.onreadystatechange = this.onStateChange.bind(this);
+      this.setRequestHeaders();
+
+      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+      this.transport.send(this.body);
+
+      /* Force Firefox to handle ready state 4 for synchronous requests */
+      if (!this.options.asynchronous && this.transport.overrideMimeType)
+        this.onStateChange();
+
+    }
+    catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState > 1 && !((readyState == 4) && this._complete))
+      this.respondToReadyState(this.transport.readyState);
+  },
+
+  setRequestHeaders: function() {
+    var headers = {
+      'X-Requested-With': 'XMLHttpRequest',
+      'X-Prototype-Version': Prototype.Version,
+      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+    };
+
+    if (this.method == 'post') {
+      headers['Content-type'] = this.options.contentType +
+        (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+      /* Force "Connection: close" for older Mozilla browsers to work
+       * around a bug where XMLHttpRequest sends an incorrect
+       * Content-length header. See Mozilla Bugzilla #246651.
+       */
+      if (this.transport.overrideMimeType &&
+          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+            headers['Connection'] = 'close';
+    }
+
+    // user-defined headers
+    if (typeof this.options.requestHeaders == 'object') {
+      var extras = this.options.requestHeaders;
+
+      if (typeof extras.push == 'function')
+        for (var i = 0, length = extras.length; i < length; i += 2)
+          headers[extras[i]] = extras[i+1];
+      else
+        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+    }
+
+    for (var name in headers)
+      this.transport.setRequestHeader(name, headers[name]);
+  },
+
+  success: function() {
+    return !this.transport.status
+        || (this.transport.status >= 200 && this.transport.status < 300);
+  },
+
+  respondToReadyState: function(readyState) {
+    var state = Ajax.Request.Events[readyState];
+    var transport = this.transport, json = this.evalJSON();
+
+    if (state == 'Complete') {
+      try {
+        this._complete = true;
+        (this.options['on' + this.transport.status]
+         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(transport, json);
+      } catch (e) {
+        this.dispatchException(e);
+      }
+
+      var contentType = this.getHeader('Content-type');
+      if (contentType && contentType.strip().
+        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
+          this.evalResponse();
+    }
+
+    try {
+      (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
+      Ajax.Responders.dispatch('on' + state, this, transport, json);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+
+    if (state == 'Complete') {
+      // avoid memory leak in MSIE: clean up
+      this.transport.onreadystatechange = Prototype.emptyFunction;
+    }
+  },
+
+  getHeader: function(name) {
+    try {
+      return this.transport.getResponseHeader(name);
+    } catch (e) { return null }
+  },
+
+  evalJSON: function() {
+    try {
+      var json = this.getHeader('X-JSON');
+      return json ? json.evalJSON() : null;
+    } catch (e) { return null }
+  },
+
+  evalResponse: function() {
+    try {
+      return eval((this.transport.responseText || '').unfilterJSON());
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  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.container = {
+      success: (container.success || 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, param) {
+      this.updateContent();
+      onComplete(transport, param);
+    }).bind(this);
+
+    this.request(url);
+  },
+
+  updateContent: function() {
+    var receiver = this.container[this.success() ? 'success' : 'failure'];
+    var response = this.transport.responseText;
+
+    if (!this.options.evalScripts) response = response.stripScripts();
+
+    if (receiver = $(receiver)) {
+      if (this.options.insertion)
+        new this.options.insertion(receiver, response);
+      else
+        receiver.update(response);
+    }
+
+    if (this.success()) {
+      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.options.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);
+  }
+});
+function $(element) {
+  if (arguments.length > 1) {
+    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+      elements.push($(arguments[i]));
+    return elements;
+  }
+  if (typeof element == 'string')
+    element = document.getElementById(element);
+  return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+  document._getElementsByXPath = function(expression, parentElement) {
+    var results = [];
+    var query = document.evaluate(expression, $(parentElement) || document,
+      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+    for (var i = 0, length = query.snapshotLength; i < length; i++)
+      results.push(query.snapshotItem(i));
+    return results;
+  };
+
+  document.getElementsByClassName = function(className, parentElement) {
+    var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
+    return document._getElementsByXPath(q, parentElement);
+  }
+
+} else document.getElementsByClassName = function(className, parentElement) {
+  var children = ($(parentElement) || document.body).getElementsByTagName('*');
+  var elements = [], child, pattern = new RegExp("(^|\\s)" + className + "(\\s|$)");
+  for (var i = 0, length = children.length; i < length; i++) {
+    child = children[i];
+    var elementClassName = child.className;
+    if (elementClassName.length == 0) continue;
+    if (elementClassName == className || elementClassName.match(pattern))
+      elements.push(Element.extend(child));
+  }
+  return elements;
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element) var Element = {};
+
+Element.extend = function(element) {
+  var F = Prototype.BrowserFeatures;
+  if (!element || !element.tagName || element.nodeType == 3 ||
+   element._extended || F.SpecificElementExtensions || element == window)
+    return element;
+
+  var methods = {}, tagName = element.tagName, cache = Element.extend.cache,
+   T = Element.Methods.ByTag;
+
+  // extend methods for all tags (Safari doesn't need this)
+  if (!F.ElementExtensions) {
+    Object.extend(methods, Element.Methods),
+    Object.extend(methods, Element.Methods.Simulated);
+  }
+
+  // extend methods for specific tags
+  if (T[tagName]) Object.extend(methods, T[tagName]);
+
+  for (var property in methods) {
+    var value = methods[property];
+    if (typeof value == 'function' && !(property in element))
+      element[property] = cache.findOrStore(value);
+  }
+
+  element._extended = Prototype.emptyFunction;
+  return element;
+};
+
+Element.extend.cache = {
+  findOrStore: function(value) {
+    return this[value] = this[value] || function() {
+      return value.apply(null, [this].concat($A(arguments)));
+    }
+  }
+};
+
+Element.Methods = {
+  visible: function(element) {
+    return $(element).style.display != 'none';
+  },
+
+  toggle: function(element) {
+    element = $(element);
+    Element[Element.visible(element) ? 'hide' : 'show'](element);
+    return element;
+  },
+
+  hide: function(element) {
+    $(element).style.display = 'none';
+    return element;
+  },
+
+  show: function(element) {
+    $(element).style.display = '';
+    return element;
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+    return element;
+  },
+
+  update: function(element, html) {
+    html = typeof html == 'undefined' ? '' : html.toString();
+    $(element).innerHTML = html.stripScripts();
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  },
+
+  replace: function(element, html) {
+    element = $(element);
+    html = typeof html == 'undefined' ? '' : html.toString();
+    if (element.outerHTML) {
+      element.outerHTML = html.stripScripts();
+    } else {
+      var range = element.ownerDocument.createRange();
+      range.selectNodeContents(element);
+      element.parentNode.replaceChild(
+        range.createContextualFragment(html.stripScripts()), element);
+    }
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  },
+
+  inspect: function(element) {
+    element = $(element);
+    var result = '<' + element.tagName.toLowerCase();
+    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+      var property = pair.first(), attribute = pair.last();
+      var value = (element[property] || '').toString();
+      if (value) result += ' ' + attribute + '=' + value.inspect(true);
+    });
+    return result + '>';
+  },
+
+  recursivelyCollect: function(element, property) {
+    element = $(element);
+    var elements = [];
+    while (element = element[property])
+      if (element.nodeType == 1)
+        elements.push(Element.extend(element));
+    return elements;
+  },
+
+  ancestors: function(element) {
+    return $(element).recursivelyCollect('parentNode');
+  },
+
+  descendants: function(element) {
+    return $A($(element).getElementsByTagName('*')).each(Element.extend);
+  },
+
+  firstDescendant: function(element) {
+    element = $(element).firstChild;
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    return $(element);
+  },
+
+  immediateDescendants: function(element) {
+    if (!(element = $(element).firstChild)) return [];
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    if (element) return [element].concat($(element).nextSiblings());
+    return [];
+  },
+
+  previousSiblings: function(element) {
+    return $(element).recursivelyCollect('previousSibling');
+  },
+
+  nextSiblings: function(element) {
+    return $(element).recursivelyCollect('nextSibling');
+  },
+
+  siblings: function(element) {
+    element = $(element);
+    return element.previousSiblings().reverse().concat(element.nextSiblings());
+  },
+
+  match: function(element, selector) {
+    if (typeof selector == 'string')
+      selector = new Selector(selector);
+    return selector.match($(element));
+  },
+
+  up: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(element.parentNode);
+    var ancestors = element.ancestors();
+    return expression ? Selector.findElement(ancestors, expression, index) :
+      ancestors[index || 0];
+  },
+
+  down: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return element.firstDescendant();
+    var descendants = element.descendants();
+    return expression ? Selector.findElement(descendants, expression, index) :
+      descendants[index || 0];
+  },
+
+  previous: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+    var previousSiblings = element.previousSiblings();
+    return expression ? Selector.findElement(previousSiblings, expression, index) :
+      previousSiblings[index || 0];
+  },
+
+  next: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+    var nextSiblings = element.nextSiblings();
+    return expression ? Selector.findElement(nextSiblings, expression, index) :
+      nextSiblings[index || 0];
+  },
+
+  getElementsBySelector: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element, args);
+  },
+
+  getElementsByClassName: function(element, className) {
+    return document.getElementsByClassName(className, element);
+  },
+
+  readAttribute: function(element, name) {
+    element = $(element);
+    if (Prototype.Browser.IE) {
+      if (!element.attributes) return null;
+      var t = Element._attributeTranslations;
+      if (t.values[name]) return t.values[name](element, name);
+      if (t.names[name])  name = t.names[name];
+      var attribute = element.attributes[name];
+      return attribute ? attribute.nodeValue : null;
+    }
+    return element.getAttribute(name);
+  },
+
+  getHeight: function(element) {
+    return $(element).getDimensions().height;
+  },
+
+  getWidth: function(element) {
+    return $(element).getDimensions().width;
+  },
+
+  classNames: function(element) {
+    return new Element.ClassNames(element);
+  },
+
+  hasClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    var elementClassName = element.className;
+    if (elementClassName.length == 0) return false;
+    if (elementClassName == className ||
+        elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
+      return true;
+    return false;
+  },
+
+  addClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element).add(className);
+    return element;
+  },
+
+  removeClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element).remove(className);
+    return element;
+  },
+
+  toggleClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className);
+    return element;
+  },
+
+  observe: function() {
+    Event.observe.apply(Event, arguments);
+    return $A(arguments).first();
+  },
+
+  stopObserving: function() {
+    Event.stopObserving.apply(Event, arguments);
+    return $A(arguments).first();
+  },
+
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    element = $(element);
+    var node = element.firstChild;
+    while (node) {
+      var nextNode = node.nextSibling;
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+        element.removeChild(node);
+      node = nextNode;
+    }
+    return element;
+  },
+
+  empty: function(element) {
+    return $(element).innerHTML.blank();
+  },
+
+  descendantOf: function(element, ancestor) {
+    element = $(element), ancestor = $(ancestor);
+    while (element = element.parentNode)
+      if (element == ancestor) return true;
+    return false;
+  },
+
+  scrollTo: function(element) {
+    element = $(element);
+    var pos = Position.cumulativeOffset(element);
+    window.scrollTo(pos[0], pos[1]);
+    return element;
+  },
+
+  getStyle: function(element, style) {
+    element = $(element);
+    style = style == 'float' ? 'cssFloat' : style.camelize();
+    var value = element.style[style];
+    if (!value) {
+      var css = document.defaultView.getComputedStyle(element, null);
+      value = css ? css[style] : null;
+    }
+    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+    return value == 'auto' ? null : value;
+  },
+
+  getOpacity: function(element) {
+    return $(element).getStyle('opacity');
+  },
+
+  setStyle: function(element, styles, camelized) {
+    element = $(element);
+    var elementStyle = element.style;
+
+    for (var property in styles)
+      if (property == 'opacity') element.setOpacity(styles[property])
+      else
+        elementStyle[(property == 'float' || property == 'cssFloat') ?
+          (elementStyle.styleFloat === undefined ? 'cssFloat' : 'styleFloat') :
+          (camelized ? property : property.camelize())] = styles[property];
+
+    return element;
+  },
+
+  setOpacity: function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1 || value === '') ? '' :
+      (value < 0.00001) ? 0 : value;
+    return element;
+  },
+
+  getDimensions: function(element) {
+    element = $(element);
+    var display = $(element).getStyle('display');
+    if (display != 'none' && display != null) // Safari bug
+      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;
+    var originalDisplay = els.display;
+    els.visibility = 'hidden';
+    els.position = 'absolute';
+    els.display = 'block';
+    var originalWidth = element.clientWidth;
+    var originalHeight = element.clientHeight;
+    els.display = originalDisplay;
+    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;
+      }
+    }
+    return element;
+  },
+
+  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 = '';
+    }
+    return element;
+  },
+
+  makeClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return element;
+    element._overflow = element.style.overflow || 'auto';
+    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+      element.style.overflow = 'hidden';
+    return element;
+  },
+
+  undoClipping: function(element) {
+    element = $(element);
+    if (!element._overflow) return element;
+    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+    element._overflow = null;
+    return element;
+  }
+};
+
+Object.extend(Element.Methods, {
+  childOf: Element.Methods.descendantOf,
+  childElements: Element.Methods.immediateDescendants
+});
+
+if (Prototype.Browser.Opera) {
+  Element.Methods._getStyle = Element.Methods.getStyle;
+  Element.Methods.getStyle = function(element, style) {
+    switch(style) {
+      case 'left':
+      case 'top':
+      case 'right':
+      case 'bottom':
+        if (Element._getStyle(element, 'position') == 'static') return null;
+      default: return Element._getStyle(element, style);
+    }
+  };
+}
+else if (Prototype.Browser.IE) {
+  Element.Methods.getStyle = function(element, style) {
+    element = $(element);
+    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+    var value = element.style[style];
+    if (!value && element.currentStyle) value = element.currentStyle[style];
+
+    if (style == 'opacity') {
+      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+        if (value[1]) return parseFloat(value[1]) / 100;
+      return 1.0;
+    }
+
+    if (value == 'auto') {
+      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+        return element['offset'+style.capitalize()] + 'px';
+      return null;
+    }
+    return value;
+  };
+
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    var filter = element.getStyle('filter'), style = element.style;
+    if (value == 1 || value === '') {
+      style.filter = filter.replace(/alpha\([^\)]*\)/gi,'');
+      return element;
+    } else if (value < 0.00001) value = 0;
+    style.filter = filter.replace(/alpha\([^\)]*\)/gi, '') +
+      'alpha(opacity=' + (value * 100) + ')';
+    return element;
+  };
+
+  // IE is missing .innerHTML support for TABLE-related elements
+  Element.Methods.update = function(element, html) {
+    element = $(element);
+    html = typeof html == 'undefined' ? '' : html.toString();
+    var tagName = element.tagName.toUpperCase();
+    if (['THEAD','TBODY','TR','TD'].include(tagName)) {
+      var div = document.createElement('div');
+      switch (tagName) {
+        case 'THEAD':
+        case 'TBODY':
+          div.innerHTML = '<table><tbody>' +  html.stripScripts() + '</tbody></table>';
+          depth = 2;
+          break;
+        case 'TR':
+          div.innerHTML = '<table><tbody><tr>' +  html.stripScripts() + '</tr></tbody></table>';
+          depth = 3;
+          break;
+        case 'TD':
+          div.innerHTML = '<table><tbody><tr><td>' +  html.stripScripts() + '</td></tr></tbody></table>';
+          depth = 4;
+      }
+      $A(element.childNodes).each(function(node) { element.removeChild(node) });
+      depth.times(function() { div = div.firstChild });
+      $A(div.childNodes).each(function(node) { element.appendChild(node) });
+    } else {
+      element.innerHTML = html.stripScripts();
+    }
+    setTimeout(function() { html.evalScripts() }, 10);
+    return element;
+  }
+}
+else if (Prototype.Browser.Gecko) {
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1) ? 0.999999 :
+      (value === '') ? '' : (value < 0.00001) ? 0 : value;
+    return element;
+  };
+}
+
+Element._attributeTranslations = {
+  names: {
+    colspan:   "colSpan",
+    rowspan:   "rowSpan",
+    valign:    "vAlign",
+    datetime:  "dateTime",
+    accesskey: "accessKey",
+    tabindex:  "tabIndex",
+    enctype:   "encType",
+    maxlength: "maxLength",
+    readonly:  "readOnly",
+    longdesc:  "longDesc"
+  },
+  values: {
+    _getAttr: function(element, attribute) {
+      return element.getAttribute(attribute, 2);
+    },
+    _flag: function(element, attribute) {
+      return $(element).hasAttribute(attribute) ? attribute : null;
+    },
+    style: function(element) {
+      return element.style.cssText.toLowerCase();
+    },
+    title: function(element) {
+      var node = element.getAttributeNode('title');
+      return node.specified ? node.nodeValue : null;
+    }
+  }
+};
+
+(function() {
+  Object.extend(this, {
+    href: this._getAttr,
+    src:  this._getAttr,
+    type: this._getAttr,
+    disabled: this._flag,
+    checked:  this._flag,
+    readonly: this._flag,
+    multiple: this._flag
+  });
+}).call(Element._attributeTranslations.values);
+
+Element.Methods.Simulated = {
+  hasAttribute: function(element, attribute) {
+    var t = Element._attributeTranslations, node;
+    attribute = t.names[attribute] || attribute;
+    node = $(element).getAttributeNode(attribute);
+    return node && node.specified;
+  }
+};
+
+Element.Methods.ByTag = {};
+
+Object.extend(Element, Element.Methods);
+
+if (!Prototype.BrowserFeatures.ElementExtensions &&
+ document.createElement('div').__proto__) {
+  window.HTMLElement = {};
+  window.HTMLElement.prototype = document.createElement('div').__proto__;
+  Prototype.BrowserFeatures.ElementExtensions = true;
+}
+
+Element.hasAttribute = function(element, attribute) {
+  if (element.hasAttribute) return element.hasAttribute(attribute);
+  return Element.Methods.Simulated.hasAttribute(element, attribute);
+};
+
+Element.addMethods = function(methods) {
+  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+  if (!methods) {
+    Object.extend(Form, Form.Methods);
+    Object.extend(Form.Element, Form.Element.Methods);
+    Object.extend(Element.Methods.ByTag, {
+      "FORM":     Object.clone(Form.Methods),
+      "INPUT":    Object.clone(Form.Element.Methods),
+      "SELECT":   Object.clone(Form.Element.Methods),
+      "TEXTAREA": Object.clone(Form.Element.Methods)
+    });
+  }
+
+  if (arguments.length == 2) {
+    var tagName = methods;
+    methods = arguments[1];
+  }
+
+  if (!tagName) Object.extend(Element.Methods, methods || {});
+  else {
+    if (tagName.constructor == Array) tagName.each(extend);
+    else extend(tagName);
+  }
+
+  function extend(tagName) {
+    tagName = tagName.toUpperCase();
+    if (!Element.Methods.ByTag[tagName])
+      Element.Methods.ByTag[tagName] = {};
+    Object.extend(Element.Methods.ByTag[tagName], methods);
+  }
+
+  function copy(methods, destination, onlyIfAbsent) {
+    onlyIfAbsent = onlyIfAbsent || false;
+    var cache = Element.extend.cache;
+    for (var property in methods) {
+      var value = methods[property];
+      if (!onlyIfAbsent || !(property in destination))
+        destination[property] = cache.findOrStore(value);
+    }
+  }
+
+  function findDOMClass(tagName) {
+    var klass;
+    var trans = {
+      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+      "FrameSet", "IFRAME": "IFrame"
+    };
+    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+    if (window[klass]) return window[klass];
+    klass = 'HTML' + tagName + 'Element';
+    if (window[klass]) return window[klass];
+    klass = 'HTML' + tagName.capitalize() + 'Element';
+    if (window[klass]) return window[klass];
+
+    window[klass] = {};
+    window[klass].prototype = document.createElement(tagName).__proto__;
+    return window[klass];
+  }
+
+  if (F.ElementExtensions) {
+    copy(Element.Methods, HTMLElement.prototype);
+    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+  }
+
+  if (F.SpecificElementExtensions) {
+    for (var tag in Element.Methods.ByTag) {
+      var klass = findDOMClass(tag);
+      if (typeof klass == "undefined") continue;
+      copy(T[tag], klass.prototype);
+    }
+  }
+
+  Object.extend(Element, Element.Methods);
+  delete Element.ByTag;
+};
+
+var 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) {
+        var tagName = this.element.tagName.toUpperCase();
+        if (['TBODY', 'TR'].include(tagName)) {
+          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($A(this).concat(classNameToAdd).join(' '));
+  },
+
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set($A(this).without(classNameToRemove).join(' '));
+  },
+
+  toString: function() {
+    return $A(this).join(' ');
+  }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
+ * license.  Please see http://www.yui-ext.com/ for more information. */
+
+var Selector = Class.create();
+
+Selector.prototype = {
+  initialize: function(expression) {
+    this.expression = expression.strip();
+    this.compileMatcher();
+  },
+
+  compileMatcher: function() {
+    // Selectors with namespaced attributes can't use the XPath version
+    if (Prototype.BrowserFeatures.XPath && !(/\[[\w-]*?:/).test(this.expression))
+      return this.compileXPathMatcher();
+
+    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+        c = Selector.criteria, le, p, m;
+
+    if (Selector._cache[e]) {
+      this.matcher = Selector._cache[e]; return;
+    }
+    this.matcher = ["this.matcher = function(root) {",
+                    "var r = root, h = Selector.handlers, c = false, n;"];
+
+    while (e && le != e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        p = ps[i];
+        if (m = e.match(p)) {
+          this.matcher.push(typeof c[i] == 'function' ? c[i](m) :
+    	      new Template(c[i]).evaluate(m));
+          e = e.replace(m[0], '');
+          break;
+        }
+      }
+    }
+
+    this.matcher.push("return h.unique(n);\n}");
+    eval(this.matcher.join('\n'));
+    Selector._cache[this.expression] = this.matcher;
+  },
+
+  compileXPathMatcher: function() {
+    var e = this.expression, ps = Selector.patterns,
+        x = Selector.xpath, le,  m;
+
+    if (Selector._cache[e]) {
+      this.xpath = Selector._cache[e]; return;
+    }
+
+    this.matcher = ['.//*'];
+    while (e && le != e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        if (m = e.match(ps[i])) {
+          this.matcher.push(typeof x[i] == 'function' ? x[i](m) :
+            new Template(x[i]).evaluate(m));
+          e = e.replace(m[0], '');
+          break;
+        }
+      }
+    }
+
+    this.xpath = this.matcher.join('');
+    Selector._cache[this.expression] = this.xpath;
+  },
+
+  findElements: function(root) {
+    root = root || document;
+    if (this.xpath) return document._getElementsByXPath(this.xpath, root);
+    return this.matcher(root);
+  },
+
+  match: function(element) {
+    return this.findElements(document).include(element);
+  },
+
+  toString: function() {
+    return this.expression;
+  },
+
+  inspect: function() {
+    return "#<Selector:" + this.expression.inspect() + ">";
+  }
+};
+
+Object.extend(Selector, {
+  _cache: {},
+
+  xpath: {
+    descendant:   "//*",
+    child:        "/*",
+    adjacent:     "/following-sibling::*[1]",
+    laterSibling: '/following-sibling::*',
+    tagName:      function(m) {
+      if (m[1] == '*') return '';
+      return "[local-name()='" + m[1].toLowerCase() +
+             "' or local-name()='" + m[1].toUpperCase() + "']";
+    },
+    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
+    id:           "[@id='#{1}']",
+    attrPresence: "[@#{1}]",
+    attr: function(m) {
+      m[3] = m[5] || m[6];
+      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
+    },
+    pseudo: function(m) {
+      var h = Selector.xpath.pseudos[m[1]];
+      if (!h) return '';
+      if (typeof h === 'function') return h(m);
+      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
+    },
+    operators: {
+      '=':  "[@#{1}='#{3}']",
+      '!=': "[@#{1}!='#{3}']",
+      '^=': "[starts-with(@#{1}, '#{3}')]",
+      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
+      '*=': "[contains(@#{1}, '#{3}')]",
+      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
+      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
+    },
+    pseudos: {
+      'first-child': '[not(preceding-sibling::*)]',
+      'last-child':  '[not(following-sibling::*)]',
+      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
+      'empty':       "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
+      'checked':     "[@checked]",
+      'disabled':    "[@disabled]",
+      'enabled':     "[not(@disabled)]",
+      'not': function(m) {
+        var e = m[6], p = Selector.patterns,
+            x = Selector.xpath, le, m, v;
+
+        var exclusion = [];
+        while (e && le != e && (/\S/).test(e)) {
+          le = e;
+          for (var i in p) {
+            if (m = e.match(p[i])) {
+              v = typeof x[i] == 'function' ? x[i](m) : new Template(x[i]).evaluate(m);
+              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
+              e = e.replace(m[0], '');
+              break;
+            }
+          }
+        }
+        return "[not(" + exclusion.join(" and ") + ")]";
+      },
+      'nth-child':      function(m) {
+        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
+      },
+      'nth-last-child': function(m) {
+        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
+      },
+      'nth-of-type':    function(m) {
+        return Selector.xpath.pseudos.nth("position() ", m);
+      },
+      'nth-last-of-type': function(m) {
+        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
+      },
+      'first-of-type':  function(m) {
+        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
+      },
+      'last-of-type':   function(m) {
+        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
+      },
+      'only-of-type':   function(m) {
+        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
+      },
+      nth: function(fragment, m) {
+        var mm, formula = m[6], predicate;
+        if (formula == 'even') formula = '2n+0';
+        if (formula == 'odd')  formula = '2n+1';
+        if (mm = formula.match(/^(\d+)$/)) // digit only
+          return '[' + fragment + "= " + mm[1] + ']';
+        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+          if (mm[1] == "-") mm[1] = -1;
+          var a = mm[1] ? Number(mm[1]) : 1;
+          var b = mm[2] ? Number(mm[2]) : 0;
+          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
+          "((#{fragment} - #{b}) div #{a} >= 0)]";
+          return new Template(predicate).evaluate({
+            fragment: fragment, a: a, b: b });
+        }
+      }
+    }
+  },
+
+  criteria: {
+    tagName:      'n = h.tagName(n, r, "#{1}", c);   c = false;',
+    className:    'n = h.className(n, r, "#{1}", c); c = false;',
+    id:           'n = h.id(n, r, "#{1}", c);        c = false;',
+    attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
+    attr: function(m) {
+      m[3] = (m[5] || m[6]);
+      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
+    },
+    pseudo:       function(m) {
+      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
+      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
+    },
+    descendant:   'c = "descendant";',
+    child:        'c = "child";',
+    adjacent:     'c = "adjacent";',
+    laterSibling: 'c = "laterSibling";'
+  },
+
+  patterns: {
+    // combinators must be listed first
+    // (and descendant needs to be last combinator)
+    laterSibling: /^\s*~\s*/,
+    child:        /^\s*>\s*/,
+    adjacent:     /^\s*\+\s*/,
+    descendant:   /^\s/,
+
+    // selectors follow
+    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
+    id:           /^#([\w\-\*]+)(\b|$)/,
+    className:    /^\.([\w\-\*]+)(\b|$)/,
+    pseudo:       /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|\s|(?=:))/,
+    attrPresence: /^\[([\w]+)\]/,
+    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\]]*?)\4|([^'"][^\]]*?)))?\]/
+  },
+
+  handlers: {
+    // UTILITY FUNCTIONS
+    // joins two collections
+    concat: function(a, b) {
+      for (var i = 0, node; node = b[i]; i++)
+        a.push(node);
+      return a;
+    },
+
+    // marks an array of nodes for counting
+    mark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._counted = true;
+      return nodes;
+    },
+
+    unmark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._counted = undefined;
+      return nodes;
+    },
+
+    // mark each child node with its position (for nth calls)
+    // "ofType" flag indicates whether we're indexing for nth-of-type
+    // rather than nth-child
+    index: function(parentNode, reverse, ofType) {
+      parentNode._counted = true;
+      if (reverse) {
+        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
+          node = nodes[i];
+          if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+        }
+      } else {
+        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+          if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+      }
+    },
+
+    // filters out duplicates and extends all nodes
+    unique: function(nodes) {
+      if (nodes.length == 0) return nodes;
+      var results = [], n;
+      for (var i = 0, l = nodes.length; i < l; i++)
+        if (!(n = nodes[i])._counted) {
+          n._counted = true;
+          results.push(Element.extend(n));
+        }
+      return Selector.handlers.unmark(results);
+    },
+
+    // COMBINATOR FUNCTIONS
+    descendant: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        h.concat(results, node.getElementsByTagName('*'));
+      return results;
+    },
+
+    child: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        for (var j = 0, children = [], child; child = node.childNodes[j]; j++)
+          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
+      }
+      return results;
+    },
+
+    adjacent: function(nodes) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        var next = this.nextElementSibling(node);
+        if (next) results.push(next);
+      }
+      return results;
+    },
+
+    laterSibling: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        h.concat(results, Element.nextSiblings(node));
+      return results;
+    },
+
+    nextElementSibling: function(node) {
+      while (node = node.nextSibling)
+	      if (node.nodeType == 1) return node;
+      return null;
+    },
+
+    previousElementSibling: function(node) {
+      while (node = node.previousSibling)
+        if (node.nodeType == 1) return node;
+      return null;
+    },
+
+    // TOKEN FUNCTIONS
+    tagName: function(nodes, root, tagName, combinator) {
+      tagName = tagName.toUpperCase();
+      var results = [], h = Selector.handlers;
+      if (nodes) {
+        if (combinator) {
+          // fastlane for ordinary descendant combinators
+          if (combinator == "descendant") {
+            for (var i = 0, node; node = nodes[i]; i++)
+              h.concat(results, node.getElementsByTagName(tagName));
+            return results;
+          } else nodes = this[combinator](nodes);
+          if (tagName == "*") return nodes;
+        }
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node.tagName.toUpperCase() == tagName) results.push(node);
+        return results;
+      } else return root.getElementsByTagName(tagName);
+    },
+
+    id: function(nodes, root, id, combinator) {
+      var targetNode = $(id), h = Selector.handlers;
+      if (!nodes && root == document) return targetNode ? [targetNode] : [];
+      if (nodes) {
+        if (combinator) {
+          if (combinator == 'child') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (targetNode.parentNode == node) return [targetNode];
+          } else if (combinator == 'descendant') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (Element.descendantOf(targetNode, node)) return [targetNode];
+          } else if (combinator == 'adjacent') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (Selector.handlers.previousElementSibling(targetNode) == node)
+                return [targetNode];
+          } else nodes = h[combinator](nodes);
+        }
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node == targetNode) return [targetNode];
+        return [];
+      }
+      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
+    },
+
+    className: function(nodes, root, className, combinator) {
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      return Selector.handlers.byClassName(nodes, root, className);
+    },
+
+    byClassName: function(nodes, root, className) {
+      if (!nodes) nodes = Selector.handlers.descendant([root]);
+      var needle = ' ' + className + ' ';
+      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
+        nodeClassName = node.className;
+        if (nodeClassName.length == 0) continue;
+        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
+          results.push(node);
+      }
+      return results;
+    },
+
+    attrPresence: function(nodes, root, attr) {
+      var results = [];
+      for (var i = 0, node; node = nodes[i]; i++)
+        if (Element.hasAttribute(node, attr)) results.push(node);
+      return results;
+    },
+
+    attr: function(nodes, root, attr, value, operator) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      var handler = Selector.operators[operator], results = [];
+      for (var i = 0, node; node = nodes[i]; i++) {
+        var nodeValue = Element.readAttribute(node, attr);
+        if (nodeValue === null) continue;
+        if (handler(nodeValue, value)) results.push(node);
+      }
+      return results;
+    },
+
+    pseudo: function(nodes, name, value, root, combinator) {
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      return Selector.pseudos[name](nodes, value, root);
+    }
+  },
+
+  pseudos: {
+    'first-child': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        if (Selector.handlers.previousElementSibling(node)) continue;
+          results.push(node);
+      }
+      return results;
+    },
+    'last-child': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        if (Selector.handlers.nextElementSibling(node)) continue;
+          results.push(node);
+      }
+      return results;
+    },
+    'only-child': function(nodes, value, root) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
+          results.push(node);
+      return results;
+    },
+    'nth-child':        function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root);
+    },
+    'nth-last-child':   function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, true);
+    },
+    'nth-of-type':      function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, false, true);
+    },
+    'nth-last-of-type': function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, true, true);
+    },
+    'first-of-type':    function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, "1", root, false, true);
+    },
+    'last-of-type':     function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, "1", root, true, true);
+    },
+    'only-of-type':     function(nodes, formula, root) {
+      var p = Selector.pseudos;
+      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
+    },
+
+    // handles the an+b logic
+    getIndices: function(a, b, total) {
+      if (a == 0) return b > 0 ? [b] : [];
+      return $R(1, total).inject([], function(memo, i) {
+        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
+        return memo;
+      });
+    },
+
+    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
+    nth: function(nodes, formula, root, reverse, ofType) {
+      if (nodes.length == 0) return [];
+      if (formula == 'even') formula = '2n+0';
+      if (formula == 'odd')  formula = '2n+1';
+      var h = Selector.handlers, results = [], indexed = [], m;
+      h.mark(nodes);
+      for (var i = 0, node; node = nodes[i]; i++) {
+        if (!node.parentNode._counted) {
+          h.index(node.parentNode, reverse, ofType);
+          indexed.push(node.parentNode);
+        }
+      }
+      if (formula.match(/^\d+$/)) { // just a number
+        formula = Number(formula);
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node.nodeIndex == formula) results.push(node);
+      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+        if (m[1] == "-") m[1] = -1;
+        var a = m[1] ? Number(m[1]) : 1;
+        var b = m[2] ? Number(m[2]) : 0;
+        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
+        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
+          for (var j = 0; j < l; j++)
+            if (node.nodeIndex == indices[j]) results.push(node);
+        }
+      }
+      h.unmark(nodes);
+      h.unmark(indexed);
+      return results;
+    },
+
+    'empty': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        // IE treats comments as element nodes
+        if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
+        results.push(node);
+      }
+      return results;
+    },
+
+    'not': function(nodes, selector, root) {
+      var h = Selector.handlers, selectorType, m;
+      var exclusions = new Selector(selector).findElements(root);
+      h.mark(exclusions);
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!node._counted) results.push(node);
+      h.unmark(exclusions);
+      return results;
+    },
+
+    'enabled': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!node.disabled) results.push(node);
+      return results;
+    },
+
+    'disabled': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (node.disabled) results.push(node);
+      return results;
+    },
+
+    'checked': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (node.checked) results.push(node);
+      return results;
+    }
+  },
+
+  operators: {
+    '=':  function(nv, v) { return nv == v; },
+    '!=': function(nv, v) { return nv != v; },
+    '^=': function(nv, v) { return nv.startsWith(v); },
+    '$=': function(nv, v) { return nv.endsWith(v); },
+    '*=': function(nv, v) { return nv.include(v); },
+    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
+    '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
+  },
+
+  matchElements: function(elements, expression) {
+    var matches = new Selector(expression).findElements(), h = Selector.handlers;
+    h.mark(matches);
+    for (var i = 0, results = [], element; element = elements[i]; i++)
+      if (element._counted) results.push(element);
+    h.unmark(matches);
+    return results;
+  },
+
+  findElement: function(elements, expression, index) {
+    if (typeof expression == 'number') {
+      index = expression; expression = false;
+    }
+    return Selector.matchElements(elements, expression || '*')[index || 0];
+  },
+
+  findChildElements: function(element, expressions) {
+    var exprs = expressions.join(','), expressions = [];
+    exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+      expressions.push(m[1].strip());
+    });
+    var results = [], h = Selector.handlers;
+    for (var i = 0, l = expressions.length, selector; i < l; i++) {
+      selector = new Selector(expressions[i].strip());
+      h.concat(results, selector.findElements(element));
+    }
+    return (l > 1) ? h.unique(results) : results;
+  }
+});
+
+function $$() {
+  return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+  reset: function(form) {
+    $(form).reset();
+    return form;
+  },
+
+  serializeElements: function(elements, getHash) {
+    var data = elements.inject({}, function(result, element) {
+      if (!element.disabled && element.name) {
+        var key = element.name, value = $(element).getValue();
+        if (value != null) {
+         	if (key in result) {
+            if (result[key].constructor != Array) result[key] = [result[key]];
+            result[key].push(value);
+          }
+          else result[key] = value;
+        }
+      }
+      return result;
+    });
+
+    return getHash ? data : Hash.toQueryString(data);
+  }
+};
+
+Form.Methods = {
+  serialize: function(form, getHash) {
+    return Form.serializeElements(Form.getElements(form), getHash);
+  },
+
+  getElements: function(form) {
+    return $A($(form).getElementsByTagName('*')).inject([],
+      function(elements, child) {
+        if (Form.Element.Serializers[child.tagName.toLowerCase()])
+          elements.push(Element.extend(child));
+        return elements;
+      }
+    );
+  },
+
+  getInputs: function(form, typeName, name) {
+    form = $(form);
+    var inputs = form.getElementsByTagName('input');
+
+    if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+      var input = inputs[i];
+      if ((typeName && input.type != typeName) || (name && input.name != name))
+        continue;
+      matchingInputs.push(Element.extend(input));
+    }
+
+    return matchingInputs;
+  },
+
+  disable: function(form) {
+    form = $(form);
+    Form.getElements(form).invoke('disable');
+    return form;
+  },
+
+  enable: function(form) {
+    form = $(form);
+    Form.getElements(form).invoke('enable');
+    return form;
+  },
+
+  findFirstElement: function(form) {
+    return $(form).getElements().find(function(element) {
+      return element.type != 'hidden' && !element.disabled &&
+        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    });
+  },
+
+  focusFirstElement: function(form) {
+    form = $(form);
+    form.findFirstElement().activate();
+    return form;
+  },
+
+  request: function(form, options) {
+    form = $(form), options = Object.clone(options || {});
+
+    var params = options.parameters;
+    options.parameters = form.serialize(true);
+
+    if (params) {
+      if (typeof params == 'string') params = params.toQueryParams();
+      Object.extend(options.parameters, params);
+    }
+
+    if (form.hasAttribute('method') && !options.method)
+      options.method = form.method;
+
+    return new Ajax.Request(form.readAttribute('action'), options);
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+  focus: function(element) {
+    $(element).focus();
+    return element;
+  },
+
+  select: function(element) {
+    $(element).select();
+    return element;
+  }
+}
+
+Form.Element.Methods = {
+  serialize: function(element) {
+    element = $(element);
+    if (!element.disabled && element.name) {
+      var value = element.getValue();
+      if (value != undefined) {
+        var pair = {};
+        pair[element.name] = value;
+        return Hash.toQueryString(pair);
+      }
+    }
+    return '';
+  },
+
+  getValue: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    return Form.Element.Serializers[method](element);
+  },
+
+  clear: function(element) {
+    $(element).value = '';
+    return element;
+  },
+
+  present: function(element) {
+    return $(element).value != '';
+  },
+
+  activate: function(element) {
+    element = $(element);
+    try {
+      element.focus();
+      if (element.select && (element.tagName.toLowerCase() != 'input' ||
+        !['button', 'reset', 'submit'].include(element.type)))
+        element.select();
+    } catch (e) {}
+    return element;
+  },
+
+  disable: function(element) {
+    element = $(element);
+    element.blur();
+    element.disabled = true;
+    return element;
+  },
+
+  enable: function(element) {
+    element = $(element);
+    element.disabled = false;
+    return element;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Field = Form.Element;
+var $F = Form.Element.Methods.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+  input: function(element) {
+    switch (element.type.toLowerCase()) {
+      case 'checkbox':
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element);
+      default:
+        return Form.Element.Serializers.textarea(element);
+    }
+  },
+
+  inputSelector: function(element) {
+    return element.checked ? element.value : null;
+  },
+
+  textarea: function(element) {
+    return element.value;
+  },
+
+  select: function(element) {
+    return this[element.type == 'select-one' ?
+      'selectOne' : 'selectMany'](element);
+  },
+
+  selectOne: function(element) {
+    var index = element.selectedIndex;
+    return index >= 0 ? this.optionValue(element.options[index]) : null;
+  },
+
+  selectMany: function(element) {
+    var values, length = element.length;
+    if (!length) return null;
+
+    for (var i = 0, values = []; i < length; i++) {
+      var opt = element.options[i];
+      if (opt.selected) values.push(this.optionValue(opt));
+    }
+    return values;
+  },
+
+  optionValue: function(opt) {
+    // extend element because hasAttribute may not be native
+    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+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();
+    var changed = ('string' == typeof this.lastValue && 'string' == typeof value
+      ? this.lastValue != value : String(this.lastValue) != String(value));
+    if (changed) {
+      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() {
+    Form.getElements(this.element).each(this.registerCallback.bind(this));
+  },
+
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':
+        case 'radio':
+          Event.observe(element, 'click', this.onElementEvent.bind(this));
+          break;
+        default:
+          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,
+  KEY_HOME:     36,
+  KEY_END:      35,
+  KEY_PAGEUP:   33,
+  KEY_PAGEDOWN: 34,
+
+  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, length = Event.observers.length; i < length; i++) {
+      Event.stopObserving.apply(this, Event.observers[i]);
+      Event.observers[i][0] = null;
+    }
+    Event.observers = false;
+  },
+
+  observe: function(element, name, observer, useCapture) {
+    element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+      (Prototype.Browser.WebKit || element.attachEvent))
+      name = 'keydown';
+
+    Event._observeAndCache(element, name, observer, useCapture);
+  },
+
+  stopObserving: function(element, name, observer, useCapture) {
+    element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+        (Prototype.Browser.WebKit || element.attachEvent))
+      name = 'keydown';
+
+    if (element.removeEventListener) {
+      element.removeEventListener(name, observer, useCapture);
+    } else if (element.detachEvent) {
+      try {
+        element.detachEvent('on' + name, observer);
+      } catch (e) {}
+    }
+  }
+});
+
+/* prevent memory leaks in IE */
+if (Prototype.Browser.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) {
+        if(element.tagName=='BODY') break;
+        var 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;
+  },
+
+  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 {
+      if (!window.opera || element.tagName=='BODY') {
+        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 (Prototype.Browser.WebKit) {
+  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];
+  }
+}
+
+Element.addMethods();
Index: trunk/web/static/style.css
===================================================================
--- trunk/web/static/style.css	(revision 236)
+++ trunk/web/static/style.css	(revision 236)
@@ -0,0 +1,87 @@
+h1 {
+    border-bottom: 1px dashed black;
+    margin-top: 0;
+    overflow: auto;
+}
+h1, h2, h3, h4, h5, h6 {
+    font-family: "Trebuchet MS", Trebuchet, Sans, sans-serif;
+    font-weight: bold;
+    color: #529;
+}
+kbd, dt {
+    font-weight: bold;
+}
+html {
+    font-size: 87.5%;
+}
+body {
+    font-family: Tahoma, Sans, sans-serif;
+}
+pre, code, samp, kbd, tt {
+    font-family: "Bitstream Vera Sans Mono", "Luxi Mono", "Courier New", monospace;
+    font-size: 100%;
+}
+#logo {
+    float: right;
+    margin: -1em -1em 0 0;
+}
+#logo img {
+    border: none;
+}
+p, li {
+    line-height: 1.5em;
+}
+.long li, .long dd {
+    margin-bottom: .5em;
+}
+pre {
+    background: #f6f6f6;
+    border: 1px solid #ddd;
+    padding: .125em;
+    line-height: 1.25em;
+}
+.note {
+    border: 1px solid #eea;
+    background: #ffd;
+    padding: .25em;
+}
+a:link, a:visited {
+    text-decoration: none;
+}
+a:link:hover, a:visited:hover {
+    text-decoration: underline;
+}
+var {
+    color: #090;
+}
+.index h1 tt {
+    font-size: 83%;
+}
+.index table {
+    border-collapse: collapse;
+}
+.index table td {
+    padding: 0 0.5em;
+}
+.index table td a {
+    font-family: "Bitstream Vera Sans Mono", "Luxi Mono", "Courier New", monospace;
+}
+
+#loadingnotice {
+    padding: 2px;
+    display: none;
+    color: #FFFFFF;
+    background: #CC2200;
+    position: fixed;
+    right: 10px;
+    top: 5px;
+}
+
+.error {
+  color: #FF0000;
+  padding: 0.25em;
+}
+
+td.error {
+  border: 1px solid red;
+}
Index: trunk/web/templates/__init__.py
===================================================================
--- trunk/web/templates/__init__.py	(revision 236)
+++ trunk/web/templates/__init__.py	(revision 236)
@@ -0,0 +1,9 @@
+__all__ = 'info command error help invalid list vnc'.split()
+for _name in __all__:
+    try:
+        _module = __import__(_name, globals(), {}, [_name])
+        globals()[_name] = getattr(_module, _name)
+    except ImportError, e:
+        import sys
+        print >> sys.stderr, 'Importing template "%s" raised error: %s' % (_name, e)
+        
Index: trunk/web/templates/controls.py
===================================================================
--- trunk/web/templates/controls.py	(revision 235)
+++ 	(revision )
@@ -1,284 +1,0 @@
-"""
-Functions to perform remctls.
-"""
-
-from sipb_xen_database import Machine, Disk, Type, NIC, CDROM, ctx, meta
-import validation
-from webcommon import CodeError, InvalidInput
-import random
-import subprocess
-import sys
-import time
-import re
-
-# ... and stolen from xend/uuid.py
-def randomUUID():
-    """Generate a random UUID."""
-
-    return [ random.randint(0, 255) for _ in range(0, 16) ]
-
-def uuidToString(u):
-    """Turn a numeric UUID to a hyphen-seperated one."""
-    return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
-                     "%02x" * 6]) % tuple(u)
-# end stolen code
-
-def kinit(username = 'daemon/sipb-xen.mit.edu', keytab = '/etc/sipb-xen.keytab'):
-    """Kinit with a given username and keytab"""
-
-    p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
-                         stderr=subprocess.PIPE)
-    e = p.wait()
-    if e:
-        raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
-
-def checkKinit():
-    """If we lack tickets, kinit."""
-    p = subprocess.Popen(['klist', '-s'])
-    if p.wait():
-        kinit()
-
-def remctl(*args, **kws):
-    """Perform a remctl and return the output.
-
-    kinits if necessary, and outputs errors to stderr.
-    """
-    checkKinit()
-    p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
-                         + list(args),
-                         stdout=subprocess.PIPE,
-                         stderr=subprocess.PIPE)
-    v = p.wait()
-    if kws.get('err'):
-        return p.stdout.read(), p.stderr.read()
-    if v:
-        print >> sys.stderr, 'Error', v, 'on remctl', args, ':'
-        print >> sys.stderr, p.stderr.read()
-        raise CodeError('ERROR on remctl')
-    return p.stdout.read()
-
-def lvcreate(machine, disk):
-    """Create a single disk for a machine"""
-    remctl('web', 'lvcreate', machine.name,
-           disk.guest_device_name, str(disk.size))
-    
-def makeDisks(machine):
-    """Update the lvm partitions to add a disk."""
-    for disk in machine.disks:
-        lvcreate(machine, disk)
-
-def bootMachine(machine, cdtype):
-    """Boot a machine with a given boot CD.
-
-    If cdtype is None, give no boot cd.  Otherwise, it is the string
-    id of the CD (e.g. 'gutsy_i386')
-    """
-    if cdtype is not None:
-        remctl('control', machine.name, 'create', 
-               cdtype)
-    else:
-        remctl('control', machine.name, 'create')
-
-def registerMachine(machine):
-    """Register a machine to be controlled by the web interface"""
-    remctl('web', 'register', machine.name)
-
-def unregisterMachine(machine):
-    """Unregister a machine to not be controlled by the web interface"""
-    remctl('web', 'unregister', machine.name)
-
-def createVm(owner, contact, name, memory, disk, is_hvm, cdrom):
-    """Create a VM and put it in the database"""
-    # put stuff in the table
-    transaction = ctx.current.create_transaction()
-    try:
-        validation.validMemory(owner, memory)
-        validation.validDisk(owner, disk  * 1. / 1024)
-        validation.validAddVm(owner)
-        res = meta.engine.execute('select nextval('
-                                  '\'"machines_machine_id_seq"\')')
-        id = res.fetchone()[0]
-        machine = Machine()
-        machine.machine_id = id
-        machine.name = name
-        machine.memory = memory
-        machine.owner = owner
-        machine.administrator = owner
-        machine.contact = contact
-        machine.uuid = uuidToString(randomUUID())
-        machine.boot_off_cd = True
-        machine_type = Type.get_by(hvm=is_hvm)
-        machine.type_id = machine_type.type_id
-        ctx.current.save(machine)
-        disk = Disk(machine.machine_id, 
-                    'hda', disk)
-        open_nics = NIC.select_by(machine_id=None)
-        if not open_nics: #No IPs left!
-            raise CodeError("No IP addresses left!  "
-                            "Contact sipb-xen-dev@mit.edu")
-        nic = open_nics[0]
-        nic.machine_id = machine.machine_id
-        nic.hostname = name
-        ctx.current.save(nic)    
-        ctx.current.save(disk)
-        transaction.commit()
-    except:
-        transaction.rollback()
-        raise
-    registerMachine(machine)
-    makeDisks(machine)
-    # tell it to boot with cdrom
-    bootMachine(machine, cdrom)
-    return machine
-
-def getUptimes(machines=None):
-    """Return a dictionary mapping machine names to uptime strings"""
-    value_string = remctl('web', 'listvms')
-    lines = value_string.splitlines()
-    d = {}
-    for line in lines:
-        lst = line.split()
-        name, id = lst[:2]
-        uptime = ' '.join(lst[2:])
-        d[name] = uptime
-    ans = {}
-    for m in machines:
-        ans[m] = d.get(m.name)
-    return ans
-
-def parseStatus(s):
-    """Parse a status string into nested tuples of strings.
-
-    s = output of xm list --long <machine_name>
-    """
-    values = re.split('([()])', s)
-    stack = [[]]
-    for v in values[2:-2]: #remove initial and final '()'
-        if not v:
-            continue
-        v = v.strip()
-        if v == '(':
-            stack.append([])
-        elif v == ')':
-            if len(stack[-1]) == 1:
-                stack[-1].append('')
-            stack[-2].append(stack[-1])
-            stack.pop()
-        else:
-            if not v:
-                continue
-            stack[-1].extend(v.split())
-    return stack[-1]
-
-def statusInfo(machine):
-    """Return the status list for a given machine.
-
-    Gets and parses xm list --long
-    """
-    value_string, err_string = remctl('control', machine.name, 'list-long', 
-                                      err=True)
-    if 'Unknown command' in err_string:
-        raise CodeError("ERROR in remctl list-long %s is not registered" % 
-                        (machine.name,))
-    elif 'does not exist' in err_string:
-        return None
-    elif err_string:
-        raise CodeError("ERROR in remctl list-long %s:  %s" % 
-                        (machine.name, err_string))
-    status = parseStatus(value_string)
-    return status
-
-def deleteVM(machine):
-    """Delete a VM."""
-    remctl('control', machine.name, 'destroy', err=True)
-    transaction = ctx.current.create_transaction()
-    delete_disk_pairs = [(machine.name, d.guest_device_name) 
-                         for d in machine.disks]
-    try:
-        for nic in machine.nics:
-            nic.machine_id = None
-            nic.hostname = None
-            ctx.current.save(nic)
-        for disk in machine.disks:
-            ctx.current.delete(disk)
-        ctx.current.delete(machine)
-        transaction.commit()
-    except:
-        transaction.rollback()
-        raise
-    for mname, dname in delete_disk_pairs:
-        remctl('web', 'lvremove', mname, dname)
-    unregisterMachine(machine)
-
-def commandResult(user, fields):
-    start_time = 0
-    print >> sys.stderr, time.time()-start_time
-    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
-    action = fields.getfirst('action')
-    cdrom = fields.getfirst('cdrom')
-    print >> sys.stderr, time.time()-start_time
-    if cdrom is not None and not CDROM.get(cdrom):
-        raise CodeError("Invalid cdrom type '%s'" % cdrom)    
-    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 
-                      'Delete VM'):
-        raise CodeError("Invalid action '%s'" % action)
-    if action == 'Reboot':
-        if cdrom is not None:
-            out, err = remctl('control', machine.name, 'reboot', cdrom,
-                              err=True)
-        else:
-            out, err = remctl('control', machine.name, 'reboot',
-                              err=True)
-        if err:
-            if re.match("Error: Domain '.*' does not exist.", err):
-                raise InvalidInput("action", "reboot", 
-                                   "Machine is not on")
-            else:
-                print >> sys.stderr, 'Error on reboot:'
-                print >> sys.stderr, err
-                raise CodeError('ERROR on remctl')
-                
-    elif action == 'Power on':
-        if validation.maxMemory(user, machine) < machine.memory:
-            raise InvalidInput('action', 'Power on',
-                               "You don't have enough free RAM quota "
-                               "to turn on this machine.")
-        bootMachine(machine, cdrom)
-    elif action == 'Power off':
-        out, err = remctl('control', machine.name, 'destroy', err=True)
-        if err:
-            if re.match("Error: Domain '.*' does not exist.", err):
-                raise InvalidInput("action", "Power off", 
-                                   "Machine is not on.")
-            else:
-                print >> sys.stderr, 'Error on power off:'
-                print >> sys.stderr, err
-                raise CodeError('ERROR on remctl')
-    elif action == 'Shutdown':
-        out, err = remctl('control', machine.name, 'shutdown', err=True)
-        if err:
-            if re.match("Error: Domain '.*' does not exist.", err):
-                raise InvalidInput("action", "Shutdown", 
-                                   "Machine is not on.")
-            else:
-                print >> sys.stderr, 'Error on Shutdown:'
-                print >> sys.stderr, err
-                raise CodeError('ERROR on remctl')
-    elif action == 'Delete VM':
-        deleteVM(machine)
-    print >> sys.stderr, time.time()-start_time
-
-    d = dict(user=user,
-             command=action,
-             machine=machine)
-    return d
-
-def resizeDisk(machine_name, disk_name, new_size):
-    remctl("web", "lvresize", machine_name, disk_name, new_size)
-
-def renameMachine(machine, old_name, new_name):
-    for disk in machine.disks:
-        remctl("web", "lvrename", old_name, 
-               disk.guest_device_name, new_name)
-    remctl("web", "moveregister", old_name, new_name)
-    
Index: trunk/web/templates/getafsgroups.py
===================================================================
--- trunk/web/templates/getafsgroups.py	(revision 235)
+++ 	(revision )
@@ -1,96 +1,0 @@
-#!/usr/bin/python
-import pprint
-import subprocess
-
-# import ldap
-# l = ldap.open("W92-130-LDAP-2.mit.edu")
-# # ldap.mit.edu is 1/2 broken right now so we're going to the working backend
-# l.simple_bind_s("", "")
-
-# def getLdapGroups(user):
-#     """
-#     getLdapGroups(user): returns a generator for the list of LDAP groups containing user
-#     """
-#     for user_data in l.search_s("ou=affiliates,dc=mit,dc=edu", ldap.SCOPE_ONELEVEL, "uid=" + user, []):
-#         for group_data in l.search_s("ou=groups,dc=mit,dc=edu", ldap.SCOPE_ONELEVEL, "uniqueMember="+user_data[0], ['cn']):
-#             yield group_data[1]['cn'][0]
-
-# def checkLdapGroups(user, group):
-#     """
-#     checkLdapGroups(user, group): returns True if and only if user is in LDAP group group
-#     """
-#     for result_data in l.search_s("ou=affiliates,dc=mit,dc=edu", ldap.SCOPE_ONELEVEL, "uid=" + user, []):
-#         if l.search_s("ou=groups,dc=mit,dc=edu", ldap.SCOPE_ONELEVEL, "(&(cn=" + group + ")(uniqueMember="+result_data[0] + "))", []) != []:
-#             return True
-#     return False
-
-class MyException(Exception):
-    pass
-
-def getAfsGroupMembers(group, cell):
-    p = subprocess.Popen(["pts", "membership", group, '-c', cell], 
-                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-    if p.wait():
-        return []
-    return [line.strip() for line in p.stdout.readlines()[1:]]
-
-def checkAfsGroup(user, group, cell):
-    """
-    checkAfsGroup(user, group) returns True if and only if user is in AFS group group in cell cell
-    """
-    return user in getAfsGroupMembers(group, cell)
-
-def getCell(locker):
-    p = subprocess.Popen(["fs", "whichcell", "/mit/" + locker], 
-                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-    if p.wait():
-        raise MyException(p.stderr.read())
-    return p.stdout.read().split()[-1][1:-1]
-
-def getLockerAcl(locker):
-    p = subprocess.Popen(["fs", "listacl", "/mit/" + locker], 
-                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-    if p.wait():
-        raise MyException(p.stderr.read())
-    lines = p.stdout.readlines()
-    values = []
-    for line in lines[1:]:
-        fields = line.split()
-        if fields[0] == 'Negative':
-            break
-        if 'rlidwka' in fields[1]:
-            values.append(fields[0])
-    return values
-
-def notLockerOwner(user, locker):
-    """
-    notLockerOwner(user, locker) returns false if and only if user administers locker.
-
-    If the user does not own the locker, returns the string reason for
-    the failure.
-    """
-    try:
-        cell = getCell(locker)
-        values = getLockerAcl(locker)
-    except MyException, e:
-        return str(e)
-
-    for entry in values:
-        if entry[0] == user or (entry[0][0:6] == "system" and 
-                                checkAfsGroup(user, entry[0], cell)):
-            return False
-    return "You don't have admin bits on /mit/" + locker
-
-
-if __name__ == "__main__":
-#    print list(getldapgroups("tabbott"))
-    print checkAfsGroup("tabbott", "system:debathena", 'athena.mit.edu')
-    print checkAfsGroup("tabbott", "system:debathena", 'sipb.mit.edu')
-    print checkAfsGroup("tabbott", "system:debathena-root", 'athena.mit.edu')
-    print checkAfsGroup("tabbott", "system:hmmt-request", 'athena.mit.edu')
-    print notLockerOwner("tabbott", "tabbott")
-    print notLockerOwner("tabbott", "debathena")
-    print notLockerOwner("tabbott", "sipb")
-    print notLockerOwner("tabbott", "lsc")
-    print notLockerOwner("tabbott", "scripts")
-    print notLockerOwner("ecprice", "hmmt")
Index: trunk/web/templates/main.py
===================================================================
--- trunk/web/templates/main.py	(revision 235)
+++ 	(revision )
@@ -1,643 +1,0 @@
-#!/usr/bin/python
-"""Main CGI script for web interface"""
-
-import base64
-import cPickle
-import cgi
-import datetime
-import hmac
-import os
-import sha
-import simplejson
-import sys
-import time
-from StringIO import StringIO
-
-
-def revertStandardError():
-    """Move stderr to stdout, and return the contents of the old stderr."""
-    errio = sys.stderr
-    if not isinstance(errio, StringIO):
-        return None
-    sys.stderr = sys.stdout
-    errio.seek(0)
-    return errio.read()
-
-def printError():
-    """Revert stderr to stdout, and print the contents of stderr"""
-    if isinstance(sys.stderr, StringIO):
-        print revertStandardError()
-
-if __name__ == '__main__':
-    import atexit
-    atexit.register(printError)
-    sys.stderr = StringIO()
-
-sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
-
-import templates
-from Cheetah.Template import Template
-from sipb_xen_database import Machine, CDROM, ctx, connect
-import validation
-from webcommon import InvalidInput, CodeError, g
-import controls
-
-class Checkpoint:
-    def __init__(self):
-        self.start_time = time.time()
-        self.checkpoints = []
-
-    def checkpoint(self, s):
-        self.checkpoints.append((s, time.time()))
-
-    def __str__(self):
-        return ('Timing info:\n%s\n' %
-                '\n'.join(['%s: %s' % (d, t - self.start_time) for
-                           (d, t) in self.checkpoints]))
-
-checkpoint = Checkpoint()
-
-
-def helppopup(subj):
-    """Return HTML code for a (?) link to a specified help topic"""
-    return ('<span class="helplink"><a href="help?subject=' + subj + 
-            '&amp;simple=true" target="_blank" ' + 
-            'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
-
-def makeErrorPre(old, addition):
-    if addition is None:
-        return
-    if old:
-        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
-    else:
-        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
-
-Template.helppopup = staticmethod(helppopup)
-Template.err = None
-
-class JsonDict:
-    """Class to store a dictionary that will be converted to JSON"""
-    def __init__(self, **kws):
-        self.data = kws
-        if 'err' in kws:
-            err = kws['err']
-            del kws['err']
-            self.addError(err)
-
-    def __str__(self):
-        return simplejson.dumps(self.data)
-
-    def addError(self, text):
-        """Add stderr text to be displayed on the website."""
-        self.data['err'] = \
-            makeErrorPre(self.data.get('err'), text)
-
-class Defaults:
-    """Class to store default values for fields."""
-    memory = 256
-    disk = 4.0
-    cdrom = ''
-    name = ''
-    vmtype = 'hvm'
-    def __init__(self, max_memory=None, max_disk=None, **kws):
-        if max_memory is not None:
-            self.memory = min(self.memory, max_memory)
-        if max_disk is not None:
-            self.max_disk = min(self.disk, max_disk)
-        for key in kws:
-            setattr(self, key, kws[key])
-
-
-
-DEFAULT_HEADERS = {'Content-Type': 'text/html'}
-
-def error(op, user, fields, err, emsg):
-    """Print an error page when a CodeError occurs"""
-    d = dict(op=op, user=user, errorMessage=str(err),
-             stderr=emsg)
-    return templates.error(searchList=[d])
-
-def invalidInput(op, user, fields, err, emsg):
-    """Print an error page when an InvalidInput exception occurs"""
-    d = dict(op=op, user=user, err_field=err.err_field,
-             err_value=str(err.err_value), stderr=emsg,
-             errorMessage=str(err))
-    return templates.invalid(searchList=[d])
-
-def hasVnc(status):
-    """Does the machine with a given status list support VNC?"""
-    if status is None:
-        return False
-    for l in status:
-        if l[0] == 'device' and l[1][0] == 'vfb':
-            d = dict(l[1][1:])
-            return 'location' in d
-    return False
-
-def parseCreate(user, fields):
-    name = fields.getfirst('name')
-    if not validation.validMachineName(name):
-        raise InvalidInput('name', name, 'You must provide a machine name.')
-    name = name.lower()
-
-    if Machine.get_by(name=name):
-        raise InvalidInput('name', name,
-                           "Name already exists.")
-    
-    owner = validation.testOwner(user, fields.getfirst('owner'))
-
-    memory = fields.getfirst('memory')
-    memory = validation.validMemory(user, memory, on=True)
-    
-    disk = fields.getfirst('disk')
-    disk = validation.validDisk(user, disk)
-
-    vm_type = fields.getfirst('vmtype')
-    if vm_type not in ('hvm', 'paravm'):
-        raise CodeError("Invalid vm type '%s'"  % vm_type)    
-    is_hvm = (vm_type == 'hvm')
-
-    cdrom = fields.getfirst('cdrom')
-    if cdrom is not None and not CDROM.get(cdrom):
-        raise CodeError("Invalid cdrom type '%s'" % cdrom)
-    return dict(contact=user, name=name, memory=memory, disk=disk,
-                owner=owner, is_hvm=is_hvm, cdrom=cdrom)
-
-def create(user, fields):
-    """Handler for create requests."""
-    try:
-        parsed_fields = parseCreate(user, fields)
-        machine = controls.createVm(**parsed_fields)
-    except InvalidInput, err:
-        pass
-    else:
-        err = None
-    g.clear() #Changed global state
-    d = getListDict(user)
-    d['err'] = err
-    if err:
-        for field in fields.keys():
-            setattr(d['defaults'], field, fields.getfirst(field))
-    else:
-        d['new_machine'] = parsed_fields['name']
-    return templates.list(searchList=[d])
-
-
-def getListDict(user):
-    machines = [m for m in Machine.select() 
-                if validation.haveAccess(user, m)]    
-    checkpoint.checkpoint('Got my machines')
-    on = {}
-    has_vnc = {}
-    on = g.uptimes
-    checkpoint.checkpoint('Got uptimes')
-    for m in machines:
-        m.uptime = g.uptimes.get(m)
-        if not on[m]:
-            has_vnc[m] = 'Off'
-        elif m.type.hvm:
-            has_vnc[m] = True
-        else:
-            has_vnc[m] = "ParaVM"+helppopup("paravm_console")
-    max_memory = validation.maxMemory(user)
-    max_disk = validation.maxDisk(user)
-    checkpoint.checkpoint('Got max mem/disk')
-    defaults = Defaults(max_memory=max_memory,
-                        max_disk=max_disk,
-                        owner=user,
-                        cdrom='gutsy-i386')
-    checkpoint.checkpoint('Got defaults')
-    d = dict(user=user,
-             cant_add_vm=validation.cantAddVm(user),
-             max_memory=max_memory,
-             max_disk=max_disk,
-             defaults=defaults,
-             machines=machines,
-             has_vnc=has_vnc,
-             uptimes=g.uptimes,
-             cdroms=CDROM.select())
-    return d
-
-def listVms(user, fields):
-    """Handler for list requests."""
-    checkpoint.checkpoint('Getting list dict')
-    d = getListDict(user)
-    checkpoint.checkpoint('Got list dict')
-    return templates.list(searchList=[d])
-            
-def vnc(user, fields):
-    """VNC applet page.
-
-    Note that due to same-domain restrictions, the applet connects to
-    the webserver, which needs to forward those requests to the xen
-    server.  The Xen server runs another proxy that (1) authenticates
-    and (2) finds the correct port for the VM.
-
-    You might want iptables like:
-
-    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
-      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 
-    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
-      --dport 10003 -j SNAT --to-source 18.187.7.142 
-    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
-      --dport 10003 -j ACCEPT
-
-    Remember to enable iptables!
-    echo 1 > /proc/sys/net/ipv4/ip_forward
-    """
-    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
-    
-    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
-
-    data = {}
-    data["user"] = user
-    data["machine"] = machine.name
-    data["expires"] = time.time()+(5*60)
-    pickled_data = cPickle.dumps(data)
-    m = hmac.new(TOKEN_KEY, digestmod=sha)
-    m.update(pickled_data)
-    token = {'data': pickled_data, 'digest': m.digest()}
-    token = cPickle.dumps(token)
-    token = base64.urlsafe_b64encode(token)
-    
-    status = controls.statusInfo(machine)
-    has_vnc = hasVnc(status)
-    
-    d = dict(user=user,
-             on=status,
-             has_vnc=has_vnc,
-             machine=machine,
-             hostname=os.environ.get('SERVER_NAME', 'localhost'),
-             authtoken=token)
-    return templates.vnc(searchList=[d])
-
-def getNicInfo(data_dict, machine):
-    """Helper function for info, get data on nics for a machine.
-
-    Modifies data_dict to include the relevant data, and returns a list
-    of (key, name) pairs to display "name: data_dict[key]" to the user.
-    """
-    data_dict['num_nics'] = len(machine.nics)
-    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
-                           ('nic%s_mac', 'NIC %s MAC Addr'),
-                           ('nic%s_ip', 'NIC %s IP'),
-                           ]
-    nic_fields = []
-    for i in range(len(machine.nics)):
-        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
-        if not i:
-            data_dict['nic%s_hostname' % i] = (machine.name + 
-                                               '.servers.csail.mit.edu')
-        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
-        data_dict['nic%s_ip' % i] = machine.nics[i].ip
-    if len(machine.nics) == 1:
-        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
-    return nic_fields
-
-def getDiskInfo(data_dict, machine):
-    """Helper function for info, get data on disks for a machine.
-
-    Modifies data_dict to include the relevant data, and returns a list
-    of (key, name) pairs to display "name: data_dict[key]" to the user.
-    """
-    data_dict['num_disks'] = len(machine.disks)
-    disk_fields_template = [('%s_size', '%s size')]
-    disk_fields = []
-    for disk in machine.disks:
-        name = disk.guest_device_name
-        disk_fields.extend([(x % name, y % name) for x, y in 
-                            disk_fields_template])
-        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
-    return disk_fields
-
-def command(user, fields):
-    """Handler for running commands like boot and delete on a VM."""
-    back = fields.getfirst('back')
-    try:
-        d = controls.commandResult(user, fields)
-        if d['command'] == 'Delete VM':
-            back = 'list'
-    except InvalidInput, err:
-        if not back:
-            raise
-        print >> sys.stderr, err
-        result = None
-    else:
-        result = 'Success!'
-        if not back:
-            return templates.command(searchList=[d])
-    if back == 'list':
-        g.clear() #Changed global state
-        d = getListDict(user)
-        d['result'] = result
-        return templates.list(searchList=[d])
-    elif back == 'info':
-        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
-        d = infoDict(user, machine)
-        d['result'] = result
-        return templates.info(searchList=[d])
-    else:
-        raise InvalidInput
-    ('back', back, 'Not a known back page.')
-
-def modifyDict(user, fields):
-    olddisk = {}
-    transaction = ctx.current.create_transaction()
-    try:
-        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
-        owner = validation.testOwner(user, fields.getfirst('owner'), machine)
-        admin = validation.testAdmin(user, fields.getfirst('administrator'),
-                                     machine)
-        contact = validation.testContact(user, fields.getfirst('contact'),
-                                         machine)
-        name = validation.testName(user, fields.getfirst('name'), machine)
-        oldname = machine.name
-        command = "modify"
-
-        memory = fields.getfirst('memory')
-        if memory is not None:
-            memory = validation.validMemory(user, memory, machine, on=False)
-            machine.memory = memory
- 
-        disksize = validation.testDisk(user, fields.getfirst('disk'))
-        if disksize is not None:
-            disksize = validation.validDisk(user, disksize, machine)
-            disk = machine.disks[0]
-            if disk.size != disksize:
-                olddisk[disk.guest_device_name] = disksize
-                disk.size = disksize
-                ctx.current.save(disk)
-        
-        if owner is not None:
-            machine.owner = owner
-        if name is not None:
-            machine.name = name
-        if admin is not None:
-            machine.administrator = admin
-        if contact is not None:
-            machine.contact = contact
-            
-        ctx.current.save(machine)
-        transaction.commit()
-    except:
-        transaction.rollback()
-        raise
-    for diskname in olddisk:
-        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
-    if name is not None:
-        controls.renameMachine(machine, oldname, name)
-    return dict(user=user,
-                command=command,
-                machine=machine)
-    
-def modify(user, fields):
-    """Handler for modifying attributes of a machine."""
-    try:
-        modify_dict = modifyDict(user, fields)
-    except InvalidInput, err:
-        result = None
-        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
-    else:
-        machine = modify_dict['machine']
-        result = 'Success!'
-        err = None
-    info_dict = infoDict(user, machine)
-    info_dict['err'] = err
-    if err:
-        for field in fields.keys():
-            setattr(info_dict['defaults'], field, fields.getfirst(field))
-    info_dict['result'] = result
-    return templates.info(searchList=[info_dict])
-    
-
-def helpHandler(user, fields):
-    """Handler for help messages."""
-    simple = fields.getfirst('simple')
-    subjects = fields.getlist('subject')
-    
-    help_mapping = dict(paravm_console="""
-ParaVM machines do not support console access over VNC.  To access
-these machines, you either need to boot with a liveCD and ssh in or
-hope that the sipb-xen maintainers add support for serial consoles.""",
-                        hvm_paravm="""
-HVM machines use the virtualization features of the processor, while
-ParaVM machines use Xen's emulation of virtualization features.  You
-want an HVM virtualized machine.""",
-                        cpu_weight="""
-Don't ask us!  We're as mystified as you are.""",
-                        owner="""
-The owner field is used to determine <a
-href="help?subject=quotas">quotas</a>.  It must be the name of a
-locker that you are an AFS administrator of.  In particular, you or an
-AFS group you are a member of must have AFS rlidwka bits on the
-locker.  You can check see who administers the LOCKER locker using the
-command 'fs la /mit/LOCKER' on Athena.)  See also <a
-href="help?subject=administrator">administrator</a>.""",
-                        administrator="""
-The administrator field determines who can access the console and
-power on and off the machine.  This can be either a user or a moira
-group.""",
-                        quotas="""
-Quotas are determined on a per-locker basis.  Each quota may have a
-maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
-active machines."""
-                   )
-    
-    if not subjects:
-        subjects = sorted(help_mapping.keys())
-        
-    d = dict(user=user,
-             simple=simple,
-             subjects=subjects,
-             mapping=help_mapping)
-    
-    return templates.help(searchList=[d])
-    
-
-def badOperation(u, e):
-    raise CodeError("Unknown operation")
-
-def infoDict(user, machine):
-    status = controls.statusInfo(machine)
-    checkpoint.checkpoint('Getting status info')
-    has_vnc = hasVnc(status)
-    if status is None:
-        main_status = dict(name=machine.name,
-                           memory=str(machine.memory))
-        uptime = None
-        cputime = None
-    else:
-        main_status = dict(status[1:])
-        start_time = float(main_status.get('start_time', 0))
-        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
-        cpu_time_float = float(main_status.get('cpu_time', 0))
-        cputime = datetime.timedelta(seconds=int(cpu_time_float))
-    checkpoint.checkpoint('Status')
-    display_fields = """name uptime memory state cpu_weight on_reboot 
-     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
-    display_fields = [('name', 'Name'),
-                      ('owner', 'Owner'),
-                      ('administrator', 'Administrator'),
-                      ('contact', 'Contact'),
-                      ('type', 'Type'),
-                      'NIC_INFO',
-                      ('uptime', 'uptime'),
-                      ('cputime', 'CPU usage'),
-                      ('memory', 'RAM'),
-                      'DISK_INFO',
-                      ('state', 'state (xen format)'),
-                      ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
-                      ('on_reboot', 'Action on VM reboot'),
-                      ('on_poweroff', 'Action on VM poweroff'),
-                      ('on_crash', 'Action on VM crash'),
-                      ('on_xend_start', 'Action on Xen start'),
-                      ('on_xend_stop', 'Action on Xen stop'),
-                      ('bootloader', 'Bootloader options'),
-                      ]
-    fields = []
-    machine_info = {}
-    machine_info['name'] = machine.name
-    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
-    machine_info['owner'] = machine.owner
-    machine_info['administrator'] = machine.administrator
-    machine_info['contact'] = machine.contact
-
-    nic_fields = getNicInfo(machine_info, machine)
-    nic_point = display_fields.index('NIC_INFO')
-    display_fields = (display_fields[:nic_point] + nic_fields + 
-                      display_fields[nic_point+1:])
-
-    disk_fields = getDiskInfo(machine_info, machine)
-    disk_point = display_fields.index('DISK_INFO')
-    display_fields = (display_fields[:disk_point] + disk_fields + 
-                      display_fields[disk_point+1:])
-    
-    main_status['memory'] += ' MiB'
-    for field, disp in display_fields:
-        if field in ('uptime', 'cputime') and locals()[field] is not None:
-            fields.append((disp, locals()[field]))
-        elif field in machine_info:
-            fields.append((disp, machine_info[field]))
-        elif field in main_status:
-            fields.append((disp, main_status[field]))
-        else:
-            pass
-            #fields.append((disp, None))
-
-    checkpoint.checkpoint('Got fields')
-
-
-    max_mem = validation.maxMemory(user, machine, False)
-    checkpoint.checkpoint('Got mem')
-    max_disk = validation.maxDisk(user, machine)
-    defaults = Defaults()
-    for name in 'machine_id name administrator owner memory contact'.split():
-        setattr(defaults, name, getattr(machine, name))
-    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
-    checkpoint.checkpoint('Got defaults')
-    d = dict(user=user,
-             cdroms=CDROM.select(),
-             on=status is not None,
-             machine=machine,
-             defaults=defaults,
-             has_vnc=has_vnc,
-             uptime=str(uptime),
-             ram=machine.memory,
-             max_mem=max_mem,
-             max_disk=max_disk,
-             owner_help=helppopup("owner"),
-             fields = fields)
-    return d
-
-def info(user, fields):
-    """Handler for info on a single VM."""
-    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
-    d = infoDict(user, machine)
-    checkpoint.checkpoint('Got infodict')
-    return templates.info(searchList=[d])
-
-mapping = dict(list=listVms,
-               vnc=vnc,
-               command=command,
-               modify=modify,
-               info=info,
-               create=create,
-               help=helpHandler)
-
-def printHeaders(headers):
-    for key, value in headers.iteritems():
-        print '%s: %s' % (key, value)
-    print
-
-
-def getUser():
-    """Return the current user based on the SSL environment variables"""
-    if 'SSL_CLIENT_S_DN_Email' in os.environ:
-        username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
-        return username
-    else:
-        return 'moo'
-
-def main(operation, user, fields):    
-    start_time = time.time()
-    fun = mapping.get(operation, badOperation)
-
-    if fun not in (helpHandler, ):
-        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
-    try:
-        checkpoint.checkpoint('Before')
-        output = fun(u, fields)
-        checkpoint.checkpoint('After')
-
-        headers = dict(DEFAULT_HEADERS)
-        if isinstance(output, tuple):
-            new_headers, output = output
-            headers.update(new_headers)
-        e = revertStandardError()
-        if e:
-            output.addError(e)
-        printHeaders(headers)
-        output_string =  str(output)
-        checkpoint.checkpoint('output as a string')
-        print output_string
-        print '<pre>%s</pre>' % checkpoint
-    except Exception, err:
-        if not fields.has_key('js'):
-            if isinstance(err, CodeError):
-                print 'Content-Type: text/html\n'
-                e = revertStandardError()
-                print error(operation, u, fields, err, e)
-                sys.exit(1)
-            if isinstance(err, InvalidInput):
-                print 'Content-Type: text/html\n'
-                e = revertStandardError()
-                print invalidInput(operation, u, fields, err, e)
-                sys.exit(1)
-        print 'Content-Type: text/plain\n'
-        print 'Uh-oh!  We experienced an error.'
-        print 'Please email sipb-xen@mit.edu with the contents of this page.'
-        print '----'
-        e = revertStandardError()
-        print e
-        print '----'
-        raise
-
-if __name__ == '__main__':
-    fields = cgi.FieldStorage()
-    u = getUser()
-    g.user = u
-    operation = os.environ.get('PATH_INFO', '')
-    if not operation:
-        print "Status: 301 Moved Permanently"
-        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
-        sys.exit(0)
-
-    if operation.startswith('/'):
-        operation = operation[1:]
-    if not operation:
-        operation = 'list'
-
-    #main(operation, u, fields)
-    import profile
-    profile.run('main(operation, u, fields)', 'log-'+operation)
-
Index: trunk/web/templates/templates.py
===================================================================
--- trunk/web/templates/templates.py	(revision 235)
+++ 	(revision )
@@ -1,9 +1,0 @@
-__all__ = 'info command error help invalid list vnc'.split()
-for _name in __all__:
-    try:
-        _module = __import__(_name, globals(), {}, [_name])
-        globals()[_name] = getattr(_module, _name)
-    except ImportError, e:
-        import sys
-        print >> sys.stderr, 'Importing template "%s" raised error: %s' % (_name, e)
-        
Index: trunk/web/templates/validation.py
===================================================================
--- trunk/web/templates/validation.py	(revision 235)
+++ 	(revision )
@@ -1,204 +1,0 @@
-#!/usr/bin/python
-
-import getafsgroups
-import re
-import string
-from sipb_xen_database import Machine, NIC
-from webcommon import InvalidInput, g
-
-MAX_MEMORY_TOTAL = 512
-MAX_MEMORY_SINGLE = 256
-MIN_MEMORY_SINGLE = 16
-MAX_DISK_TOTAL = 50
-MAX_DISK_SINGLE = 50
-MIN_DISK_SINGLE = 0.1
-MAX_VMS_TOTAL = 10
-MAX_VMS_ACTIVE = 4
-
-def getMachinesByOwner(user, machine=None):
-    """Return the machines owned by the same as a machine.
-    
-    If the machine is None, return the machines owned by the same
-    user.
-    """
-    if machine:
-        owner = machine.owner
-    else:
-        owner = user
-    return Machine.select_by(owner=owner)
-
-def maxMemory(user, machine=None, on=True):
-    """Return the maximum memory for a machine or a user.
-
-    If machine is None, return the memory available for a new 
-    machine.  Else, return the maximum that machine can have.
-
-    on is whether the machine should be turned on.  If false, the max
-    memory for the machine to change to, if it is left off, is
-    returned.
-    """
-    if not on:
-        return MAX_MEMORY_SINGLE
-    machines = getMachinesByOwner(user, machine)
-    active_machines = [x for x in machines if g.uptimes[x]]
-    mem_usage = sum([x.memory for x in active_machines if x != machine])
-    return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
-
-def maxDisk(user, machine=None):
-    machines = getMachinesByOwner(user, machine)
-    disk_usage = sum([sum([y.size for y in x.disks])
-                      for x in machines if x != machine])
-    return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
-
-def cantAddVm(user):
-    machines = getMachinesByOwner(user)
-    active_machines = [x for x in machines if g.uptimes[x]]
-    if len(machines) >= MAX_VMS_TOTAL:
-        return 'You have too many VMs to create a new one.'
-    if len(active_machines) >= MAX_VMS_ACTIVE:
-        return ('You already have the maximum number of VMs turned on.  '
-                'To create more, turn one off.')
-    return False
-
-def validAddVm(user):
-    reason = cantAddVm(user)
-    if reason:
-        raise InvalidInput('create', True, reason)
-    return True
-
-def haveAccess(user, machine):
-    """Return whether a user has administrative access to a machine"""
-    if user == 'moo':
-        return True
-    if user in (machine.administrator, machine.owner):
-        return True
-    if getafsgroups.checkAfsGroup(user, machine.administrator, 
-                                  'athena.mit.edu'): #XXX Cell?
-        return True
-    if not getafsgroups.notLockerOwner(user, machine.owner):
-        return True
-    return owns(user, machine)
-
-def owns(user, machine):
-    """Return whether a user owns a machine"""
-    if user == 'moo':
-        return True
-    return not getafsgroups.notLockerOwner(user, machine.owner)
-
-def validMachineName(name):
-    """Check that name is valid for a machine name"""
-    if not name:
-        return False
-    charset = string.ascii_letters + string.digits + '-_'
-    if name[0] in '-_' or len(name) > 22:
-        return False
-    for x in name:
-        if x not in charset:
-            return False
-    return True
-
-def validMemory(user, memory, machine=None, on=True):
-    """Parse and validate limits for memory for a given user and machine.
-
-    on is whether the memory must be valid after the machine is
-    switched on.
-    """
-    try:
-        memory = int(memory)
-        if memory < MIN_MEMORY_SINGLE:
-            raise ValueError
-    except ValueError:
-        raise InvalidInput('memory', memory, 
-                           "Minimum %s MiB" % MIN_MEMORY_SINGLE)
-    if memory > maxMemory(user, machine, on):
-        raise InvalidInput('memory', memory,
-                           'Maximum %s MiB for %s' % (maxMemory(user, machine),
-                                                      user))
-    return memory
-
-def validDisk(user, disk, machine=None):
-    """Parse and validate limits for disk for a given user and machine."""
-    try:
-        disk = float(disk)
-        if disk > maxDisk(user, machine):
-            raise InvalidInput('disk', disk,
-                               "Maximum %s G" % maxDisk(user, machine))
-        disk = int(disk * 1024)
-        if disk < MIN_DISK_SINGLE * 1024:
-            raise ValueError
-    except ValueError:
-        raise InvalidInput('disk', disk,
-                           "Minimum %s GiB" % MIN_DISK_SINGLE)
-    return disk
-            
-def testMachineId(user, machine_id, exists=True):
-    """Parse, validate and check authorization for a given user and machine.
-
-    If exists is False, don't check that it exists.
-    """
-    if machine_id is None:
-        raise InvalidInput('machine_id', machine_id, 
-                           "Must specify a machine ID.")
-    try:
-        machine_id = int(machine_id)
-    except ValueError:
-        raise InvalidInput('machine_id', machine_id, "Must be an integer.")
-    machine = Machine.get(machine_id)
-    if exists and machine is None:
-        raise InvalidInput('machine_id', machine_id, "Does not exist.")
-    if machine is not None and not haveAccess(user, machine):
-        raise InvalidInput('machine_id', machine_id,
-                           "You do not have access to this machine.")
-    return machine
-
-def testAdmin(user, admin, machine):
-    if admin in (None, machine.administrator):
-        return None
-    if admin == user:
-        return admin
-    if getafsgroups.checkAfsGroup(user, admin, 'athena.mit.edu'):
-        return admin
-    if getafsgroups.checkAfsGroup(user, 'system:'+admin,
-                                  'athena.mit.edu'):
-        return 'system:'+admin
-    return admin
-    
-def testOwner(user, owner, machine=None):
-    if owner == user or machine is not None and owner == machine.owner:
-        return owner
-    if owner is None:
-        raise InvalidInput('owner', owner, "Owner must be specified")
-    value = getafsgroups.notLockerOwner(user, owner)
-    if not value:
-        return owner
-    raise InvalidInput('owner', owner, value)
-
-def testContact(user, contact, machine=None):
-    if contact in (None, machine.contact):
-        return None
-    if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
-        raise InvalidInput('contact', contact, "Not a valid email.")
-    return contact
-
-def testDisk(user, disksize, machine=None):
-    return disksize
-
-def testName(user, name, machine=None):
-    if name in (None, machine.name):
-        return None
-    if not Machine.select_by(name=name):
-        return name
-    raise InvalidInput('name', name, "Name is already taken.")
-
-def testHostname(user, hostname, machine):
-    for nic in machine.nics:
-        if hostname == nic.hostname:
-            return hostname
-    # check if doesn't already exist
-    if NIC.select_by(hostname=hostname):
-        raise InvalidInput('hostname', hostname,
-                           "Already exists")
-    if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
-        raise InvalidInput('hostname', hostname, "Not a valid hostname; "
-                           "must only use number, letters, and dashes.")
-    return hostname
Index: trunk/web/templates/webcommon.py
===================================================================
--- trunk/web/templates/webcommon.py	(revision 235)
+++ 	(revision )
@@ -1,44 +1,0 @@
-"""Exceptions for the web interface."""
-
-from sipb_xen_database import Machine
-
-class MyException(Exception):
-    """Base class for my exceptions"""
-    pass
-
-class InvalidInput(MyException):
-    """Exception for user-provided input is invalid but maybe in good faith.
-
-    This would include setting memory to negative (which might be a
-    typo) but not setting an invalid boot CD (which requires bypassing
-    the select box).
-    """
-    def __init__(self, err_field, err_value, expl=None):
-        MyException.__init__(self, expl)
-        self.err_field = err_field
-        self.err_value = err_value
-
-class CodeError(MyException):
-    """Exception for internal errors or bad faith input."""
-    pass
-
-import controls
-
-class Global(object):
-    """Global state of the system, to avoid duplicate remctls to get state"""
-    def __init__(self, user):
-        self.user = user
-
-    def __get_uptimes(self):
-        if not hasattr(self, '_uptimes'):
-            self._uptimes = controls.getUptimes(Machine.select())
-        return self._uptimes
-    uptimes = property(__get_uptimes)
-
-    def clear(self):
-        """Clear the state so future accesses reload it."""
-        for attr in ('_uptimes', ):
-            if hasattr(self, attr):
-                delattr(self, attr)
-
-g = Global(None)
Index: trunk/web/validation.py
===================================================================
--- trunk/web/validation.py	(revision 236)
+++ trunk/web/validation.py	(revision 236)
@@ -0,0 +1,204 @@
+#!/usr/bin/python
+
+import getafsgroups
+import re
+import string
+from sipb_xen_database import Machine, NIC
+from webcommon import InvalidInput, g
+
+MAX_MEMORY_TOTAL = 512
+MAX_MEMORY_SINGLE = 256
+MIN_MEMORY_SINGLE = 16
+MAX_DISK_TOTAL = 50
+MAX_DISK_SINGLE = 50
+MIN_DISK_SINGLE = 0.1
+MAX_VMS_TOTAL = 10
+MAX_VMS_ACTIVE = 4
+
+def getMachinesByOwner(user, machine=None):
+    """Return the machines owned by the same as a machine.
+    
+    If the machine is None, return the machines owned by the same
+    user.
+    """
+    if machine:
+        owner = machine.owner
+    else:
+        owner = user
+    return Machine.select_by(owner=owner)
+
+def maxMemory(user, machine=None, on=True):
+    """Return the maximum memory for a machine or a user.
+
+    If machine is None, return the memory available for a new 
+    machine.  Else, return the maximum that machine can have.
+
+    on is whether the machine should be turned on.  If false, the max
+    memory for the machine to change to, if it is left off, is
+    returned.
+    """
+    if not on:
+        return MAX_MEMORY_SINGLE
+    machines = getMachinesByOwner(user, machine)
+    active_machines = [x for x in machines if g.uptimes[x]]
+    mem_usage = sum([x.memory for x in active_machines if x != machine])
+    return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
+
+def maxDisk(user, machine=None):
+    machines = getMachinesByOwner(user, machine)
+    disk_usage = sum([sum([y.size for y in x.disks])
+                      for x in machines if x != machine])
+    return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
+
+def cantAddVm(user):
+    machines = getMachinesByOwner(user)
+    active_machines = [x for x in machines if g.uptimes[x]]
+    if len(machines) >= MAX_VMS_TOTAL:
+        return 'You have too many VMs to create a new one.'
+    if len(active_machines) >= MAX_VMS_ACTIVE:
+        return ('You already have the maximum number of VMs turned on.  '
+                'To create more, turn one off.')
+    return False
+
+def validAddVm(user):
+    reason = cantAddVm(user)
+    if reason:
+        raise InvalidInput('create', True, reason)
+    return True
+
+def haveAccess(user, machine):
+    """Return whether a user has administrative access to a machine"""
+    if user == 'moo':
+        return True
+    if user in (machine.administrator, machine.owner):
+        return True
+    if getafsgroups.checkAfsGroup(user, machine.administrator, 
+                                  'athena.mit.edu'): #XXX Cell?
+        return True
+    if not getafsgroups.notLockerOwner(user, machine.owner):
+        return True
+    return owns(user, machine)
+
+def owns(user, machine):
+    """Return whether a user owns a machine"""
+    if user == 'moo':
+        return True
+    return not getafsgroups.notLockerOwner(user, machine.owner)
+
+def validMachineName(name):
+    """Check that name is valid for a machine name"""
+    if not name:
+        return False
+    charset = string.ascii_letters + string.digits + '-_'
+    if name[0] in '-_' or len(name) > 22:
+        return False
+    for x in name:
+        if x not in charset:
+            return False
+    return True
+
+def validMemory(user, memory, machine=None, on=True):
+    """Parse and validate limits for memory for a given user and machine.
+
+    on is whether the memory must be valid after the machine is
+    switched on.
+    """
+    try:
+        memory = int(memory)
+        if memory < MIN_MEMORY_SINGLE:
+            raise ValueError
+    except ValueError:
+        raise InvalidInput('memory', memory, 
+                           "Minimum %s MiB" % MIN_MEMORY_SINGLE)
+    if memory > maxMemory(user, machine, on):
+        raise InvalidInput('memory', memory,
+                           'Maximum %s MiB for %s' % (maxMemory(user, machine),
+                                                      user))
+    return memory
+
+def validDisk(user, disk, machine=None):
+    """Parse and validate limits for disk for a given user and machine."""
+    try:
+        disk = float(disk)
+        if disk > maxDisk(user, machine):
+            raise InvalidInput('disk', disk,
+                               "Maximum %s G" % maxDisk(user, machine))
+        disk = int(disk * 1024)
+        if disk < MIN_DISK_SINGLE * 1024:
+            raise ValueError
+    except ValueError:
+        raise InvalidInput('disk', disk,
+                           "Minimum %s GiB" % MIN_DISK_SINGLE)
+    return disk
+            
+def testMachineId(user, machine_id, exists=True):
+    """Parse, validate and check authorization for a given user and machine.
+
+    If exists is False, don't check that it exists.
+    """
+    if machine_id is None:
+        raise InvalidInput('machine_id', machine_id, 
+                           "Must specify a machine ID.")
+    try:
+        machine_id = int(machine_id)
+    except ValueError:
+        raise InvalidInput('machine_id', machine_id, "Must be an integer.")
+    machine = Machine.get(machine_id)
+    if exists and machine is None:
+        raise InvalidInput('machine_id', machine_id, "Does not exist.")
+    if machine is not None and not haveAccess(user, machine):
+        raise InvalidInput('machine_id', machine_id,
+                           "You do not have access to this machine.")
+    return machine
+
+def testAdmin(user, admin, machine):
+    if admin in (None, machine.administrator):
+        return None
+    if admin == user:
+        return admin
+    if getafsgroups.checkAfsGroup(user, admin, 'athena.mit.edu'):
+        return admin
+    if getafsgroups.checkAfsGroup(user, 'system:'+admin,
+                                  'athena.mit.edu'):
+        return 'system:'+admin
+    return admin
+    
+def testOwner(user, owner, machine=None):
+    if owner == user or machine is not None and owner == machine.owner:
+        return owner
+    if owner is None:
+        raise InvalidInput('owner', owner, "Owner must be specified")
+    value = getafsgroups.notLockerOwner(user, owner)
+    if not value:
+        return owner
+    raise InvalidInput('owner', owner, value)
+
+def testContact(user, contact, machine=None):
+    if contact in (None, machine.contact):
+        return None
+    if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
+        raise InvalidInput('contact', contact, "Not a valid email.")
+    return contact
+
+def testDisk(user, disksize, machine=None):
+    return disksize
+
+def testName(user, name, machine=None):
+    if name in (None, machine.name):
+        return None
+    if not Machine.select_by(name=name):
+        return name
+    raise InvalidInput('name', name, "Name is already taken.")
+
+def testHostname(user, hostname, machine):
+    for nic in machine.nics:
+        if hostname == nic.hostname:
+            return hostname
+    # check if doesn't already exist
+    if NIC.select_by(hostname=hostname):
+        raise InvalidInput('hostname', hostname,
+                           "Already exists")
+    if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
+        raise InvalidInput('hostname', hostname, "Not a valid hostname; "
+                           "must only use number, letters, and dashes.")
+    return hostname
Index: trunk/web/webcommon.py
===================================================================
--- trunk/web/webcommon.py	(revision 236)
+++ trunk/web/webcommon.py	(revision 236)
@@ -0,0 +1,44 @@
+"""Exceptions for the web interface."""
+
+from sipb_xen_database import Machine
+
+class MyException(Exception):
+    """Base class for my exceptions"""
+    pass
+
+class InvalidInput(MyException):
+    """Exception for user-provided input is invalid but maybe in good faith.
+
+    This would include setting memory to negative (which might be a
+    typo) but not setting an invalid boot CD (which requires bypassing
+    the select box).
+    """
+    def __init__(self, err_field, err_value, expl=None):
+        MyException.__init__(self, expl)
+        self.err_field = err_field
+        self.err_value = err_value
+
+class CodeError(MyException):
+    """Exception for internal errors or bad faith input."""
+    pass
+
+import controls
+
+class Global(object):
+    """Global state of the system, to avoid duplicate remctls to get state"""
+    def __init__(self, user):
+        self.user = user
+
+    def __get_uptimes(self):
+        if not hasattr(self, '_uptimes'):
+            self._uptimes = controls.getUptimes(Machine.select())
+        return self._uptimes
+    uptimes = property(__get_uptimes)
+
+    def clear(self):
+        """Clear the state so future accesses reload it."""
+        for attr in ('_uptimes', ):
+            if hasattr(self, attr):
+                delattr(self, attr)
+
+g = Global(None)
