Initial checkin of ajaxterm module
authorJamie Cameron <jcameron@webmin.com>
Tue, 1 Sep 2009 19:52:57 +0000 (12:52 -0700)
committerJamie Cameron <jcameron@webmin.com>
Tue, 1 Sep 2009 19:52:57 +0000 (12:52 -0700)
19 files changed:
ajaxterm/ajaxterm/README.txt [new file with mode: 0644]
ajaxterm/ajaxterm/ajaxterm.1 [new file with mode: 0644]
ajaxterm/ajaxterm/ajaxterm.css [new file with mode: 0644]
ajaxterm/ajaxterm/ajaxterm.html [new file with mode: 0644]
ajaxterm/ajaxterm/ajaxterm.js [new file with mode: 0644]
ajaxterm/ajaxterm/ajaxterm.py [new file with mode: 0755]
ajaxterm/ajaxterm/configure [new file with mode: 0755]
ajaxterm/ajaxterm/configure.ajaxterm.bin [new file with mode: 0644]
ajaxterm/ajaxterm/configure.initd.debian [new file with mode: 0644]
ajaxterm/ajaxterm/configure.initd.gentoo [new file with mode: 0644]
ajaxterm/ajaxterm/configure.initd.redhat [new file with mode: 0644]
ajaxterm/ajaxterm/configure.makefile [new file with mode: 0644]
ajaxterm/ajaxterm/qweb.py [new file with mode: 0644]
ajaxterm/ajaxterm/sarissa.js [new file with mode: 0644]
ajaxterm/ajaxterm/sarissa_dhtml.js [new file with mode: 0644]
ajaxterm/images/icon.gif [new file with mode: 0644]
ajaxterm/index.cgi [new file with mode: 0755]
ajaxterm/module.info [new file with mode: 0644]
ajaxterm/proxy.cgi [new file with mode: 0755]

diff --git a/ajaxterm/ajaxterm/README.txt b/ajaxterm/ajaxterm/README.txt
new file mode 100644 (file)
index 0000000..4b0ae99
--- /dev/null
@@ -0,0 +1,120 @@
+= [http://antony.lesuisse.org/qweb/trac/wiki/AjaxTerm Ajaxterm] =\r
+\r
+Ajaxterm is a web based terminal. It was totally inspired and works almost\r
+exactly like http://anyterm.org/ except it's much easier to install (see\r
+comparaison with anyterm below).\r
+\r
+Ajaxterm written in python (and some AJAX javascript for client side) and depends only on python2.3 or better.[[BR]]\r
+Ajaxterm is '''very simple to install''' on Linux, MacOS X, FreeBSD, Solaris, cygwin and any Unix that runs python2.3.[[BR]]\r
+Ajaxterm was written by Antony Lesuisse (email: al AT udev.org), License Public Domain.\r
+\r
+Use the [/qweb/forum/viewforum.php?id=2 Forum], if you have any question or remark.\r
+\r
+== News ==\r
+\r
+ * 2006-10-29: v0.10 allow space in login, cgi launch fix, redhat init\r
+ * 2006-07-12: v0.9 change uid, daemon fix (Daniel Fischer)\r
+ * 2006-07-04: v0.8 add login support to ssh (Sven Geggus), change max width to 256\r
+ * 2006-05-31: v0.7 minor fixes, daemon option\r
+ * 2006-05-23: v0.6 Applied debian and gentoo patches, renamed to Ajaxterm, default port 8022\r
+\r
+== Download and Install ==\r
+\r
+ * Release: [/qweb/files/Ajaxterm-0.10.tar.gz Ajaxterm-0.10.tar.gz]\r
+ * Browse src: [/qweb/trac/browser/trunk/ajaxterm/ ajaxterm/]\r
+\r
+To install Ajaxterm issue the following commands:\r
+{{{\r
+wget http://antony.lesuisse.org/qweb/files/Ajaxterm-0.10.tar.gz\r
+tar zxvf Ajaxterm-0.10.tar.gz\r
+cd Ajaxterm-0.10\r
+./ajaxterm.py\r
+}}}\r
+Then point your browser to this URL : http://localhost:8022/\r
+\r
+== Screenshot ==\r
+\r
+{{{\r
+#!html\r
+<center><img src="/qweb/trac/attachment/wiki/AjaxTerm/scr.png?format=raw" alt="ajaxterm screenshot" style=""/></center>\r
+}}}\r
+\r
+== Documentation and Caveats ==\r
+\r
+ * Ajaxterm only support latin1, if you use Ubuntu or any LANG==en_US.UTF-8 distribution don't forget to "unset LANG".\r
+\r
+ * If run as root ajaxterm will run /bin/login, otherwise it will run ssh\r
+   localhost. To use an other command use the -c option.\r
+\r
+ * By default Ajaxterm only listen at 127.0.0.1:8022. For remote access, it is\r
+   strongly recommended to use '''https SSL/TLS''', and that is simple to\r
+   configure if you use the apache web server using mod_proxy.[[BR]][[BR]]\r
+   Using ssl will also speed up ajaxterm (probably because of keepalive).[[BR]][[BR]]\r
+   Here is an configuration example:\r
+\r
+{{{\r
+    Listen 443\r
+    NameVirtualHost *:443\r
+\r
+    <VirtualHost *:443>\r
+       ServerName localhost\r
+       SSLEngine On\r
+       SSLCertificateKeyFile ssl/apache.pem\r
+       SSLCertificateFile ssl/apache.pem\r
+\r
+       ProxyRequests Off\r
+       <Proxy *>\r
+               Order deny,allow\r
+               Allow from all\r
+       </Proxy>\r
+       ProxyPass /ajaxterm/ http://localhost:8022/\r
+       ProxyPassReverse /ajaxterm/ http://localhost:8022/\r
+    </VirtualHost>\r
+}}}\r
+\r
+ * Using GET HTTP request seems to speed up ajaxterm, just click on GET in the\r
+   interface, but be warned that your keystrokes might be loggued (by apache or\r
+   any proxy). I usually enable it after the login.\r
+\r
+ * Ajaxterm commandline usage:\r
+\r
+{{{\r
+usage: ajaxterm.py [options]\r
+\r
+options:\r
+  -h, --help            show this help message and exit\r
+  -pPORT, --port=PORT   Set the TCP port (default: 8022)\r
+  -cCMD, --command=CMD  set the command (default: /bin/login or ssh localhost)\r
+  -l, --log             log requests to stderr (default: quiet mode)\r
+  -d, --daemon          run as daemon in the background\r
+  -PPIDFILE, --pidfile=PIDFILE\r
+                        set the pidfile (default: /var/run/ajaxterm.pid)\r
+  -iINDEX_FILE, --index=INDEX_FILE\r
+                        default index file (default: ajaxterm.html)\r
+  -uUID, --uid=UID      Set the daemon's user id\r
+}}}\r
+\r
+ * Ajaxterm was first written as a demo for qweb (my web framework), but\r
+   actually doesn't use many features of qweb.\r
+\r
+ * Compared to anyterm:\r
+   * There are no partial updates, ajaxterm updates either all the screen or\r
+     nothing. That make the code simpler and I also think it's faster. HTTP\r
+     replies are always gzencoded. When used in 80x25 mode, almost all of\r
+     them are below the 1500 bytes (size of an ethernet frame) and we just\r
+     replace the screen with the reply (no javascript string handling).\r
+   * Ajaxterm polls the server for updates with an exponentially growing\r
+     timeout when the screen hasn't changed. The timeout is also resetted as\r
+     soon as a key is pressed. Anyterm blocks on a pending request and use a\r
+     parallel connection for keypresses. The anyterm approch is better\r
+     when there aren't any keypress.\r
+\r
+ * Ajaxterm files are released in the Public Domain, (except [http://sarissa.sourceforge.net/doc/ sarissa*] which are LGPL).\r
+\r
+== TODO ==\r
+\r
+ * insert mode ESC [ 4 h\r
+ * change size x,y from gui (sending signal)\r
+ * vt102 graphic codepage\r
+ * use innerHTML or prototype instead of sarissa\r
+\r
diff --git a/ajaxterm/ajaxterm/ajaxterm.1 b/ajaxterm/ajaxterm/ajaxterm.1
new file mode 100644 (file)
index 0000000..46f2acb
--- /dev/null
@@ -0,0 +1,35 @@
+.TH ajaxterm "1" "May 2006" "ajaxterm 0.5" "User commands"
+.SH NAME
+ajaxterm \- Web based terminal written in python
+
+.SH DESCRITPION
+\fBajaxterm\fR is a web based terminal written in python and some AJAX
+javascript for client side.
+It can use almost any web browser and even works through firewalls.
+
+.SH USAGE
+\fBajaxterm.py\fR [options]
+
+.SH OPTIONS
+A summary of the options supported by \fBajaxterm\fR is included below.
+    \fB-h, --help\fR            show this help message and exit
+    \fB-pPORT, --port=PORT\fR   Set the TCP port (default: 8022)
+    \fB-cCMD, --command=CMD\fR  set the command (default: /bin/login or ssh localhost)
+    \fB-l, --log\fR             log requests to stderr (default: quiet mode)
+
+.SH AUTHOR
+Antony Lesuisse <al@udev.org>
+
+This manual page was written for the Debian system by
+Julien Valroff <julien@kirya.net> (but may be used by others).
+
+.SH "REPORTING BUGS"
+Report any bugs to the author: Antony Lesuisse <al@udev.org>
+
+.SH COPYRIGHT
+Copyright Antony Lesuisse <al@udev.org>
+
+.SH SEE ALSO
+- \fBajaxterm\fR wiki page: http://antony.lesuisse.org/qweb/trac/wiki/AjaxTerm
+.br
+- \fBajaxterm\fR forum: http://antony.lesuisse.org/qweb/forum/viewforum.php?id=2
diff --git a/ajaxterm/ajaxterm/ajaxterm.css b/ajaxterm/ajaxterm/ajaxterm.css
new file mode 100644 (file)
index 0000000..b9a5f87
--- /dev/null
@@ -0,0 +1,64 @@
+pre.stat {
+       margin: 0px;
+       padding: 4px;
+       display: block;
+       font-family: monospace;
+       white-space: pre;
+       background-color: black;
+       border-top: 1px solid black;
+       color: white;
+}
+pre.stat span {
+       padding: 0px;
+}
+pre.stat .on {
+       background-color: #080;
+       font-weight: bold;
+       color: white;
+       cursor: pointer;
+}
+pre.stat .off {
+       background-color: #888;
+       font-weight: bold;
+       color: white;
+       cursor: pointer;
+}
+pre.term {
+       margin: 0px;
+       padding: 4px;
+       display: block;
+       font-family: monospace;
+       white-space: pre;
+       background-color: black;
+       border-top: 1px solid white;
+       color: #eee;
+}
+pre.term span.f0  { color: #000; }
+pre.term span.f1  { color: #b00; }
+pre.term span.f2  { color: #0b0; }
+pre.term span.f3  { color: #bb0; }
+pre.term span.f4  { color: #00b; }
+pre.term span.f5  { color: #b0b; }
+pre.term span.f6  { color: #0bb; }
+pre.term span.f7  { color: #bbb; }
+pre.term span.f8  { color: #666; }
+pre.term span.f9  { color: #f00; }
+pre.term span.f10 { color: #0f0; }
+pre.term span.f11 { color: #ff0; }
+pre.term span.f12 { color: #00f; }
+pre.term span.f13 { color: #f0f; }
+pre.term span.f14 { color: #0ff; }
+pre.term span.f15 { color: #fff; }
+pre.term span.b0  { background-color: #000; }
+pre.term span.b1  { background-color: #b00; }
+pre.term span.b2  { background-color: #0b0; }
+pre.term span.b3  { background-color: #bb0; }
+pre.term span.b4  { background-color: #00b; }
+pre.term span.b5  { background-color: #b0b; }
+pre.term span.b6  { background-color: #0bb; }
+pre.term span.b7  { background-color: #bbb; }
+
+body { background-color: #888; }
+#term {
+       float: left;
+}
diff --git a/ajaxterm/ajaxterm/ajaxterm.html b/ajaxterm/ajaxterm/ajaxterm.html
new file mode 100644 (file)
index 0000000..9edf759
--- /dev/null
@@ -0,0 +1,19 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+       <title>Ajaxterm</title>
+       <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
+       <link rel="stylesheet" type="text/css" href="ajaxterm.css"/>
+       <script type="text/javascript" src="sarissa.js"></script>
+       <script type="text/javascript" src="sarissa_dhtml.js"></script>
+       <script type="text/javascript" src="ajaxterm.js"></script>
+       <script type="text/javascript">
+       window.onload=function() {
+               t=ajaxterm.Terminal("term",80,25);
+       };
+       </script>
+</head>
+<body>
+<div id="term"></div>
+</body>
+</html>
diff --git a/ajaxterm/ajaxterm/ajaxterm.js b/ajaxterm/ajaxterm/ajaxterm.js
new file mode 100644 (file)
index 0000000..07eca3b
--- /dev/null
@@ -0,0 +1,279 @@
+ajaxterm={};
+ajaxterm.Terminal_ctor=function(id,width,height) {
+       var ie=0;
+       if(window.ActiveXObject)
+               ie=1;
+       var sid=""+Math.round(Math.random()*1000000000);
+       var query0="s="+sid+"&w="+width+"&h="+height;
+       var query1=query0+"&c=1&k=";
+       var buf="";
+       var timeout;
+       var error_timeout;
+       var keybuf=[];
+       var sending=0;
+       var rmax=1;
+
+       var div=document.getElementById(id);
+       var dstat=document.createElement('pre');
+       var sled=document.createElement('span');
+       var opt_get=document.createElement('a');
+       var opt_color=document.createElement('a');
+       var opt_paste=document.createElement('a');
+       var sdebug=document.createElement('span');
+       var dterm=document.createElement('div');
+
+       function debug(s) {
+               sdebug.innerHTML=s;
+       }
+       function error() {
+               sled.className='off';
+               debug("Connection lost timeout ts:"+((new Date).getTime()));
+       }
+       function opt_add(opt,name) {
+               opt.className='off';
+               opt.innerHTML=' '+name+' ';
+               dstat.appendChild(opt);
+               dstat.appendChild(document.createTextNode(' '));
+       }
+       function do_get(event) {
+               opt_get.className=(opt_get.className=='off')?'on':'off';
+               debug('GET '+opt_get.className);
+       }
+       function do_color(event) {
+               var o=opt_color.className=(opt_color.className=='off')?'on':'off';
+               if(o=='on')
+                       query1=query0+"&c=1&k=";
+               else
+                       query1=query0+"&k=";
+               debug('Color '+opt_color.className);
+       }
+       function mozilla_clipboard() {
+                // mozilla sucks
+               try {
+                       netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+               } catch (err) {
+                       debug('Access denied, <a href="http://kb.mozillazine.org/Granting_JavaScript_access_to_the_clipboard" target="_blank">more info</a>');
+                       return undefined;
+               }
+               var clip = Components.classes["@mozilla.org/widget/clipboard;1"].createInstance(Components.interfaces.nsIClipboard);
+               var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
+               if (!clip || !trans) {
+                       return undefined;
+               }
+               trans.addDataFlavor("text/unicode");
+               clip.getData(trans,clip.kGlobalClipboard);
+               var str=new Object();
+               var strLength=new Object();
+               try {
+                       trans.getTransferData("text/unicode",str,strLength);
+               } catch(err) {
+                       return "";
+               }
+               if (str) {
+                       str=str.value.QueryInterface(Components.interfaces.nsISupportsString);
+               }
+               if (str) {
+                       return str.data.substring(0,strLength.value / 2);
+               } else {
+                       return "";
+               }
+       }
+       function do_paste(event) {
+               var p=undefined;
+               if (window.clipboardData) {
+                       p=window.clipboardData.getData("Text");
+               } else if(window.netscape) {
+                       p=mozilla_clipboard();
+               }
+               if (p) {
+                       debug('Pasted');
+                       queue(encodeURIComponent(p));
+               } else {
+               }
+       }
+       function update() {
+//             debug("ts: "+((new Date).getTime())+" rmax:"+rmax);
+               if(sending==0) {
+                       sending=1;
+                       sled.className='on';
+                       var r=new XMLHttpRequest();
+                       var send="";
+                       while(keybuf.length>0) {
+                               send+=keybuf.pop();
+                       }
+                       var query=query1+send;
+                       if(opt_get.className=='on') {
+                               r.open("GET","u?"+query,true);
+                               if(ie) {
+                                       r.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
+                               }
+                       } else {
+                               r.open("POST","u",true);
+                       }
+                       r.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
+                       r.onreadystatechange = function () {
+//                             debug("xhr:"+((new Date).getTime())+" state:"+r.readyState+" status:"+r.status+" statusText:"+r.statusText);
+                               if (r.readyState==4) {
+                                       if(r.status==200) {
+                                               window.clearTimeout(error_timeout);
+                                               de=r.responseXML.documentElement;
+                                               if(de.tagName=="pre") {
+                                                       if(ie) {
+                                                               Sarissa.updateContentFromNode(de, dterm);
+                                                       } else {
+                                                               Sarissa.updateContentFromNode(de, dterm);
+//                                                             old=div.firstChild;
+//                                                             div.replaceChild(de,old);
+                                                       }
+                                                       rmax=100;
+                                               } else {
+                                                       rmax*=2;
+                                                       if(rmax>2000)
+                                                               rmax=2000;
+                                               }
+                                               sending=0;
+                                               sled.className='off';
+                                               timeout=window.setTimeout(update,rmax);
+                                       } else {
+                                               debug("Connection error status:"+r.status);
+                                       }
+                               }
+                       }
+                       error_timeout=window.setTimeout(error,5000);
+                       if(opt_get.className=='on') {
+                               r.send(null);
+                       } else {
+                               r.send(query);
+                       }
+               }
+       }
+       function queue(s) {
+               keybuf.unshift(s);
+               if(sending==0) {
+                       window.clearTimeout(timeout);
+                       timeout=window.setTimeout(update,1);
+               }
+       }
+       function keypress(ev) {
+               if (!ev) var ev=window.event;
+//             s="kp keyCode="+ev.keyCode+" which="+ev.which+" shiftKey="+ev.shiftKey+" ctrlKey="+ev.ctrlKey+" altKey="+ev.altKey;
+//             debug(s);
+//             return false;
+//             else { if (!ev.ctrlKey || ev.keyCode==17) { return; }
+               var kc;
+               var k="";
+               if (ev.keyCode)
+                       kc=ev.keyCode;
+               if (ev.which)
+                       kc=ev.which;
+               if (ev.altKey) {
+                       if (kc>=65 && kc<=90)
+                               kc+=32;
+                       if (kc>=97 && kc<=122) {
+                               k=String.fromCharCode(27)+String.fromCharCode(kc);
+                       }
+               } else if (ev.ctrlKey) {
+                       if (kc>=65 && kc<=90) k=String.fromCharCode(kc-64); // Ctrl-A..Z
+                       else if (kc>=97 && kc<=122) k=String.fromCharCode(kc-96); // Ctrl-A..Z
+                       else if (kc==54)  k=String.fromCharCode(30); // Ctrl-^
+                       else if (kc==109) k=String.fromCharCode(31); // Ctrl-_
+                       else if (kc==219) k=String.fromCharCode(27); // Ctrl-[
+                       else if (kc==220) k=String.fromCharCode(28); // Ctrl-\
+                       else if (kc==221) k=String.fromCharCode(29); // Ctrl-]
+                       else if (kc==219) k=String.fromCharCode(29); // Ctrl-]
+                       else if (kc==219) k=String.fromCharCode(0);  // Ctrl-@
+               } else if (ev.which==0) {
+                       if (kc==9) k=String.fromCharCode(9);  // Tab
+                       else if (kc==8) k=String.fromCharCode(127);  // Backspace
+                       else if (kc==27) k=String.fromCharCode(27); // Escape
+                       else {
+                               if (kc==33) k="[5~";        // PgUp
+                               else if (kc==34) k="[6~";   // PgDn
+                               else if (kc==35) k="[4~";   // End
+                               else if (kc==36) k="[1~";   // Home
+                               else if (kc==37) k="[D";    // Left
+                               else if (kc==38) k="[A";    // Up
+                               else if (kc==39) k="[C";    // Right
+                               else if (kc==40) k="[B";    // Down
+                               else if (kc==45) k="[2~";   // Ins
+                               else if (kc==46) k="[3~";   // Del
+                               else if (kc==112) k="[[A";  // F1
+                               else if (kc==113) k="[[B";  // F2
+                               else if (kc==114) k="[[C";  // F3
+                               else if (kc==115) k="[[D";  // F4
+                               else if (kc==116) k="[[E";  // F5
+                               else if (kc==117) k="[17~"; // F6
+                               else if (kc==118) k="[18~"; // F7
+                               else if (kc==119) k="[19~"; // F8
+                               else if (kc==120) k="[20~"; // F9
+                               else if (kc==121) k="[21~"; // F10
+                               else if (kc==122) k="[23~"; // F11
+                               else if (kc==123) k="[24~"; // F12
+                               if (k.length) {
+                                       k=String.fromCharCode(27)+k;
+                               }
+                       }
+               } else {
+                       if (kc==8)
+                               k=String.fromCharCode(127);  // Backspace
+                       else
+                               k=String.fromCharCode(kc);
+               }
+               if(k.length) {
+//                     queue(encodeURIComponent(k));
+                       if(k=="+") {
+                               queue("%2B");
+                       } else {
+                               queue(escape(k));
+                       }
+               }
+               ev.cancelBubble=true;
+               if (ev.stopPropagation) ev.stopPropagation();
+               if (ev.preventDefault)  ev.preventDefault();
+               return false;
+       }
+       function keydown(ev) {
+               if (!ev) var ev=window.event;
+               if (ie) {
+//                     s="kd keyCode="+ev.keyCode+" which="+ev.which+" shiftKey="+ev.shiftKey+" ctrlKey="+ev.ctrlKey+" altKey="+ev.altKey;
+//                     debug(s);
+                       o={9:1,8:1,27:1,33:1,34:1,35:1,36:1,37:1,38:1,39:1,40:1,45:1,46:1,112:1,
+                       113:1,114:1,115:1,116:1,117:1,118:1,119:1,120:1,121:1,122:1,123:1};
+                       if (o[ev.keyCode] || ev.ctrlKey || ev.altKey) {
+                               ev.which=0;
+                               return keypress(ev);
+                       }
+               }
+       }
+       function init() {
+               sled.appendChild(document.createTextNode('\xb7'));
+               sled.className='off';
+               dstat.appendChild(sled);
+               dstat.appendChild(document.createTextNode(' '));
+               opt_add(opt_color,'Colors');
+               opt_color.className='on';
+               opt_add(opt_get,'GET');
+               opt_add(opt_paste,'Paste');
+               dstat.appendChild(sdebug);
+               dstat.className='stat';
+               div.appendChild(dstat);
+               div.appendChild(dterm);
+               if(opt_color.addEventListener) {
+                       opt_get.addEventListener('click',do_get,true);
+                       opt_color.addEventListener('click',do_color,true);
+                       opt_paste.addEventListener('click',do_paste,true);
+               } else {
+                       opt_get.attachEvent("onclick", do_get);
+                       opt_color.attachEvent("onclick", do_color);
+                       opt_paste.attachEvent("onclick", do_paste);
+               }
+               document.onkeypress=keypress;
+               document.onkeydown=keydown;
+               timeout=window.setTimeout(update,100);
+       }
+       init();
+}
+ajaxterm.Terminal=function(id,width,height) {
+       return new this.Terminal_ctor(id,width,height);
+}
+
diff --git a/ajaxterm/ajaxterm/ajaxterm.py b/ajaxterm/ajaxterm/ajaxterm.py
new file mode 100755 (executable)
index 0000000..8b3af37
--- /dev/null
@@ -0,0 +1,567 @@
+#!/usr/bin/env python
+
+""" Ajaxterm """
+
+import array,cgi,fcntl,glob,mimetypes,optparse,os,pty,random,re,signal,select,sys,threading,time,termios,struct,pwd
+
+os.chdir(os.path.normpath(os.path.dirname(__file__)))
+# Optional: Add QWeb in sys path
+sys.path[0:0]=glob.glob('../../python')
+
+import qweb
+
+class Terminal:
+       def __init__(self,width=80,height=24):
+               self.width=width
+               self.height=height
+               self.init()
+               self.reset()
+       def init(self):
+               self.esc_seq={
+                       "\x00": None,
+                       "\x05": self.esc_da,
+                       "\x07": None,
+                       "\x08": self.esc_0x08,
+                       "\x09": self.esc_0x09,
+                       "\x0a": self.esc_0x0a,
+                       "\x0b": self.esc_0x0a,
+                       "\x0c": self.esc_0x0a,
+                       "\x0d": self.esc_0x0d,
+                       "\x0e": None,
+                       "\x0f": None,
+                       "\x1b#8": None,
+                       "\x1b=": None,
+                       "\x1b>": None,
+                       "\x1b(0": None,
+                       "\x1b(A": None,
+                       "\x1b(B": None,
+                       "\x1b[c": self.esc_da,
+                       "\x1b[0c": self.esc_da,
+                       "\x1b]R": None,
+                       "\x1b7": self.esc_save,
+                       "\x1b8": self.esc_restore,
+                       "\x1bD": None,
+                       "\x1bE": None,
+                       "\x1bH": None,
+                       "\x1bM": self.esc_ri,
+                       "\x1bN": None,
+                       "\x1bO": None,
+                       "\x1bZ": self.esc_da,
+                       "\x1ba": None,
+                       "\x1bc": self.reset,
+                       "\x1bn": None,
+                       "\x1bo": None,
+               }
+               for k,v in self.esc_seq.items():
+                       if v==None:
+                               self.esc_seq[k]=self.esc_ignore
+               # regex
+               d={
+                       r'\[\??([0-9;]*)([@ABCDEFGHJKLMPXacdefghlmnqrstu`])' : self.csi_dispatch,
+                       r'\]([^\x07]+)\x07' : self.esc_ignore,
+               }
+               self.esc_re=[]
+               for k,v in d.items():
+                       self.esc_re.append((re.compile('\x1b'+k),v))
+               # define csi sequences
+               self.csi_seq={
+                       '@': (self.csi_at,[1]),
+                       '`': (self.csi_G,[1]),
+                       'J': (self.csi_J,[0]),
+                       'K': (self.csi_K,[0]),
+               }
+               for i in [i[4] for i in dir(self) if i.startswith('csi_') and len(i)==5]:
+                       if not self.csi_seq.has_key(i):
+                               self.csi_seq[i]=(getattr(self,'csi_'+i),[1])
+               # Init 0-256 to latin1 and html translation table
+               self.trl1=""
+               for i in range(256):
+                       if i<32:
+                               self.trl1+=" "
+                       elif i<127 or i>160:
+                               self.trl1+=chr(i)
+                       else:
+                               self.trl1+="?"
+               self.trhtml=""
+               for i in range(256):
+                       if i==0x0a or (i>32 and i<127) or i>160:
+                               self.trhtml+=chr(i)
+                       elif i<=32:
+                               self.trhtml+="\xa0"
+                       else:
+                               self.trhtml+="?"
+       def reset(self,s=""):
+               self.scr=array.array('i',[0x000700]*(self.width*self.height))
+               self.st=0
+               self.sb=self.height-1
+               self.cx_bak=self.cx=0
+               self.cy_bak=self.cy=0
+               self.cl=0
+               self.sgr=0x000700
+               self.buf=""
+               self.outbuf=""
+               self.last_html=""
+       def peek(self,y1,x1,y2,x2):
+               return self.scr[self.width*y1+x1:self.width*y2+x2]
+       def poke(self,y,x,s):
+               pos=self.width*y+x
+               self.scr[pos:pos+len(s)]=s
+       def zero(self,y1,x1,y2,x2):
+               w=self.width*(y2-y1)+x2-x1+1
+               z=array.array('i',[0x000700]*w)
+               self.scr[self.width*y1+x1:self.width*y2+x2+1]=z
+       def scroll_up(self,y1,y2):
+               self.poke(y1,0,self.peek(y1+1,0,y2,self.width))
+               self.zero(y2,0,y2,self.width-1)
+       def scroll_down(self,y1,y2):
+               self.poke(y1+1,0,self.peek(y1,0,y2-1,self.width))
+               self.zero(y1,0,y1,self.width-1)
+       def scroll_right(self,y,x):
+               self.poke(y,x+1,self.peek(y,x,y,self.width))
+               self.zero(y,x,y,x)
+       def cursor_down(self):
+               if self.cy>=self.st and self.cy<=self.sb:
+                       self.cl=0
+                       q,r=divmod(self.cy+1,self.sb+1)
+                       if q:
+                               self.scroll_up(self.st,self.sb)
+                               self.cy=self.sb
+                       else:
+                               self.cy=r
+       def cursor_right(self):
+               q,r=divmod(self.cx+1,self.width)
+               if q:
+                       self.cl=1
+               else:
+                       self.cx=r
+       def echo(self,c):
+               if self.cl:
+                       self.cursor_down()
+                       self.cx=0
+               self.scr[(self.cy*self.width)+self.cx]=self.sgr|ord(c)
+               self.cursor_right()
+       def esc_0x08(self,s):
+               self.cx=max(0,self.cx-1)
+       def esc_0x09(self,s):
+               x=self.cx+8
+               q,r=divmod(x,8)
+               self.cx=(q*8)%self.width
+       def esc_0x0a(self,s):
+               self.cursor_down()
+       def esc_0x0d(self,s):
+               self.cl=0
+               self.cx=0
+       def esc_save(self,s):
+               self.cx_bak=self.cx
+               self.cy_bak=self.cy
+       def esc_restore(self,s):
+               self.cx=self.cx_bak
+               self.cy=self.cy_bak
+               self.cl=0
+       def esc_da(self,s):
+               self.outbuf="\x1b[?6c"
+       def esc_ri(self,s):
+               self.cy=max(self.st,self.cy-1)
+               if self.cy==self.st:
+                       self.scroll_down(self.st,self.sb)
+       def esc_ignore(self,*s):
+               pass
+#              print "term:ignore: %s"%repr(s)
+       def csi_dispatch(self,seq,mo):
+       # CSI sequences
+               s=mo.group(1)
+               c=mo.group(2)
+               f=self.csi_seq.get(c,None)
+               if f:
+                       try:
+                               l=[min(int(i),1024) for i in s.split(';') if len(i)<4]
+                       except ValueError:
+                               l=[]
+                       if len(l)==0:
+                               l=f[1]
+                       f[0](l)
+#              else:
+#                      print 'csi ignore',c,l
+       def csi_at(self,l):
+               for i in range(l[0]):
+                       self.scroll_right(self.cy,self.cx)
+       def csi_A(self,l):
+               self.cy=max(self.st,self.cy-l[0])
+       def csi_B(self,l):
+               self.cy=min(self.sb,self.cy+l[0])
+       def csi_C(self,l):
+               self.cx=min(self.width-1,self.cx+l[0])
+               self.cl=0
+       def csi_D(self,l):
+               self.cx=max(0,self.cx-l[0])
+               self.cl=0
+       def csi_E(self,l):
+               self.csi_B(l)
+               self.cx=0
+               self.cl=0
+       def csi_F(self,l):
+               self.csi_A(l)
+               self.cx=0
+               self.cl=0
+       def csi_G(self,l):
+               self.cx=min(self.width,l[0])-1
+       def csi_H(self,l):
+               if len(l)<2: l=[1,1]
+               self.cx=min(self.width,l[1])-1
+               self.cy=min(self.height,l[0])-1
+               self.cl=0
+       def csi_J(self,l):
+               if l[0]==0:
+                       self.zero(self.cy,self.cx,self.height-1,self.width-1)
+               elif l[0]==1:
+                       self.zero(0,0,self.cy,self.cx)
+               elif l[0]==2:
+                       self.zero(0,0,self.height-1,self.width-1)
+       def csi_K(self,l):
+               if l[0]==0:
+                       self.zero(self.cy,self.cx,self.cy,self.width-1)
+               elif l[0]==1:
+                       self.zero(self.cy,0,self.cy,self.cx)
+               elif l[0]==2:
+                       self.zero(self.cy,0,self.cy,self.width-1)
+       def csi_L(self,l):
+               for i in range(l[0]):
+                       if self.cy<self.sb:
+                               self.scroll_down(self.cy,self.sb)
+       def csi_M(self,l):
+               if self.cy>=self.st and self.cy<=self.sb:
+                       for i in range(l[0]):
+                               self.scroll_up(self.cy,self.sb)
+       def csi_P(self,l):
+               w,cx,cy=self.width,self.cx,self.cy
+               end=self.peek(cy,cx,cy,w)
+               self.csi_K([0])
+               self.poke(cy,cx,end[l[0]:])
+       def csi_X(self,l):
+               self.zero(self.cy,self.cx,self.cy,self.cx+l[0])
+       def csi_a(self,l):
+               self.csi_C(l)
+       def csi_c(self,l):
+               #'\x1b[?0c' 0-8 cursor size
+               pass
+       def csi_d(self,l):
+               self.cy=min(self.height,l[0])-1
+       def csi_e(self,l):
+               self.csi_B(l)
+       def csi_f(self,l):
+               self.csi_H(l)
+       def csi_h(self,l):
+               if l[0]==4:
+                       pass
+#                      print "insert on"
+       def csi_l(self,l):
+               if l[0]==4:
+                       pass
+#                      print "insert off"
+       def csi_m(self,l):
+               for i in l:
+                       if i==0 or i==39 or i==49 or i==27:
+                               self.sgr=0x000700
+                       elif i==1:
+                               self.sgr=(self.sgr|0x000800)
+                       elif i==7:
+                               self.sgr=0x070000
+                       elif i>=30 and i<=37:
+                               c=i-30
+                               self.sgr=(self.sgr&0xff08ff)|(c<<8)
+                       elif i>=40 and i<=47:
+                               c=i-40
+                               self.sgr=(self.sgr&0x00ffff)|(c<<16)
+#                      else:
+#                              print "CSI sgr ignore",l,i
+#              print 'sgr: %r %x'%(l,self.sgr)
+       def csi_r(self,l):
+               if len(l)<2: l=[0,self.height]
+               self.st=min(self.height-1,l[0]-1)
+               self.sb=min(self.height-1,l[1]-1)
+               self.sb=max(self.st,self.sb)
+       def csi_s(self,l):
+               self.esc_save(0)
+       def csi_u(self,l):
+               self.esc_restore(0)
+       def escape(self):
+               e=self.buf
+               if len(e)>32:
+#                      print "error %r"%e
+                       self.buf=""
+               elif e in self.esc_seq:
+                       self.esc_seq[e](e)
+                       self.buf=""
+               else:
+                       for r,f in self.esc_re:
+                               mo=r.match(e)
+                               if mo:
+                                       f(e,mo)
+                                       self.buf=""
+                                       break
+#              if self.buf=='': print "ESC %r\n"%e
+       def write(self,s):
+               for i in s:
+                       if len(self.buf) or (i in self.esc_seq):
+                               self.buf+=i
+                               self.escape()
+                       elif i == '\x1b':
+                               self.buf+=i
+                       else:
+                               self.echo(i)
+       def read(self):
+               b=self.outbuf
+               self.outbuf=""
+               return b
+       def dump(self):
+               r=''
+               for i in self.scr:
+                       r+=chr(i&255)
+               return r
+       def dumplatin1(self):
+               return self.dump().translate(self.trl1)
+       def dumphtml(self,color=1):
+               h=self.height
+               w=self.width
+               r=""
+               span=""
+               span_bg,span_fg=-1,-1
+               for i in range(h*w):
+                       q,c=divmod(self.scr[i],256)
+                       if color:
+                               bg,fg=divmod(q,256)
+                       else:
+                               bg,fg=0,7
+                       if i==self.cy*w+self.cx:
+                               bg,fg=1,7
+                       if (bg!=span_bg or fg!=span_fg or i==h*w-1):
+                               if len(span):
+                                       r+='<span class="f%d b%d">%s</span>'%(span_fg,span_bg,cgi.escape(span.translate(self.trhtml)))
+                               span=""
+                               span_bg,span_fg=bg,fg
+                       span+=chr(c)
+                       if i%w==w-1:
+                               span+='\n'
+               r='<?xml version="1.0" encoding="ISO-8859-1"?><pre class="term">%s</pre>'%r
+               if self.last_html==r:
+                       return '<?xml version="1.0"?><idem></idem>'
+               else:
+                       self.last_html=r
+#                      print self
+                       return r
+       def __repr__(self):
+               d=self.dumplatin1()
+               r=""
+               for i in range(self.height):
+                       r+="|%s|\n"%d[self.width*i:self.width*(i+1)]
+               return r
+
+class SynchronizedMethod:
+       def __init__(self,lock,orig):
+               self.lock=lock
+               self.orig=orig
+       def __call__(self,*l):
+               self.lock.acquire()
+               r=self.orig(*l)
+               self.lock.release()
+               return r
+
+class Multiplex:
+       def __init__(self,cmd=None):
+               signal.signal(signal.SIGCHLD, signal.SIG_IGN)
+               self.cmd=cmd
+               self.proc={}
+               self.lock=threading.RLock()
+               self.thread=threading.Thread(target=self.loop)
+               self.alive=1
+               # synchronize methods
+               for name in ['create','fds','proc_read','proc_write','dump','die','run']:
+                       orig=getattr(self,name)
+                       setattr(self,name,SynchronizedMethod(self.lock,orig))
+               self.thread.start()
+       def create(self,w=80,h=25):
+               pid,fd=pty.fork()
+               if pid==0:
+                       try:
+                               fdl=[int(i) for i in os.listdir('/proc/self/fd')]
+                       except OSError:
+                               fdl=range(256)
+                       for i in [i for i in fdl if i>2]:
+                               try:
+                                       os.close(i)
+                               except OSError:
+                                       pass
+                       if self.cmd:
+                               cmd=['/bin/sh','-c',self.cmd]
+                       elif os.getuid()==0:
+                               cmd=['/bin/login']
+                       else:
+                               sys.stdout.write("Login: ")
+                               login=sys.stdin.readline().strip()
+                               if re.match('^[0-9A-Za-z-_. ]+$',login):
+                                       cmd=['ssh']
+                                       cmd+=['-oPreferredAuthentications=keyboard-interactive,password']
+                                       cmd+=['-oNoHostAuthenticationForLocalhost=yes']
+                                       cmd+=['-oLogLevel=FATAL']
+                                       cmd+=['-F/dev/null','-l',login,'localhost']
+                               else:
+                                       os._exit(0)
+                       env={}
+                       env["COLUMNS"]=str(w)
+                       env["LINES"]=str(h)
+                       env["TERM"]="linux"
+                       env["PATH"]=os.environ['PATH']
+                       os.execvpe(cmd[0],cmd,env)
+               else:
+                       fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
+                       # python bug http://python.org/sf/1112949 on amd64
+                       fcntl.ioctl(fd, struct.unpack('i',struct.pack('I',termios.TIOCSWINSZ))[0], struct.pack("HHHH",h,w,0,0))
+                       self.proc[fd]={'pid':pid,'term':Terminal(w,h),'buf':'','time':time.time()}
+                       return fd
+       def die(self):
+               self.alive=0
+       def run(self):
+               return self.alive
+       def fds(self):
+               return self.proc.keys()
+       def proc_kill(self,fd):
+               if fd in self.proc:
+                       self.proc[fd]['time']=0
+               t=time.time()
+               for i in self.proc.keys():
+                       t0=self.proc[i]['time']
+                       if (t-t0)>120:
+                               try:
+                                       os.close(i)
+                                       os.kill(self.proc[i]['pid'],signal.SIGTERM)
+                               except (IOError,OSError):
+                                       pass
+                               del self.proc[i]
+       def proc_read(self,fd):
+               try:
+                       t=self.proc[fd]['term']
+                       t.write(os.read(fd,65536))
+                       reply=t.read()
+                       if reply:
+                               os.write(fd,reply)
+                       self.proc[fd]['time']=time.time()
+               except (KeyError,IOError,OSError):
+                       self.proc_kill(fd)
+       def proc_write(self,fd,s):
+               try:
+                       os.write(fd,s)
+               except (IOError,OSError):
+                       self.proc_kill(fd)
+       def dump(self,fd,color=1):
+               try:
+                       return self.proc[fd]['term'].dumphtml(color)
+               except KeyError:
+                       return False
+       def loop(self):
+               while self.run():
+                       fds=self.fds()
+                       i,o,e=select.select(fds, [], [], 1.0)
+                       for fd in i:
+                               self.proc_read(fd)
+                       if len(i):
+                               time.sleep(0.002)
+               for i in self.proc.keys():
+                       try:
+                               os.close(i)
+                               os.kill(self.proc[i]['pid'],signal.SIGTERM)
+                       except (IOError,OSError):
+                               pass
+
+class AjaxTerm:
+       def __init__(self,cmd=None,index_file='ajaxterm.html'):
+               self.files={}
+               for i in ['css','html','js']:
+                       for j in glob.glob('*.%s'%i):
+                               self.files[j]=file(j).read()
+               self.files['index']=file(index_file).read()
+               self.mime = mimetypes.types_map.copy()
+               self.mime['.html']= 'text/html; charset=UTF-8'
+               self.multi = Multiplex(cmd)
+               self.session = {}
+       def __call__(self, environ, start_response):
+               req = qweb.QWebRequest(environ, start_response,session=None)
+               if req.PATH_INFO.endswith('/u'):
+                       s=req.REQUEST["s"]
+                       k=req.REQUEST["k"]
+                       c=req.REQUEST["c"]
+                       w=req.REQUEST.int("w")
+                       h=req.REQUEST.int("h")
+                       if s in self.session:
+                               term=self.session[s]
+                       else:
+                               if not (w>2 and w<256 and h>2 and h<100):
+                                       w,h=80,25
+                               term=self.session[s]=self.multi.create(w,h)
+                       if k:
+                               self.multi.proc_write(term,k)
+                       time.sleep(0.002)
+                       dump=self.multi.dump(term,c)
+                       req.response_headers['Content-Type']='text/xml'
+                       if isinstance(dump,str):
+                               req.write(dump)
+                               req.response_gzencode=1
+                       else:
+                               del self.session[s]
+                               req.write('<?xml version="1.0"?><idem></idem>')
+#                      print "sessions %r"%self.session
+               else:
+                       n=os.path.basename(req.PATH_INFO)
+                       if n in self.files:
+                               req.response_headers['Content-Type'] = self.mime.get(os.path.splitext(n)[1].lower(), 'application/octet-stream')
+                               req.write(self.files[n])
+                       else:
+                               req.response_headers['Content-Type'] = 'text/html; charset=UTF-8'
+                               req.write(self.files['index'])
+               return req
+
+def main():
+       parser = optparse.OptionParser()
+       parser.add_option("-p", "--port", dest="port", default="8022", help="Set the TCP port (default: 8022)")
+       parser.add_option("-c", "--command", dest="cmd", default=None,help="set the command (default: /bin/login or ssh localhost)")
+       parser.add_option("-l", "--log", action="store_true", dest="log",default=0,help="log requests to stderr (default: quiet mode)")
+       parser.add_option("-d", "--daemon", action="store_true", dest="daemon", default=0, help="run as daemon in the background")
+       parser.add_option("-P", "--pidfile",dest="pidfile",default="/var/run/ajaxterm.pid",help="set the pidfile (default: /var/run/ajaxterm.pid)")
+       parser.add_option("-i", "--index", dest="index_file", default="ajaxterm.html",help="default index file (default: ajaxterm.html)")
+       parser.add_option("-u", "--uid", dest="uid", help="Set the daemon's user id")
+       (o, a) = parser.parse_args()
+       if o.daemon:
+               pid=os.fork()
+               if pid == 0:
+                       #os.setsid() ?
+                       os.setpgrp()
+                       nullin = file('/dev/null', 'r')
+                       nullout = file('/dev/null', 'w')
+                       os.dup2(nullin.fileno(), sys.stdin.fileno())
+                       os.dup2(nullout.fileno(), sys.stdout.fileno())
+                       os.dup2(nullout.fileno(), sys.stderr.fileno())
+                       if os.getuid()==0 and o.uid:
+                               try:
+                                       os.setuid(int(o.uid))
+                               except:
+                                       os.setuid(pwd.getpwnam(o.uid).pw_uid)
+               else:
+                       try:
+                               file(o.pidfile,'w+').write(str(pid)+'\n')
+                       except:
+                               pass
+                       print 'AjaxTerm at http://localhost:%s/ pid: %d' % (o.port,pid)
+                       sys.exit(0)
+       else:
+               print 'AjaxTerm at http://localhost:%s/' % o.port
+       at=AjaxTerm(o.cmd,o.index_file)
+#      f=lambda:os.system('firefox http://localhost:%s/&'%o.port)
+#      qweb.qweb_wsgi_autorun(at,ip='localhost',port=int(o.port),threaded=0,log=o.log,callback_ready=None)
+       try:
+               qweb.QWebWSGIServer(at,ip='localhost',port=int(o.port),threaded=0,log=o.log).serve_forever()
+       except KeyboardInterrupt,e:
+               sys.excepthook(*sys.exc_info())
+       at.multi.die()
+
+if __name__ == '__main__':
+       main()
+
diff --git a/ajaxterm/ajaxterm/configure b/ajaxterm/ajaxterm/configure
new file mode 100755 (executable)
index 0000000..45391f4
--- /dev/null
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+
+import optparse,os
+
+parser = optparse.OptionParser()
+parser.add_option("", "--prefix", dest="prefix",default="/usr/local",help="installation prefix (default: /usr/local)")
+parser.add_option("", "--confdir", dest="confdir", default="/etc",help="configuration files directory prefix (default: /etc)")
+parser.add_option("", "--port", dest="port", default="8022", help="set the listening TCP port (default: 8022)")
+parser.add_option("", "--command", dest="cmd", default=None,help="set the command (default: /bin/login or ssh localhost)")
+(o, a) = parser.parse_args()
+
+print "Configuring prefix=",o.prefix," port=",o.port
+
+etc=o.confdir
+port=o.port
+cmd=o.cmd
+bin=os.path.join(o.prefix,"bin")
+lib=os.path.join(o.prefix,"share/ajaxterm")
+man=os.path.join(o.prefix,"share/man/man1")
+
+file("ajaxterm.bin","w").write(file("configure.ajaxterm.bin").read()%locals())
+file("Makefile","w").write(file("configure.makefile").read()%locals())
+
+if os.path.isfile("/etc/gentoo-release"):
+       file("ajaxterm.initd","w").write(file("configure.initd.gentoo").read()%locals())
+elif os.path.isfile("/etc/fedora-release") or os.path.isfile("/etc/redhat-release"):
+       file("ajaxterm.initd","w").write(file("configure.initd.redhat").read()%locals())
+else:
+       file("ajaxterm.initd","w").write(file("configure.initd.debian").read()%locals())
+
+os.system("chmod a+x ajaxterm.bin")
+os.system("chmod a+x ajaxterm.initd")
diff --git a/ajaxterm/ajaxterm/configure.ajaxterm.bin b/ajaxterm/ajaxterm/configure.ajaxterm.bin
new file mode 100644 (file)
index 0000000..4d1f5a9
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+PYTHONPATH=%(lib)s exec %(lib)s/ajaxterm.py $@
diff --git a/ajaxterm/ajaxterm/configure.initd.debian b/ajaxterm/ajaxterm/configure.initd.debian
new file mode 100644 (file)
index 0000000..9010827
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin
+DAEMON=%(bin)s/ajaxterm
+PORT=%(port)s
+PIDFILE=/var/run/ajaxterm.pid
+
+[ -x "$DAEMON" ] || exit 0
+
+#. /lib/lsb/init-functions
+
+case "$1" in
+       start)
+               echo "Starting ajaxterm on port $PORT"
+               start-stop-daemon --start --pidfile $PIDFILE --exec $DAEMON -- --daemon --port=$PORT --uid=nobody || return 2
+       ;;
+       stop)
+               echo "Stopping ajaxterm"
+               start-stop-daemon  --stop --pidfile $PIDFILE
+               rm -f $PIDFILE
+       ;;
+       restart|force-reload)
+               $0 stop
+               sleep 1
+               $0 start
+       ;;
+       *)
+               echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2
+               exit 3
+       ;;
+esac
+
+:
diff --git a/ajaxterm/ajaxterm/configure.initd.gentoo b/ajaxterm/ajaxterm/configure.initd.gentoo
new file mode 100644 (file)
index 0000000..ac28ef0
--- /dev/null
@@ -0,0 +1,27 @@
+#!/sbin/runscript
+
+# AjaxTerm Gentoo script, 08 May 2006 Mark Gillespie 
+
+DAEMON=%(bin)s/ajaxterm
+PORT=%(port)s
+PIDFILE=/var/run/ajaxterm.pid 
+
+depend()
+{
+    need net
+}
+
+start()
+{
+    ebegin "Starting AjaxTerm on port $PORT"
+    start-stop-daemon --start --pidfile $PIDFILE --exec $DAEMON -- --daemon --port=$PORT --uid=nobody
+    eend $?
+}
+
+stop()
+{
+    ebegin "Stopping AjaxTerm"
+    start-stop-daemon  --stop --pidfile $PIDFILE 
+    rm -f $PIDFILE 
+    eend $?
+}
diff --git a/ajaxterm/ajaxterm/configure.initd.redhat b/ajaxterm/ajaxterm/configure.initd.redhat
new file mode 100644 (file)
index 0000000..5c97885
--- /dev/null
@@ -0,0 +1,75 @@
+#
+# ajaxterm     Startup script for ajaxterm
+#
+# chkconfig: - 99 99
+# description: Ajaxterm is a yadda yadda yadda
+# processname: ajaxterm
+# pidfile: /var/run/ajaxterm.pid
+# version: 1.0 Kevin Reichhart - ajaxterminit at lastname dot org
+
+# Source function library.
+. /etc/rc.d/init.d/functions
+
+if [ -f /etc/sysconfig/ajaxterm ]; then
+    . /etc/sysconfig/ajaxterm
+fi
+
+ajaxterm=/usr/local/bin/ajaxterm
+prog=ajaxterm
+pidfile=${PIDFILE-/var/run/ajaxterm.pid}
+lockfile=${LOCKFILE-/var/lock/subsys/ajaxterm}
+port=${PORT-8022}
+user=${xUSER-nobody}
+RETVAL=0
+
+
+start() {
+    echo -n $"Starting $prog: "
+    daemon $ajaxterm --daemon --port=$port --uid=$user $OPTIONS
+    RETVAL=$?
+    echo
+    [ $RETVAL = 0 ] && touch ${lockfile}
+    return $RETVAL
+}
+stop() {
+    echo -n $"Stopping $prog: "
+    killproc $ajaxterm
+    RETVAL=$?
+    echo
+    [ $RETVAL = 0 ] && rm -f ${lockfile} ${pidfile}
+}
+reload() {
+    echo -n $"Reloading $prog: "
+    killproc $ajaxterm -HUP
+    RETVAL=$?
+    echo
+}
+
+# See how we were called.
+case "$1" in
+    start)
+        start
+        ;;
+    stop)
+        stop
+        ;;
+    status)
+        status python ajaxterm
+        RETVAL=$?
+        ;;
+    restart)
+        stop
+        start
+        ;;
+    condrestart)
+        if [ -f ${pidfile} ] ; then
+            stop
+            start
+        fi
+        ;;
+    *)
+    echo $"Usage: $prog {start|stop|restart|condrestart}"
+    exit 1
+esac
+
+exit $RETVAL
diff --git a/ajaxterm/ajaxterm/configure.makefile b/ajaxterm/ajaxterm/configure.makefile
new file mode 100644 (file)
index 0000000..6bd8085
--- /dev/null
@@ -0,0 +1,20 @@
+build:
+       true
+
+install:
+       install -d "%(bin)s"
+       install -d "%(lib)s"
+       install ajaxterm.bin "%(bin)s/ajaxterm"
+       install ajaxterm.initd "%(etc)s/init.d/ajaxterm"
+       install -m 644 ajaxterm.css ajaxterm.html ajaxterm.js qweb.py sarissa.js sarissa_dhtml.js "%(lib)s"
+       install -m 755 ajaxterm.py "%(lib)s"
+       gzip --best -c ajaxterm.1 > ajaxterm.1.gz
+       install -d "%(man)s"
+       install ajaxterm.1.gz "%(man)s"
+
+clean:
+       rm ajaxterm.bin
+       rm ajaxterm.initd
+       rm ajaxterm.1.gz
+       rm Makefile
+
diff --git a/ajaxterm/ajaxterm/qweb.py b/ajaxterm/ajaxterm/qweb.py
new file mode 100644 (file)
index 0000000..20c5092
--- /dev/null
@@ -0,0 +1,1356 @@
+#!/usr/bin/python2.3
+#
+# vim:set et ts=4 fdc=0 fdn=2 fdl=0:
+#
+# There are no blank lines between blocks beacause i use folding from:
+# http://www.vim.org/scripts/script.php?script_id=515
+#
+
+"""= QWeb Framework =
+
+== What is QWeb ? ==
+
+QWeb is a python based [http://www.python.org/doc/peps/pep-0333/ WSGI]
+compatible web framework, it provides an infratructure to quickly build web
+applications consisting of:
+
+ * A lightweight request handler (QWebRequest)
+ * An xml templating engine (QWebXml and QWebHtml)
+ * A simple name based controler (qweb_control)
+ * A standalone WSGI Server (QWebWSGIServer)
+ * A cgi and fastcgi WSGI wrapper (taken from flup)
+ * A startup function that starts cgi, factgi or standalone according to the
+   evironement (qweb_autorun).
+
+QWeb applications are runnable in standalone mode (from commandline), via
+FastCGI, Regular CGI or by any python WSGI compliant server.
+
+QWeb doesn't provide any database access but it integrates nicely with ORMs
+such as SQLObject, SQLAlchemy or plain DB-API.
+
+Written by Antony Lesuisse (email al AT udev.org)
+
+Homepage: http://antony.lesuisse.org/qweb/trac/
+
+Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum]
+
+== Quick Start (for Linux, MacOS X and cygwin) ==
+
+Make sure you have at least python 2.3 installed and run the following commands:
+
+{{{
+$ wget http://antony.lesuisse.org/qweb/files/QWeb-0.7.tar.gz
+$ tar zxvf QWeb-0.7.tar.gz
+$ cd QWeb-0.7/examples/blog
+$ ./blog.py
+}}}
+
+And point your browser to http://localhost:8080/
+
+You may also try AjaxTerm which uses qweb request handler.
+
+== Download ==
+
+ * Version 0.7:
+   * Source [/qweb/files/QWeb-0.7.tar.gz QWeb-0.7.tar.gz]
+   * Python 2.3 Egg [/qweb/files/QWeb-0.7-py2.3.egg QWeb-0.7-py2.3.egg]
+   * Python 2.4 Egg [/qweb/files/QWeb-0.7-py2.4.egg QWeb-0.7-py2.4.egg]
+
+ * [/qweb/trac/browser Browse the source repository]
+
+== Documentation ==
+
+ * [/qweb/trac/browser/trunk/README.txt?format=raw Read the included documentation] 
+ * QwebTemplating
+
+== Mailin-list ==
+
+ * Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum]
+ * No mailing-list exists yet, discussion should happen on: [http://mail.python.org/mailman/listinfo/web-sig web-sig] [http://mail.python.org/pipermail/web-sig/ archives]
+
+QWeb Components:
+----------------
+
+QWeb also feature a simple components api, that enables developers to easily
+produces reusable components.
+
+Default qweb components:
+
+    - qweb_static:
+        A qweb component to serve static content from the filesystem or from
+        zipfiles.
+
+    - qweb_dbadmin:
+        scaffolding for sqlobject
+
+License
+-------
+qweb/fcgi.py wich is BSD-like from saddi.com.
+Everything else is put in the public domain.
+
+
+TODO
+----
+    Announce QWeb to python-announce-list@python.org web-sig@python.org
+    qweb_core
+        rename request methods into
+            request_save_files
+            response_404
+            response_redirect
+            response_download
+        request callback_generator, callback_function ?
+        wsgi callback_server_local
+        xml tags explicitly call render_attributes(t_att)?
+        priority form-checkbox over t-value (for t-option)
+
+"""
+
+import BaseHTTPServer,SocketServer,Cookie
+import cgi,datetime,email,email.Message,errno,gzip,os,random,re,socket,sys,tempfile,time,types,urllib,urlparse,xml.dom
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+try:
+    import cStringIO as StringIO
+except ImportError:
+    import StringIO
+
+#----------------------------------------------------------
+# Qweb Xml t-raw t-esc t-if t-foreach t-set t-call t-trim
+#----------------------------------------------------------
+class QWebEval:
+    def __init__(self,data):
+        self.data=data
+    def __getitem__(self,expr):
+        if self.data.has_key(expr):
+            return self.data[expr]
+        r=None
+        try:
+            r=eval(expr,self.data)
+        except NameError,e:
+            pass
+        except AttributeError,e:
+            pass
+        except Exception,e:
+            print "qweb: expression error '%s' "%expr,e
+        if self.data.has_key("__builtins__"):
+            del self.data["__builtins__"]
+        return r
+    def eval_object(self,expr):
+        return self[expr]
+    def eval_str(self,expr):
+        if expr=="0":
+            return self.data[0]
+        if isinstance(self[expr],unicode):
+            return self[expr].encode("utf8")
+        return str(self[expr])
+    def eval_format(self,expr):
+        try:
+            return str(expr%self)
+        except:
+            return "qweb: format error '%s' "%expr
+#       if isinstance(r,unicode):
+#           return r.encode("utf8")
+    def eval_bool(self,expr):
+        if self.eval_object(expr):
+            return 1
+        else:
+            return 0
+class QWebXml:
+    """QWeb Xml templating engine
+    
+    The templating engine use a very simple syntax, "magic" xml attributes, to
+    produce any kind of texutal output (even non-xml).
+    
+    QWebXml:
+        the template engine core implements the basic magic attributes:
+    
+        t-att t-raw t-esc t-if t-foreach t-set t-call t-trim
+    
+    """
+    def __init__(self,x=None,zipname=None):
+        self.node=xml.dom.Node
+        self._t={}
+        self._render_tag={}
+        prefix='render_tag_'
+        for i in [j for j in dir(self) if j.startswith(prefix)]:
+            name=i[len(prefix):].replace('_','-')
+            self._render_tag[name]=getattr(self.__class__,i)
+
+        self._render_att={}
+        prefix='render_att_'
+        for i in [j for j in dir(self) if j.startswith(prefix)]:
+            name=i[len(prefix):].replace('_','-')
+            self._render_att[name]=getattr(self.__class__,i)
+
+        if x!=None:
+            if zipname!=None:
+                import zipfile
+                zf=zipfile.ZipFile(zipname, 'r')
+                self.add_template(zf.read(x))
+            else:
+                self.add_template(x)
+    def register_tag(self,tag,func):
+        self._render_tag[tag]=func
+    def add_template(self,x):
+        if hasattr(x,'documentElement'):
+            dom=x
+        elif x.startswith("<?xml"):
+            import xml.dom.minidom
+            dom=xml.dom.minidom.parseString(x)
+        else:
+            import xml.dom.minidom
+            dom=xml.dom.minidom.parse(x)
+        for n in dom.documentElement.childNodes:
+            if n.nodeName=="t":
+                self._t[str(n.getAttribute("t-name"))]=n
+    def get_template(self,name):
+        return self._t[name]
+
+    def eval_object(self,expr,v):
+        return QWebEval(v).eval_object(expr)
+    def eval_str(self,expr,v):
+        return QWebEval(v).eval_str(expr)
+    def eval_format(self,expr,v):
+        return QWebEval(v).eval_format(expr)
+    def eval_bool(self,expr,v):
+        return QWebEval(v).eval_bool(expr)
+
+    def render(self,tname,v={},out=None):
+        if self._t.has_key(tname):
+            return self.render_node(self._t[tname],v)
+        else:
+            return 'qweb: template "%s" not found'%tname
+    def render_node(self,e,v):
+        r=""
+        if e.nodeType==self.node.TEXT_NODE or e.nodeType==self.node.CDATA_SECTION_NODE:
+            r=e.data.encode("utf8")
+        elif e.nodeType==self.node.ELEMENT_NODE:
+            pre=""
+            g_att=""
+            t_render=None
+            t_att={}
+            for (an,av) in e.attributes.items():
+                an=str(an)
+                if isinstance(av,types.UnicodeType):
+                    av=av.encode("utf8")
+                else:
+                    av=av.nodeValue.encode("utf8")
+                if an.startswith("t-"):
+                    for i in self._render_att:
+                        if an[2:].startswith(i):
+                            g_att+=self._render_att[i](self,e,an,av,v)
+                            break
+                    else:
+                        if self._render_tag.has_key(an[2:]):
+                            t_render=an[2:]
+                        t_att[an[2:]]=av
+                else:
+                    g_att+=' %s="%s"'%(an,cgi.escape(av,1));
+            if t_render:
+                if self._render_tag.has_key(t_render):
+                    r=self._render_tag[t_render](self,e,t_att,g_att,v)
+            else:
+                r=self.render_element(e,g_att,v,pre,t_att.get("trim",0))
+        return r
+    def render_element(self,e,g_att,v,pre="",trim=0):
+        g_inner=[]
+        for n in e.childNodes:
+            g_inner.append(self.render_node(n,v))
+        name=str(e.nodeName)
+        inner="".join(g_inner)
+        if trim==0:
+            pass
+        elif trim=='left':
+            inner=inner.lstrip()
+        elif trim=='right':
+            inner=inner.rstrip()
+        elif trim=='both':
+            inner=inner.strip()
+        if name=="t":
+            return inner
+        elif len(inner):
+            return "<%s%s>%s%s</%s>"%(name,g_att,pre,inner,name)
+        else:
+            return "<%s%s/>"%(name,g_att)
+
+    # Attributes
+    def render_att_att(self,e,an,av,v):
+        if an.startswith("t-attf-"):
+            att,val=an[7:],self.eval_format(av,v)
+        elif an.startswith("t-att-"):
+            att,val=(an[6:],self.eval_str(av,v))
+        else:
+            att,val=self.eval_object(av,v)
+        return ' %s="%s"'%(att,cgi.escape(val,1))
+
+    # Tags
+    def render_tag_raw(self,e,t_att,g_att,v):
+        return self.eval_str(t_att["raw"],v)
+    def render_tag_rawf(self,e,t_att,g_att,v):
+        return self.eval_format(t_att["rawf"],v)
+    def render_tag_esc(self,e,t_att,g_att,v):
+        return cgi.escape(self.eval_str(t_att["esc"],v))
+    def render_tag_escf(self,e,t_att,g_att,v):
+        return cgi.escape(self.eval_format(t_att["escf"],v))
+    def render_tag_foreach(self,e,t_att,g_att,v):
+        expr=t_att["foreach"]
+        enum=self.eval_object(expr,v)
+        if enum!=None:
+            var=t_att.get('as',expr).replace('.','_')
+            d=v.copy()
+            size=-1
+            if isinstance(enum,types.ListType):
+                size=len(enum)
+            elif isinstance(enum,types.TupleType):
+                size=len(enum)
+            elif hasattr(enum,'count'):
+                size=enum.count()
+            d["%s_size"%var]=size
+            d["%s_all"%var]=enum
+            index=0
+            ru=[]
+            for i in enum:
+                d["%s_value"%var]=i
+                d["%s_index"%var]=index
+                d["%s_first"%var]=index==0
+                d["%s_even"%var]=index%2
+                d["%s_odd"%var]=(index+1)%2
+                d["%s_last"%var]=index+1==size
+                if index%2:
+                    d["%s_parity"%var]='odd'
+                else:
+                    d["%s_parity"%var]='even'
+                if isinstance(i,types.DictType):
+                    d.update(i)
+                else:
+                    d[var]=i
+                ru.append(self.render_element(e,g_att,d))
+                index+=1
+            return "".join(ru)
+        else:
+            return "qweb: t-foreach %s not found."%expr
+    def render_tag_if(self,e,t_att,g_att,v):
+        if self.eval_bool(t_att["if"],v):
+            return self.render_element(e,g_att,v)
+        else:
+            return ""
+    def render_tag_call(self,e,t_att,g_att,v):
+        # TODO t-prefix
+        if t_att.has_key("import"):
+            d=v
+        else:
+            d=v.copy()
+        d[0]=self.render_element(e,g_att,d)
+        return self.render(t_att["call"],d)
+    def render_tag_set(self,e,t_att,g_att,v):
+        if t_att.has_key("eval"):
+            v[t_att["set"]]=self.eval_object(t_att["eval"],v)
+        else:
+            v[t_att["set"]]=self.render_element(e,g_att,v)
+        return ""
+
+#----------------------------------------------------------
+# QWeb HTML (+deprecated QWebFORM and QWebOLD)
+#----------------------------------------------------------
+class QWebURL:
+    """ URL helper
+    assert req.PATH_INFO== "/site/admin/page_edit"
+    u = QWebURL(root_path="/site/",req_path=req.PATH_INFO)
+    s=u.url2_href("user/login",{'a':'1'})
+    assert s=="../user/login?a=1"
+    
+    """
+    def __init__(self, root_path="/", req_path="/",defpath="",defparam={}):
+        self.defpath=defpath
+        self.defparam=defparam
+        self.root_path=root_path
+        self.req_path=req_path
+        self.req_list=req_path.split("/")[:-1]
+        self.req_len=len(self.req_list)
+    def decode(self,s):
+        h={}
+        for k,v in cgi.parse_qsl(s,1):
+            h[k]=v
+        return h
+    def encode(self,h):
+        return urllib.urlencode(h.items())
+    def request(self,req):
+        return req.REQUEST
+    def copy(self,path=None,param=None):
+        npath=self.defpath
+        if path:
+            npath=path
+        nparam=self.defparam.copy()
+        if param:
+            nparam.update(param)
+        return QWebURL(self.root_path,self.req_path,npath,nparam)
+    def path(self,path=''):
+        if not path:
+            path=self.defpath
+        pl=(self.root_path+path).split('/')
+        i=0
+        for i in range(min(len(pl), self.req_len)):
+            if pl[i]!=self.req_list[i]:
+                break
+        else:
+            i+=1
+        dd=self.req_len-i
+        if dd<0:
+            dd=0
+        return '/'.join(['..']*dd+pl[i:])
+    def href(self,path='',arg={}):
+        p=self.path(path)
+        tmp=self.defparam.copy()
+        tmp.update(arg)
+        s=self.encode(tmp)
+        if len(s):
+            return p+"?"+s
+        else:
+            return p
+    def form(self,path='',arg={}):
+        p=self.path(path)
+        tmp=self.defparam.copy()
+        tmp.update(arg)
+        r=''.join(['<input type="hidden" name="%s" value="%s"/>'%(k,cgi.escape(str(v),1)) for k,v in tmp.items()])
+        return (p,r)
+class QWebField:
+    def __init__(self,name=None,default="",check=None):
+        self.name=name
+        self.default=default
+        self.check=check
+        # optional attributes
+        self.type=None
+        self.trim=1
+        self.required=1
+        self.cssvalid="form_valid"
+        self.cssinvalid="form_invalid"
+        # set by addfield
+        self.form=None
+        # set by processing
+        self.input=None
+        self.css=None
+        self.value=None
+        self.valid=None
+        self.invalid=None
+        self.validate(1)
+    def validate(self,val=1,update=1):
+        if val:
+            self.valid=1
+            self.invalid=0
+            self.css=self.cssvalid
+        else:
+            self.valid=0
+            self.invalid=1
+            self.css=self.cssinvalid
+        if update and self.form:
+            self.form.update()
+    def invalidate(self,update=1):
+        self.validate(0,update)
+class QWebForm:
+    class QWebFormF:
+        pass
+    def __init__(self,e=None,arg=None,default=None):
+        self.fields={}
+        # all fields have been submitted
+        self.submitted=False
+        self.missing=[]
+        # at least one field is invalid or missing
+        self.invalid=False
+        self.error=[]
+        # all fields have been submitted and are valid
+        self.valid=False
+        # fields under self.f for convenience
+        self.f=self.QWebFormF()
+        if e:
+            self.add_template(e)
+        # assume that the fields are done with the template
+        if default:
+            self.set_default(default,e==None)
+        if arg!=None:
+            self.process_input(arg)
+    def __getitem__(self,k):
+        return self.fields[k]
+    def set_default(self,default,add_missing=1):
+        for k,v in default.items():
+            if self.fields.has_key(k):
+                self.fields[k].default=str(v)
+            elif add_missing:
+                self.add_field(QWebField(k,v))
+    def add_field(self,f):
+        self.fields[f.name]=f
+        f.form=self
+        setattr(self.f,f.name,f)
+    def add_template(self,e):
+        att={}
+        for (an,av) in e.attributes.items():
+            an=str(an)
+            if an.startswith("t-"):
+                att[an[2:]]=av.encode("utf8")
+        for i in ["form-text", "form-password", "form-radio", "form-checkbox", "form-select","form-textarea"]:
+            if att.has_key(i):
+                name=att[i].split(".")[-1]
+                default=att.get("default","")
+                check=att.get("check",None)
+                f=QWebField(name,default,check)
+                if i=="form-textarea":
+                    f.type="textarea"
+                    f.trim=0
+                if i=="form-checkbox":
+                    f.type="checkbox"
+                    f.required=0
+                self.add_field(f)
+        for n in e.childNodes:
+            if n.nodeType==n.ELEMENT_NODE:
+                self.add_template(n)
+    def process_input(self,arg):
+        for f in self.fields.values():
+            if arg.has_key(f.name):
+                f.input=arg[f.name]
+                f.value=f.input
+                if f.trim:
+                    f.input=f.input.strip()
+                f.validate(1,False)
+                if f.check==None:
+                    continue
+                elif callable(f.check):
+                    pass
+                elif isinstance(f.check,str):
+                    v=f.check
+                    if f.check=="email":
+                        v=r"/^[^@#!& ]+@[A-Za-z0-9-][.A-Za-z0-9-]{0,64}\.[A-Za-z]{2,5}$/"
+                    if f.check=="date":
+                        v=r"/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/"
+                    if not re.match(v[1:-1],f.input):
+                        f.validate(0,False)
+            else:
+                f.value=f.default
+        self.update()
+    def validate_all(self,val=1):
+        for f in self.fields.values():
+            f.validate(val,0)
+        self.update()
+    def invalidate_all(self):
+        self.validate_all(0)
+    def update(self):
+        self.submitted=True
+        self.valid=True
+        self.errors=[]
+        for f in self.fields.values():
+            if f.required and f.input==None:
+                self.submitted=False
+                self.valid=False
+                self.missing.append(f.name)
+            if f.invalid:
+                self.valid=False
+                self.error.append(f.name)
+        # invalid have been submitted and 
+        self.invalid=self.submitted and self.valid==False
+    def collect(self):
+        d={}
+        for f in self.fields.values():
+            d[f.name]=f.value
+        return d
+class QWebURLEval(QWebEval):
+    def __init__(self,data):
+        QWebEval.__init__(self,data)
+    def __getitem__(self,expr):
+        r=QWebEval.__getitem__(self,expr)
+        if isinstance(r,str):
+            return urllib.quote_plus(r)
+        else:
+            return r
+class QWebHtml(QWebXml):
+    """QWebHtml
+    QWebURL:
+    QWebField:
+    QWebForm:
+    QWebHtml:
+        an extended template engine, with a few utility class to easily produce
+        HTML, handle URLs and process forms, it adds the following magic attributes:
+    
+        t-href t-action t-form-text t-form-password t-form-textarea t-form-radio
+        t-form-checkbox t-form-select t-option t-selected t-checked t-pager
+    
+    # explication URL:
+    # v['tableurl']=QWebUrl({p=afdmin,saar=,orderby=,des=,mlink;meta_active=})
+    # t-href="tableurl?desc=1"
+    #
+    # explication FORM: t-if="form.valid()"
+    # Foreach i
+    #   email: <input type="text" t-esc-name="i" t-esc-value="form[i].value" t-esc-class="form[i].css"/>
+    #   <input type="radio" name="spamtype" t-esc-value="i" t-selected="i==form.f.spamtype.value"/>
+    #   <option t-esc-value="cc" t-selected="cc==form.f.country.value"><t t-esc="cname"></option>
+    # Simple forms:
+    #   <input t-form-text="form.email" t-check="email"/>
+    #   <input t-form-password="form.email" t-check="email"/>
+    #   <input t-form-radio="form.email" />
+    #   <input t-form-checkbox="form.email" />
+    #   <textarea t-form-textarea="form.email" t-check="email"/>
+    #   <select t-form-select="form.email"/>
+    #       <option t-value="1">
+    #   <input t-form-radio="form.spamtype" t-value="1"/> Cars
+    #   <input t-form-radio="form.spamtype" t-value="2"/> Sprt
+    """
+    # QWebForm from a template
+    def form(self,tname,arg=None,default=None):
+        form=QWebForm(self._t[tname],arg,default)
+        return form
+
+    # HTML Att
+    def eval_url(self,av,v):
+        s=QWebURLEval(v).eval_format(av)
+        a=s.split('?',1)
+        arg={}
+        if len(a)>1:
+            for k,v in cgi.parse_qsl(a[1],1):
+                arg[k]=v
+        b=a[0].split('/',1)
+        path=''
+        if len(b)>1:
+            path=b[1]
+        u=b[0]
+        return u,path,arg
+    def render_att_url_(self,e,an,av,v):
+        u,path,arg=self.eval_url(av,v)
+        if not isinstance(v.get(u,0),QWebURL):
+            out='qweb: missing url %r %r %r'%(u,path,arg)
+        else:
+            out=v[u].href(path,arg)
+        return ' %s="%s"'%(an[6:],cgi.escape(out,1))
+    def render_att_href(self,e,an,av,v):
+        return self.render_att_url_(e,"t-url-href",av,v)
+    def render_att_checked(self,e,an,av,v):
+        if self.eval_bool(av,v):
+            return ' %s="%s"'%(an[2:],an[2:])
+        else:
+            return ''
+    def render_att_selected(self,e,an,av,v):
+        return self.render_att_checked(e,an,av,v)
+
+    # HTML Tags forms
+    def render_tag_rawurl(self,e,t_att,g_att,v):
+        u,path,arg=self.eval_url(t_att["rawurl"],v)
+        return v[u].href(path,arg)
+    def render_tag_escurl(self,e,t_att,g_att,v):
+        u,path,arg=self.eval_url(t_att["escurl"],v)
+        return cgi.escape(v[u].href(path,arg))
+    def render_tag_action(self,e,t_att,g_att,v):
+        u,path,arg=self.eval_url(t_att["action"],v)
+        if not isinstance(v.get(u,0),QWebURL):
+            action,input=('qweb: missing url %r %r %r'%(u,path,arg),'')
+        else:
+            action,input=v[u].form(path,arg)
+        g_att+=' action="%s"'%action
+        return self.render_element(e,g_att,v,input)
+    def render_tag_form_text(self,e,t_att,g_att,v):
+        f=self.eval_object(t_att["form-text"],v)
+        g_att+=' type="text" name="%s" value="%s" class="%s"'%(f.name,cgi.escape(f.value,1),f.css)
+        return self.render_element(e,g_att,v)
+    def render_tag_form_password(self,e,t_att,g_att,v):
+        f=self.eval_object(t_att["form-password"],v)
+        g_att+=' type="password" name="%s" value="%s" class="%s"'%(f.name,cgi.escape(f.value,1),f.css)
+        return self.render_element(e,g_att,v)
+    def render_tag_form_textarea(self,e,t_att,g_att,v):
+        type="textarea"
+        f=self.eval_object(t_att["form-textarea"],v)
+        g_att+=' name="%s" class="%s"'%(f.name,f.css)
+        r="<%s%s>%s</%s>"%(type,g_att,cgi.escape(f.value,1),type)
+        return r
+    def render_tag_form_radio(self,e,t_att,g_att,v):
+        f=self.eval_object(t_att["form-radio"],v)
+        val=t_att["value"]
+        g_att+=' type="radio" name="%s" value="%s"'%(f.name,val)
+        if f.value==val:
+            g_att+=' checked="checked"'
+        return self.render_element(e,g_att,v)
+    def render_tag_form_checkbox(self,e,t_att,g_att,v):
+        f=self.eval_object(t_att["form-checkbox"],v)
+        val=t_att["value"]
+        g_att+=' type="checkbox" name="%s" value="%s"'%(f.name,val)
+        if f.value==val:
+            g_att+=' checked="checked"'
+        return self.render_element(e,g_att,v)
+    def render_tag_form_select(self,e,t_att,g_att,v):
+        f=self.eval_object(t_att["form-select"],v)
+        g_att+=' name="%s" class="%s"'%(f.name,f.css)
+        return self.render_element(e,g_att,v)
+    def render_tag_option(self,e,t_att,g_att,v):
+        f=self.eval_object(e.parentNode.getAttribute("t-form-select"),v)
+        val=t_att["option"]
+        g_att+=' value="%s"'%(val)
+        if f.value==val:
+            g_att+=' selected="selected"'
+        return self.render_element(e,g_att,v)
+
+    # HTML Tags others
+    def render_tag_pager(self,e,t_att,g_att,v):
+        pre=t_att["pager"]
+        total=int(self.eval_str(t_att["total"],v))
+        start=int(self.eval_str(t_att["start"],v))
+        step=int(self.eval_str(t_att.get("step","100"),v))
+        scope=int(self.eval_str(t_att.get("scope","5"),v))
+        # Compute Pager
+        p=pre+"_"
+        d={}
+        d[p+"tot_size"]=total
+        d[p+"tot_page"]=tot_page=total/step
+        d[p+"win_start0"]=total and start
+        d[p+"win_start1"]=total and start+1
+        d[p+"win_end0"]=max(0,min(start+step-1,total-1))
+        d[p+"win_end1"]=min(start+step,total)
+        d[p+"win_page0"]=win_page=start/step
+        d[p+"win_page1"]=win_page+1
+        d[p+"prev"]=(win_page!=0)
+        d[p+"prev_start"]=(win_page-1)*step
+        d[p+"next"]=(tot_page>=win_page+1)
+        d[p+"next_start"]=(win_page+1)*step
+        l=[]
+        begin=win_page-scope
+        end=win_page+scope
+        if begin<0:
+            end-=begin
+        if end>tot_page:
+            begin-=(end-tot_page)
+        i=max(0,begin)
+        while i<=min(end,tot_page) and total!=step:
+            l.append( { p+"page0":i, p+"page1":i+1, p+"start":i*step, p+"sel":(win_page==i) })
+            i+=1
+        d[p+"active"]=len(l)>1
+        d[p+"list"]=l
+        # Update v
+        v.update(d)
+        return ""
+
+#----------------------------------------------------------
+# QWeb Simple Controller
+#----------------------------------------------------------
+def qweb_control(self,jump='main',p=[]):
+    """ qweb_control(self,jump='main',p=[]):
+    A simple function to handle the controler part of your application. It
+    dispatch the control to the jump argument, while ensuring that prefix
+    function have been called.
+
+    qweb_control replace '/' to '_' and strip '_' from the jump argument.
+
+    name1
+    name1_name2
+    name1_name2_name3
+
+    """
+    jump=jump.replace('/','_').strip('_')
+    if not hasattr(self,jump):
+        return 0
+    done={}
+    todo=[]
+    while 1:
+        if jump!=None:
+            tmp=""
+            todo=[]
+            for i in jump.split("_"):
+                tmp+=i+"_";
+                if not done.has_key(tmp[:-1]):
+                    todo.append(tmp[:-1])
+            jump=None
+        elif len(todo):
+            i=todo.pop(0)
+            done[i]=1
+            if hasattr(self,i):
+                f=getattr(self,i)
+                r=f(*p)
+                if isinstance(r,types.StringType):
+                    jump=r
+        else:
+            break
+    return 1
+
+#----------------------------------------------------------
+# QWeb WSGI Request handler
+#----------------------------------------------------------
+class QWebSession(dict):
+    def __init__(self,environ,**kw):
+        dict.__init__(self)
+        default={
+            "path" : tempfile.gettempdir(),
+            "cookie_name" : "QWEBSID",
+            "cookie_lifetime" : 0,
+            "cookie_path" : '/',
+            "cookie_domain" : '',
+            "limit_cache" : 1,
+            "probability" : 0.01,
+            "maxlifetime" : 3600,
+            "disable" : 0,
+        }
+        for k,v in default.items():
+            setattr(self,'session_%s'%k,kw.get(k,v))
+        # Try to find session
+        self.session_found_cookie=0
+        self.session_found_url=0
+        self.session_found=0
+        self.session_orig=""
+        # Try cookie
+        c=Cookie.SimpleCookie()
+        c.load(environ.get('HTTP_COOKIE', ''))
+        if c.has_key(self.session_cookie_name):
+            sid=c[self.session_cookie_name].value[:64]
+            if re.match('[a-f0-9]+$',sid) and self.session_load(sid):
+                self.session_id=sid
+                self.session_found_cookie=1
+                self.session_found=1
+        # Try URL
+        if not self.session_found_cookie:
+            mo=re.search('&%s=([a-f0-9]+)'%self.session_cookie_name,environ.get('QUERY_STRING',''))
+            if mo and self.session_load(mo.group(1)):
+                self.session_id=mo.group(1)
+                self.session_found_url=1
+                self.session_found=1
+        # New session
+        if not self.session_found:
+            self.session_id='%032x'%random.randint(1,2**128)
+        self.session_trans_sid="&amp;%s=%s"%(self.session_cookie_name,self.session_id)
+        # Clean old session
+        if random.random() < self.session_probability:
+            self.session_clean()
+    def session_get_headers(self):
+        h=[]
+        if (not self.session_disable) and (len(self) or len(self.session_orig)):
+            self.session_save()
+            if not self.session_found_cookie:
+                c=Cookie.SimpleCookie()
+                c[self.session_cookie_name] = self.session_id
+                c[self.session_cookie_name]['path'] = self.session_cookie_path
+                if self.session_cookie_domain:
+                    c[self.session_cookie_name]['domain'] = self.session_cookie_domain
+#               if self.session_cookie_lifetime:
+#                   c[self.session_cookie_name]['expires'] = TODO date localtime or not, datetime.datetime(1970, 1, 1)
+                h.append(("Set-Cookie", c[self.session_cookie_name].OutputString()))
+            if self.session_limit_cache:
+                h.append(('Cache-Control','no-store, no-cache, must-revalidate, post-check=0, pre-check=0'))
+                h.append(('Expires','Thu, 19 Nov 1981 08:52:00 GMT'))
+                h.append(('Pragma','no-cache'))
+        return h
+    def session_load(self,sid):
+        fname=os.path.join(self.session_path,'qweb_sess_%s'%sid)
+        try:
+            orig=file(fname).read()
+            d=pickle.loads(orig)
+        except:
+            return
+        self.session_orig=orig
+        self.update(d)
+        return 1
+    def session_save(self):
+        if not os.path.isdir(self.session_path):
+            os.makedirs(self.session_path)
+        fname=os.path.join(self.session_path,'qweb_sess_%s'%self.session_id)
+        try:
+            oldtime=os.path.getmtime(fname)
+        except OSError,IOError:
+            oldtime=0
+        dump=pickle.dumps(self.copy())
+        if (dump != self.session_orig) or (time.time() > oldtime+self.session_maxlifetime/4):
+            tmpname=os.path.join(self.session_path,'qweb_sess_%s_%x'%(self.session_id,random.randint(1,2**32)))
+            f=file(tmpname,'wb')
+            f.write(dump)
+            f.close()
+            if sys.platform=='win32' and os.path.isfile(fname):
+                os.remove(fname)
+            os.rename(tmpname,fname)
+    def session_clean(self):
+        t=time.time()
+        try:
+            for i in [os.path.join(self.session_path,i) for i in os.listdir(self.session_path) if i.startswith('qweb_sess_')]:
+                if (t > os.path.getmtime(i)+self.session_maxlifetime):
+                    os.unlink(i)
+        except OSError,IOError:
+            pass
+class QWebSessionMem(QWebSession):
+    def session_load(self,sid):
+        global _qweb_sessions
+        if not "_qweb_sessions" in globals():
+            _qweb_sessions={}
+        if _qweb_sessions.has_key(sid):
+            self.session_orig=_qweb_sessions[sid]
+            self.update(self.session_orig)
+            return 1
+    def session_save(self):
+        global _qweb_sessions
+        if not "_qweb_sessions" in globals():
+            _qweb_sessions={}
+        _qweb_sessions[self.session_id]=self.copy()
+class QWebSessionService:
+    def __init__(self, wsgiapp, url_rewrite=0):
+        self.wsgiapp=wsgiapp
+        self.url_rewrite_tags="a=href,area=href,frame=src,form=,fieldset="
+    def __call__(self, environ, start_response):
+        # TODO
+        # use QWebSession to provide environ["qweb.session"]
+        return self.wsgiapp(environ,start_response)
+class QWebDict(dict):
+    def __init__(self,*p):
+        dict.__init__(self,*p)
+    def __getitem__(self,key):
+        return self.get(key,"")
+    def int(self,key):
+        try:
+            return int(self.get(key,"0"))
+        except ValueError:
+            return 0
+class QWebListDict(dict):
+    def __init__(self,*p):
+        dict.__init__(self,*p)
+    def __getitem__(self,key):
+        return self.get(key,[])
+    def appendlist(self,key,val):
+        if self.has_key(key):
+            self[key].append(val)
+        else:
+            self[key]=[val]
+    def get_qwebdict(self):
+        d=QWebDict()
+        for k,v in self.items():
+            d[k]=v[-1]
+        return d
+class QWebRequest:
+    """QWebRequest a WSGI request handler.
+
+    QWebRequest is a WSGI request handler that feature GET, POST and POST
+    multipart methods, handles cookies and headers and provide a dict-like
+    SESSION Object (either on the filesystem or in memory).
+
+    It is constructed with the environ and start_response WSGI arguments:
+    
+      req=qweb.QWebRequest(environ, start_response)
+    
+    req has the folowing attributes :
+    
+      req.environ standard WSGI dict (CGI and wsgi ones)
+    
+    Some CGI vars as attributes from environ for convenience: 
+    
+      req.SCRIPT_NAME
+      req.PATH_INFO
+      req.REQUEST_URI
+    
+    Some computed value (also for convenience)
+    
+      req.FULL_URL full URL recontructed (http://host/query)
+      req.FULL_PATH (URL path before ?querystring)
+    
+    Dict constructed from querystring and POST datas, PHP-like.
+    
+      req.GET contains GET vars
+      req.POST contains POST vars
+      req.REQUEST contains merge of GET and POST
+      req.FILES contains uploaded files
+      req.GET_LIST req.POST_LIST req.REQUEST_LIST req.FILES_LIST multiple arguments versions
+      req.debug() returns an HTML dump of those vars
+    
+    A dict-like session object.
+    
+      req.SESSION the session start when the dict is not empty.
+    
+    Attribute for handling the response
+    
+      req.response_headers dict-like to set headers
+      req.response_cookies a SimpleCookie to set cookies
+      req.response_status a string to set the status like '200 OK'
+    
+      req.write() to write to the buffer
+    
+    req itselfs is an iterable object with the buffer, it will also also call
+    start_response automatically before returning anything via the iterator.
+    
+    To make it short, it means that you may use
+    
+      return req
+    
+    at the end of your request handling to return the reponse to any WSGI
+    application server.
+    """
+    #
+    # This class contains part ripped from colubrid (with the permission of
+    # mitsuhiko) see http://wsgiarea.pocoo.org/colubrid/
+    #
+    # - the class HttpHeaders
+    # - the method load_post_data (tuned version)
+    #
+    class HttpHeaders(object):
+        def __init__(self):
+            self.data = [('Content-Type', 'text/html')]
+        def __setitem__(self, key, value):
+            self.set(key, value)
+        def __delitem__(self, key):
+            self.remove(key)
+        def __contains__(self, key):
+            key = key.lower()
+            for k, v in self.data:
+                if k.lower() == key:
+                    return True
+            return False
+        def add(self, key, value):
+            self.data.append((key, value))
+        def remove(self, key, count=-1):
+            removed = 0
+            data = []
+            for _key, _value in self.data:
+                if _key.lower() != key.lower():
+                    if count > -1:
+                        if removed >= count:
+                            break
+                        else:
+                            removed += 1
+                    data.append((_key, _value))
+            self.data = data
+        def clear(self):
+            self.data = []
+        def set(self, key, value):
+            self.remove(key)
+            self.add(key, value)
+        def get(self, key=False, httpformat=False):
+            if not key:
+                result = self.data
+            else:
+                result = []
+                for _key, _value in self.data:
+                    if _key.lower() == key.lower():
+                        result.append((_key, _value))
+            if httpformat:
+                return '\n'.join(['%s: %s' % item for item in result])
+            return result
+    def load_post_data(self,environ,POST,FILES):
+        length = int(environ['CONTENT_LENGTH'])
+        DATA = environ['wsgi.input'].read(length)
+        if environ.get('CONTENT_TYPE', '').startswith('multipart'):
+            lines = ['Content-Type: %s' % environ.get('CONTENT_TYPE', '')]
+            for key, value in environ.items():
+                if key.startswith('HTTP_'):
+                    lines.append('%s: %s' % (key, value))
+            raw = '\r\n'.join(lines) + '\r\n\r\n' + DATA
+            msg = email.message_from_string(raw)
+            for sub in msg.get_payload():
+                if not isinstance(sub, email.Message.Message):
+                    continue
+                name_dict = cgi.parse_header(sub['Content-Disposition'])[1]
+                if 'filename' in name_dict:
+                    # Nested MIME Messages are not supported'
+                    if type([]) == type(sub.get_payload()):
+                        continue
+                    if not name_dict['filename'].strip():
+                        continue
+                    filename = name_dict['filename']
+                    # why not keep all the filename? because IE always send 'C:\documents and settings\blub\blub.png'
+                    filename = filename[filename.rfind('\\') + 1:]
+                    if 'Content-Type' in sub:
+                        content_type = sub['Content-Type']
+                    else:
+                        content_type = None
+                    s = { "name":filename, "type":content_type, "data":sub.get_payload() }
+                    FILES.appendlist(name_dict['name'], s)
+                else:
+                    POST.appendlist(name_dict['name'], sub.get_payload())
+        else:
+            POST.update(cgi.parse_qs(DATA,keep_blank_values=1))
+        return DATA
+
+    def __init__(self,environ,start_response,session=QWebSession):
+        self.environ=environ
+        self.start_response=start_response
+        self.buffer=[]
+
+        self.SCRIPT_NAME = environ.get('SCRIPT_NAME', '')
+        self.PATH_INFO = environ.get('PATH_INFO', '')
+        # extensions:
+        self.FULL_URL = environ['FULL_URL'] = self.get_full_url(environ)
+        # REQUEST_URI is optional, fake it if absent
+        if not environ.has_key("REQUEST_URI"):
+            environ["REQUEST_URI"]=urllib.quote(self.SCRIPT_NAME+self.PATH_INFO)
+            if environ.get('QUERY_STRING'):
+                environ["REQUEST_URI"]+='?'+environ['QUERY_STRING']
+        self.REQUEST_URI = environ["REQUEST_URI"]
+        # full quote url path before the ?
+        self.FULL_PATH = environ['FULL_PATH'] = self.REQUEST_URI.split('?')[0]
+
+        self.request_cookies=Cookie.SimpleCookie()
+        self.request_cookies.load(environ.get('HTTP_COOKIE', ''))
+
+        self.response_started=False
+        self.response_gzencode=False
+        self.response_cookies=Cookie.SimpleCookie()
+        # to delete a cookie use: c[key]['expires'] = datetime.datetime(1970, 1, 1)
+        self.response_headers=self.HttpHeaders()
+        self.response_status="200 OK"
+
+        self.php=None
+        if self.environ.has_key("php"):
+            self.php=environ["php"]
+            self.SESSION=self.php._SESSION
+            self.GET=self.php._GET
+            self.POST=self.php._POST
+            self.REQUEST=self.php._ARG
+            self.FILES=self.php._FILES
+        else:
+            if isinstance(session,QWebSession):
+                self.SESSION=session
+            elif session:
+                self.SESSION=session(environ)
+            else:
+                self.SESSION=None
+            self.GET_LIST=QWebListDict(cgi.parse_qs(environ.get('QUERY_STRING', ''),keep_blank_values=1))
+            self.POST_LIST=QWebListDict()
+            self.FILES_LIST=QWebListDict()
+            self.REQUEST_LIST=QWebListDict(self.GET_LIST)
+            if environ['REQUEST_METHOD'] == 'POST':
+                self.DATA=self.load_post_data(environ,self.POST_LIST,self.FILES_LIST)
+                self.REQUEST_LIST.update(self.POST_LIST)
+            self.GET=self.GET_LIST.get_qwebdict()
+            self.POST=self.POST_LIST.get_qwebdict()
+            self.FILES=self.FILES_LIST.get_qwebdict()
+            self.REQUEST=self.REQUEST_LIST.get_qwebdict()
+    def get_full_url(environ):
+        # taken from PEP 333
+        if 'FULL_URL' in environ:
+            return environ['FULL_URL']
+        url = environ['wsgi.url_scheme']+'://'
+        if environ.get('HTTP_HOST'):
+            url += environ['HTTP_HOST']
+        else:
+            url += environ['SERVER_NAME']
+            if environ['wsgi.url_scheme'] == 'https':
+                if environ['SERVER_PORT'] != '443':
+                    url += ':' + environ['SERVER_PORT']
+            else:
+                if environ['SERVER_PORT'] != '80':
+                    url += ':' + environ['SERVER_PORT']
+        if environ.has_key('REQUEST_URI'):
+            url += environ['REQUEST_URI']
+        else:
+            url += urllib.quote(environ.get('SCRIPT_NAME', ''))
+            url += urllib.quote(environ.get('PATH_INFO', ''))
+            if environ.get('QUERY_STRING'):
+                url += '?' + environ['QUERY_STRING']
+        return url
+    get_full_url=staticmethod(get_full_url)
+    def save_files(self):
+        for k,v in self.FILES.items():
+            if not v.has_key("tmp_file"):
+                f=tempfile.NamedTemporaryFile()
+                f.write(v["data"])
+                f.flush()
+                v["tmp_file"]=f
+                v["tmp_name"]=f.name
+    def debug(self):
+        body=''
+        for name,d in [
+            ("GET",self.GET), ("POST",self.POST), ("REQUEST",self.REQUEST), ("FILES",self.FILES),
+            ("GET_LIST",self.GET_LIST), ("POST_LIST",self.POST_LIST), ("REQUEST_LIST",self.REQUEST_LIST), ("FILES_LIST",self.FILES_LIST),
+            ("SESSION",self.SESSION), ("environ",self.environ),
+        ]:
+            body+='<table border="1" width="100%" align="center">\n'
+            body+='<tr><th colspan="2" align="center">%s</th></tr>\n'%name
+            keys=d.keys()
+            keys.sort()
+            body+=''.join(['<tr><td>%s</td><td>%s</td></tr>\n'%(k,cgi.escape(repr(d[k]))) for k in keys])
+            body+='</table><br><br>\n\n'
+        return body
+    def write(self,s):
+        self.buffer.append(s)
+    def echo(self,*s):
+        self.buffer.extend([str(i) for i in s])
+    def response(self):
+        if not self.response_started:
+            if not self.php:
+                for k,v in self.FILES.items():
+                    if v.has_key("tmp_file"):
+                        try:
+                            v["tmp_file"].close()
+                        except OSError:
+                            pass
+                if self.response_gzencode and self.environ.get('HTTP_ACCEPT_ENCODING','').find('gzip')!=-1:
+                    zbuf=StringIO.StringIO()
+                    zfile=gzip.GzipFile(mode='wb', fileobj=zbuf)
+                    zfile.write(''.join(self.buffer))
+                    zfile.close()
+                    zbuf=zbuf.getvalue()
+                    self.buffer=[zbuf]
+                    self.response_headers['Content-Encoding']="gzip"
+                    self.response_headers['Content-Length']=str(len(zbuf))
+                headers = self.response_headers.get()
+                if isinstance(self.SESSION, QWebSession):
+                    headers.extend(self.SESSION.session_get_headers())
+                headers.extend([('Set-Cookie', self.response_cookies[i].OutputString()) for i in self.response_cookies])
+                self.start_response(self.response_status, headers)
+            self.response_started=True
+        return self.buffer
+    def __iter__(self):
+        return self.response().__iter__()
+    def http_redirect(self,url,permanent=1):
+        if permanent:
+            self.response_status="301 Moved Permanently"
+        else:
+            self.response_status="302 Found"
+        self.response_headers["Location"]=url
+    def http_404(self,msg="<h1>404 Not Found</h1>"):
+        self.response_status="404 Not Found"
+        if msg:
+            self.write(msg)
+    def http_download(self,fname,fstr,partial=0):
+#       allow fstr to be a file-like object
+#       if parital:
+#           say accept ranages
+#           parse range headers...
+#           if range:
+#               header("HTTP/1.1 206 Partial Content");
+#               header("Content-Range: bytes $offset-".($fsize-1)."/".$fsize);
+#               header("Content-Length: ".($fsize-$offset));
+#               fseek($fd,$offset);
+#           else:
+        self.response_headers["Content-Type"]="application/octet-stream"
+        self.response_headers["Content-Disposition"]="attachment; filename=\"%s\""%fname
+        self.response_headers["Content-Transfer-Encoding"]="binary"
+        self.response_headers["Content-Length"]="%d"%len(fstr)
+        self.write(fstr)
+
+#----------------------------------------------------------
+# QWeb WSGI HTTP Server to run any WSGI app
+# autorun, run an app as FCGI or CGI otherwise launch the server
+#----------------------------------------------------------
+class QWebWSGIHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+    def log_message(self,*p):
+        if self.server.log:
+            return BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,*p)
+    def address_string(self):
+        return self.client_address[0]
+    def start_response(self,status,headers):
+        l=status.split(' ',1)
+        self.send_response(int(l[0]),l[1])
+        ctype_sent=0
+        for i in headers:
+            if i[0].lower()=="content-type":
+                ctype_sent=1
+            self.send_header(*i)
+        if not ctype_sent:
+            self.send_header("Content-type", "text/html")
+        self.end_headers()
+        return self.write
+    def write(self,data):
+        try:
+            self.wfile.write(data)
+        except (socket.error, socket.timeout),e:
+            print e
+    def bufferon(self):
+        if not getattr(self,'wfile_buf',0):
+            self.wfile_buf=1
+            self.wfile_bak=self.wfile
+            self.wfile=StringIO.StringIO()
+    def bufferoff(self):
+        if self.wfile_buf:
+            buf=self.wfile
+            self.wfile=self.wfile_bak
+            self.write(buf.getvalue())
+            self.wfile_buf=0
+    def serve(self,type):
+        path_info, parameters, query = urlparse.urlparse(self.path)[2:5]
+        environ = {
+            'wsgi.version':         (1,0),
+            'wsgi.url_scheme':      'http',
+            'wsgi.input':           self.rfile,
+            'wsgi.errors':          sys.stderr,
+            'wsgi.multithread':     0,
+            'wsgi.multiprocess':    0,
+            'wsgi.run_once':        0,
+            'REQUEST_METHOD':       self.command,
+            'SCRIPT_NAME':          '',
+            'QUERY_STRING':         query,
+            'CONTENT_TYPE':         self.headers.get('Content-Type', ''),
+            'CONTENT_LENGTH':       self.headers.get('Content-Length', ''),
+            'REMOTE_ADDR':          self.client_address[0],
+            'REMOTE_PORT':          str(self.client_address[1]),
+            'SERVER_NAME':          self.server.server_address[0],
+            'SERVER_PORT':          str(self.server.server_address[1]),
+            'SERVER_PROTOCOL':      self.request_version,
+            # extention
+            'FULL_PATH':            self.path,
+            'qweb.mode':            'standalone',
+        }
+        if path_info:
+            environ['PATH_INFO'] = urllib.unquote(path_info)
+        for key, value in self.headers.items():
+            environ['HTTP_' + key.upper().replace('-', '_')] = value
+        # Hack to avoid may TCP packets
+        self.bufferon()
+        appiter=self.server.wsgiapp(environ, self.start_response)
+        for data in appiter:
+            self.write(data)
+            self.bufferoff()
+        self.bufferoff()
+    def do_GET(self):
+        self.serve('GET')
+    def do_POST(self):
+        self.serve('GET')
+class QWebWSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
+    """ QWebWSGIServer
+        qweb_wsgi_autorun(wsgiapp,ip='127.0.0.1',port=8080,threaded=1)
+        A WSGI HTTP server threaded or not and a function to automatically run your
+        app according to the environement (either standalone, CGI or FastCGI).
+
+        This feature is called QWeb autorun. If you want to  To use it on your
+        application use the following lines at the end of the main application
+        python file:
+
+        if __name__ == '__main__':
+            qweb.qweb_wsgi_autorun(your_wsgi_app)
+
+        this function will select the approriate running mode according to the
+        calling environement (http-server, FastCGI or CGI).
+    """
+    def __init__(self, wsgiapp, ip, port, threaded=1, log=1):
+        BaseHTTPServer.HTTPServer.__init__(self, (ip, port), QWebWSGIHandler)
+        self.wsgiapp = wsgiapp
+        self.threaded = threaded
+        self.log = log
+    def process_request(self,*p):
+        if self.threaded:
+            return SocketServer.ThreadingMixIn.process_request(self,*p)
+        else:
+            return BaseHTTPServer.HTTPServer.process_request(self,*p)
+def qweb_wsgi_autorun(wsgiapp,ip='127.0.0.1',port=8080,threaded=1,log=1,callback_ready=None):
+    if sys.platform=='win32':
+        fcgi=0
+    else:
+        fcgi=1
+        sock = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM)
+        try:
+            sock.getpeername()
+        except socket.error, e:
+            if e[0] == errno.ENOTSOCK:
+                fcgi=0
+    if fcgi or os.environ.has_key('REQUEST_METHOD'):
+        import fcgi
+        fcgi.WSGIServer(wsgiapp,multithreaded=False).run()
+    else:
+        if log:
+            print 'Serving on %s:%d'%(ip,port)
+        s=QWebWSGIServer(wsgiapp,ip=ip,port=port,threaded=threaded,log=log)
+        if callback_ready:
+            callback_ready()
+        try:
+            s.serve_forever()
+        except KeyboardInterrupt,e:
+            sys.excepthook(*sys.exc_info())
+
+#----------------------------------------------------------
+# Qweb Documentation
+#----------------------------------------------------------
+def qweb_doc():
+    body=__doc__
+    for i in [QWebXml ,QWebHtml ,QWebForm ,QWebURL ,qweb_control ,QWebRequest ,QWebSession ,QWebWSGIServer ,qweb_wsgi_autorun]:
+        n=i.__name__
+        d=i.__doc__
+        body+='\n\n%s\n%s\n\n%s'%(n,'-'*len(n),d)
+    return body
+
+    print qweb_doc()
+
+#
diff --git a/ajaxterm/ajaxterm/sarissa.js b/ajaxterm/ajaxterm/sarissa.js
new file mode 100644 (file)
index 0000000..220edb2
--- /dev/null
@@ -0,0 +1,647 @@
+/**
+ * ====================================================================
+ * About
+ * ====================================================================
+ * Sarissa is an ECMAScript library acting as a cross-browser wrapper for native XML APIs.
+ * The library supports Gecko based browsers like Mozilla and Firefox,
+ * Internet Explorer (5.5+ with MSXML3.0+), Konqueror, Safari and a little of Opera
+ * @version 0.9.6.1
+ * @author: Manos Batsis, mailto: mbatsis at users full stop sourceforge full stop net
+ * ====================================================================
+ * Licence
+ * ====================================================================
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 or
+ * the GNU Lesser General Public License version 2.1 as published by
+ * the Free Software Foundation (your choice between the two).
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License or GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * or GNU Lesser General Public License along with this program; if not,
+ * write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ * or visit http://www.gnu.org
+ *
+ */
+/**
+ * <p>Sarissa is a utility class. Provides "static" methods for DOMDocument and 
+ * XMLHTTP objects, DOM Node serializatrion to XML strings and other goodies.</p>
+ * @constructor
+ */
+function Sarissa(){};
+/** @private */
+Sarissa.PARSED_OK = "Document contains no parsing errors";
+/**
+ * Tells you whether transformNode and transformNodeToObject are available. This functionality
+ * is contained in sarissa_ieemu_xslt.js and is deprecated. If you want to control XSLT transformations
+ * use the XSLTProcessor
+ * @deprecated
+ * @type boolean
+ */
+Sarissa.IS_ENABLED_TRANSFORM_NODE = false;
+/**
+ * tells you whether XMLHttpRequest (or equivalent) is available
+ * @type boolean
+ */
+Sarissa.IS_ENABLED_XMLHTTP = false;
+/**
+ * tells you whether selectNodes/selectSingleNode is available
+ * @type boolean
+ */
+Sarissa.IS_ENABLED_SELECT_NODES = false;
+var _sarissa_iNsCounter = 0;
+var _SARISSA_IEPREFIX4XSLPARAM = "";
+var _SARISSA_HAS_DOM_IMPLEMENTATION = document.implementation && true;
+var _SARISSA_HAS_DOM_CREATE_DOCUMENT = _SARISSA_HAS_DOM_IMPLEMENTATION && document.implementation.createDocument;
+var _SARISSA_HAS_DOM_FEATURE = _SARISSA_HAS_DOM_IMPLEMENTATION && document.implementation.hasFeature;
+var _SARISSA_IS_MOZ = _SARISSA_HAS_DOM_CREATE_DOCUMENT && _SARISSA_HAS_DOM_FEATURE;
+var _SARISSA_IS_SAFARI = (navigator.userAgent && navigator.vendor && (navigator.userAgent.toLowerCase().indexOf("applewebkit") != -1 || navigator.vendor.indexOf("Apple") != -1));
+var _SARISSA_IS_IE = document.all && window.ActiveXObject && navigator.userAgent.toLowerCase().indexOf("msie") > -1  && navigator.userAgent.toLowerCase().indexOf("opera") == -1;
+if(!window.Node || !window.Node.ELEMENT_NODE){
+    var Node = {ELEMENT_NODE: 1, ATTRIBUTE_NODE: 2, TEXT_NODE: 3, CDATA_SECTION_NODE: 4, ENTITY_REFERENCE_NODE: 5,  ENTITY_NODE: 6, PROCESSING_INSTRUCTION_NODE: 7, COMMENT_NODE: 8, DOCUMENT_NODE: 9, DOCUMENT_TYPE_NODE: 10, DOCUMENT_FRAGMENT_NODE: 11, NOTATION_NODE: 12};
+};
+
+// IE initialization
+if(_SARISSA_IS_IE){
+    // for XSLT parameter names, prefix needed by IE
+    _SARISSA_IEPREFIX4XSLPARAM = "xsl:";
+    // used to store the most recent ProgID available out of the above
+    var _SARISSA_DOM_PROGID = "";
+    var _SARISSA_XMLHTTP_PROGID = "";
+    /**
+     * Called when the Sarissa_xx.js file is parsed, to pick most recent
+     * ProgIDs for IE, then gets destroyed.
+     * @param idList an array of MSXML PROGIDs from which the most recent will be picked for a given object
+     * @param enabledList an array of arrays where each array has two items; the index of the PROGID for which a certain feature is enabled
+     */
+    pickRecentProgID = function (idList, enabledList){
+        // found progID flag
+        var bFound = false;
+        for(var i=0; i < idList.length && !bFound; i++){
+            try{
+                var oDoc = new ActiveXObject(idList[i]);
+                o2Store = idList[i];
+                bFound = true;
+                for(var j=0;j<enabledList.length;j++)
+                    if(i <= enabledList[j][1])
+                        Sarissa["IS_ENABLED_"+enabledList[j][0]] = true;
+            }catch (objException){
+                // trap; try next progID
+            };
+        };
+        if (!bFound)
+            throw "Could not retreive a valid progID of Class: " + idList[idList.length-1]+". (original exception: "+e+")";
+        idList = null;
+        return o2Store;
+    };
+    // pick best available MSXML progIDs
+    _SARISSA_DOM_PROGID = pickRecentProgID(["Msxml2.DOMDocument.5.0", "Msxml2.DOMDocument.4.0", "Msxml2.DOMDocument.3.0", "MSXML2.DOMDocument", "MSXML.DOMDocument", "Microsoft.XMLDOM"], [["SELECT_NODES", 2],["TRANSFORM_NODE", 2]]);
+    _SARISSA_XMLHTTP_PROGID = pickRecentProgID(["Msxml2.XMLHTTP.5.0", "Msxml2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP"], [["XMLHTTP", 4]]);
+    _SARISSA_THREADEDDOM_PROGID = pickRecentProgID(["Msxml2.FreeThreadedDOMDocument.5.0", "MSXML2.FreeThreadedDOMDocument.4.0", "MSXML2.FreeThreadedDOMDocument.3.0"]);
+    _SARISSA_XSLTEMPLATE_PROGID = pickRecentProgID(["Msxml2.XSLTemplate.5.0", "Msxml2.XSLTemplate.4.0", "MSXML2.XSLTemplate.3.0"], [["XSLTPROC", 2]]);
+    // we dont need this anymore
+    pickRecentProgID = null;
+    //============================================
+    // Factory methods (IE)
+    //============================================
+    // see non-IE version
+    Sarissa.getDomDocument = function(sUri, sName){
+        var oDoc = new ActiveXObject(_SARISSA_DOM_PROGID);
+        // if a root tag name was provided, we need to load it in the DOM
+        // object
+        if (sName){
+            // if needed, create an artifical namespace prefix the way Moz
+            // does
+            if (sUri){
+                oDoc.loadXML("<a" + _sarissa_iNsCounter + ":" + sName + " xmlns:a" + _sarissa_iNsCounter + "=\"" + sUri + "\" />");
+                // don't use the same prefix again
+                ++_sarissa_iNsCounter;
+            }
+            else
+                oDoc.loadXML("<" + sName + "/>");
+        };
+        return oDoc;
+    };
+    // see non-IE version   
+    Sarissa.getParseErrorText = function (oDoc) {
+        var parseErrorText = Sarissa.PARSED_OK;
+        if(oDoc.parseError != 0){
+            parseErrorText = "XML Parsing Error: " + oDoc.parseError.reason + 
+                "\nLocation: " + oDoc.parseError.url + 
+                "\nLine Number " + oDoc.parseError.line + ", Column " + 
+                oDoc.parseError.linepos + 
+                ":\n" + oDoc.parseError.srcText +
+                "\n";
+            for(var i = 0;  i < oDoc.parseError.linepos;i++){
+                parseErrorText += "-";
+            };
+            parseErrorText +=  "^\n";
+        };
+        return parseErrorText;
+    };
+    // see non-IE version
+    Sarissa.setXpathNamespaces = function(oDoc, sNsSet) {
+        oDoc.setProperty("SelectionLanguage", "XPath");
+        oDoc.setProperty("SelectionNamespaces", sNsSet);
+    };   
+    /**
+     * Basic implementation of Mozilla's XSLTProcessor for IE. 
+     * Reuses the same XSLT stylesheet for multiple transforms
+     * @constructor
+     */
+    XSLTProcessor = function(){
+        this.template = new ActiveXObject(_SARISSA_XSLTEMPLATE_PROGID);
+        this.processor = null;
+    };
+    /**
+     * Impoprts the given XSLT DOM and compiles it to a reusable transform
+     * @argument xslDoc The XSLT DOMDocument to import
+     */
+    XSLTProcessor.prototype.importStylesheet = function(xslDoc){
+        // convert stylesheet to free threaded
+        var converted = new ActiveXObject(_SARISSA_THREADEDDOM_PROGID); 
+        converted.loadXML(xslDoc.xml);
+        this.template.stylesheet = converted;
+        this.processor = this.template.createProcessor();
+        // (re)set default param values
+        this.paramsSet = new Array();
+    };
+    /**
+     * Transform the given XML DOM
+     * @argument sourceDoc The XML DOMDocument to transform
+     * @return The transformation result as a DOM Document
+     */
+    XSLTProcessor.prototype.transformToDocument = function(sourceDoc){
+        this.processor.input = sourceDoc;
+        var outDoc = new ActiveXObject(_SARISSA_DOM_PROGID);
+        this.processor.output = outDoc; 
+        this.processor.transform();
+        return outDoc;
+    };
+    /**
+     * Set global XSLT parameter of the imported stylesheet
+     * @argument nsURI The parameter namespace URI
+     * @argument name The parameter base name
+     * @argument value The new parameter value
+     */
+    XSLTProcessor.prototype.setParameter = function(nsURI, name, value){
+        /* nsURI is optional but cannot be null */
+        if(nsURI){
+            this.processor.addParameter(name, value, nsURI);
+        }else{
+            this.processor.addParameter(name, value);
+        };
+        /* update updated params for getParameter */
+        if(!this.paramsSet[""+nsURI]){
+            this.paramsSet[""+nsURI] = new Array();
+        };
+        this.paramsSet[""+nsURI][name] = value;
+    };
+    /**
+     * Gets a parameter if previously set by setParameter. Returns null
+     * otherwise
+     * @argument name The parameter base name
+     * @argument value The new parameter value
+     * @return The parameter value if reviously set by setParameter, null otherwise
+     */
+    XSLTProcessor.prototype.getParameter = function(nsURI, name){
+        nsURI = nsURI || "";
+        if(nsURI in this.paramsSet && name in this.paramsSet[nsURI]){
+            return this.paramsSet[nsURI][name];
+        }else{
+            return null;
+        };
+    };
+}
+else{ /* end IE initialization, try to deal with real browsers now ;-) */
+    if(_SARISSA_HAS_DOM_CREATE_DOCUMENT){
+        /**
+         * <p>Ensures the document was loaded correctly, otherwise sets the
+         * parseError to -1 to indicate something went wrong. Internal use</p>
+         * @private
+         */
+        Sarissa.__handleLoad__ = function(oDoc){
+            if (!oDoc.documentElement || oDoc.documentElement.tagName == "parsererror")
+                oDoc.parseError = -1;
+            Sarissa.__setReadyState__(oDoc, 4);
+        };
+        /**
+        * <p>Attached by an event handler to the load event. Internal use.</p>
+        * @private
+        */
+        _sarissa_XMLDocument_onload = function(){
+            Sarissa.__handleLoad__(this);
+        };
+        /**
+         * <p>Sets the readyState property of the given DOM Document object.
+         * Internal use.</p>
+         * @private
+         * @argument oDoc the DOM Document object to fire the
+         *          readystatechange event
+         * @argument iReadyState the number to change the readystate property to
+         */
+        Sarissa.__setReadyState__ = function(oDoc, iReadyState){
+            oDoc.readyState = iReadyState;
+            if (oDoc.onreadystatechange != null && typeof oDoc.onreadystatechange == "function")
+                oDoc.onreadystatechange();
+        };
+        Sarissa.getDomDocument = function(sUri, sName){
+            var oDoc = document.implementation.createDocument(sUri?sUri:"", sName?sName:"", null);
+            oDoc.addEventListener("load", _sarissa_XMLDocument_onload, false);
+            return oDoc;
+        };
+        if(window.XMLDocument){
+            /**
+            * <p>Emulate IE's onreadystatechange attribute</p>
+            */
+            XMLDocument.prototype.onreadystatechange = null;
+            /**
+            * <p>Emulates IE's readyState property, which always gives an integer from 0 to 4:</p>
+            * <ul><li>1 == LOADING,</li>
+            * <li>2 == LOADED,</li>
+            * <li>3 == INTERACTIVE,</li>
+            * <li>4 == COMPLETED</li></ul>
+            */
+            XMLDocument.prototype.readyState = 0;
+            /**
+            * <p>Emulate IE's parseError attribute</p>
+            */
+            XMLDocument.prototype.parseError = 0;
+
+            // NOTE: setting async to false will only work with documents
+            // called over HTTP (meaning a server), not the local file system,
+            // unless you are using Moz 1.4+.
+            // BTW the try>catch block is for 1.4; I haven't found a way to check if
+            // the property is implemented without
+            // causing an error and I dont want to use user agent stuff for that...
+            var _SARISSA_SYNC_NON_IMPLEMENTED = false;// ("async" in XMLDocument.prototype) ? false: true;
+            /**
+            * <p>Keeps a handle to the original load() method. Internal use and only
+            * if Mozilla version is lower than 1.4</p>
+            * @private
+            */
+            XMLDocument.prototype._sarissa_load = XMLDocument.prototype.load;
+
+            /**
+            * <p>Overrides the original load method to provide synchronous loading for
+            * Mozilla versions prior to 1.4, using an XMLHttpRequest object (if
+            * async is set to false)</p>
+            * @returns the DOM Object as it was before the load() call (may be  empty)
+            */
+            XMLDocument.prototype.load = function(sURI) {
+                var oDoc = document.implementation.createDocument("", "", null);
+                Sarissa.copyChildNodes(this, oDoc);
+                this.parseError = 0;
+                Sarissa.__setReadyState__(this, 1);
+                try {
+                    if(this.async == false && _SARISSA_SYNC_NON_IMPLEMENTED) {
+                        var tmp = new XMLHttpRequest();
+                        tmp.open("GET", sURI, false);
+                        tmp.send(null);
+                        Sarissa.__setReadyState__(this, 2);
+                        Sarissa.copyChildNodes(tmp.responseXML, this);
+                        Sarissa.__setReadyState__(this, 3);
+                    }
+                    else {
+                        this._sarissa_load(sURI);
+                    };
+                }
+                catch (objException) {
+                    this.parseError = -1;
+                }
+                finally {
+                    if(this.async == false){
+                        Sarissa.__handleLoad__(this);
+                    };
+                };
+                return oDoc;
+            };
+            
+            
+        }//if(window.XMLDocument)
+        else if(document.implementation && document.implementation.hasFeature && document.implementation.hasFeature('LS', '3.0')){
+            Document.prototype.async = true;
+            Document.prototype.onreadystatechange = null;
+            Document.prototype.parseError = 0;
+            Document.prototype.load = function(sURI) {
+                var parser = document.implementation.createLSParser(this.async ? document.implementation.MODE_ASYNCHRONOUS : document.implementation.MODE_SYNCHRONOUS, null);
+                if(this.async){
+                    var self = this;
+                    parser.addEventListener("load", 
+                        function(e) { 
+                            self.readyState = 4;
+                            Sarissa.copyChildNodes(e.newDocument, self.documentElement, false);
+                            self.onreadystatechange.call(); 
+                        }, 
+                        false); 
+                };
+                try {
+                    var oDoc = parser.parseURI(sURI);
+                }
+                catch(e){
+                    this.parseError = -1;
+                };
+                if(!this.async)
+                   Sarissa.copyChildNodes(oDoc, this.documentElement, false);
+                return oDoc;
+            };
+            /**
+            * <p>Factory method to obtain a new DOM Document object</p>
+            * @argument sUri the namespace of the root node (if any)
+            * @argument sUri the local name of the root node (if any)
+            * @returns a new DOM Document
+            */
+            Sarissa.getDomDocument = function(sUri, sName){
+                return document.implementation.createDocument(sUri?sUri:"", sName?sName:"", null);
+            };        
+        };
+    };//if(_SARISSA_HAS_DOM_CREATE_DOCUMENT)
+};
+//==========================================
+// Common stuff
+//==========================================
+if(!window.DOMParser){
+    /*
+    * DOMParser is a utility class, used to construct DOMDocuments from XML strings
+    * @constructor
+    */
+    DOMParser = function() {
+    };
+    if(_SARISSA_IS_SAFARI){
+        /** 
+        * Construct a new DOM Document from the given XMLstring
+        * @param sXml the given XML string
+        * @param contentType the content type of the document the given string represents (one of text/xml, application/xml, application/xhtml+xml). 
+        * @return a new DOM Document from the given XML string
+        */
+        DOMParser.prototype.parseFromString = function(sXml, contentType){
+            if(contentType.toLowerCase() != "application/xml"){
+                throw "Cannot handle content type: \"" + contentType + "\"";
+            };
+            var xmlhttp = new XMLHttpRequest();
+            xmlhttp.open("GET", "data:text/xml;charset=utf-8," + encodeURIComponent(str), false);
+            xmlhttp.send(null);
+            return xmlhttp.responseXML;
+        };
+    }else if(Sarissa.getDomDocument && Sarissa.getDomDocument() && "loadXML" in Sarissa.getDomDocument()){
+        DOMParser.prototype.parseFromString = function(sXml, contentType){
+            var doc = Sarissa.getDomDocument();
+            doc.loadXML(sXml);
+            return doc;
+        };
+    };
+};
+
+if(window.XMLHttpRequest){
+    Sarissa.IS_ENABLED_XMLHTTP = true;
+}
+else if(_SARISSA_IS_IE){
+    /**
+     * Emulate XMLHttpRequest
+     * @constructor
+     */
+    XMLHttpRequest = function() {
+        return new ActiveXObject(_SARISSA_XMLHTTP_PROGID);
+    };
+    Sarissa.IS_ENABLED_XMLHTTP = true;
+};
+
+if(!window.document.importNode && _SARISSA_IS_IE){
+    try{
+        /**
+        * Implements importNode for the current window document in IE using innerHTML.
+        * Testing showed that DOM was multiple times slower than innerHTML for this,
+        * sorry folks. If you encounter trouble (who knows what IE does behind innerHTML)
+        * please gimme a call.
+        * @param oNode the Node to import
+        * @param bChildren whether to include the children of oNode
+        * @returns the imported node for further use
+        */
+        window.document.importNode = function(oNode, bChildren){
+            var importNode = document.createElement("div");
+            if(bChildren)
+                importNode.innerHTML = Sarissa.serialize(oNode);
+            else
+                importNode.innerHTML = Sarissa.serialize(oNode.cloneNode(false));
+            return importNode.firstChild;
+        };
+        }catch(e){};
+};
+if(!Sarissa.getParseErrorText){
+    /**
+     * <p>Returns a human readable description of the parsing error. Usefull
+     * for debugging. Tip: append the returned error string in a &lt;pre&gt;
+     * element if you want to render it.</p>
+     * <p>Many thanks to Christian Stocker for the initial patch.</p>
+     * @argument oDoc The target DOM document
+     * @returns The parsing error description of the target Document in
+     *          human readable form (preformated text)
+     */
+    Sarissa.getParseErrorText = function (oDoc){
+        var parseErrorText = Sarissa.PARSED_OK;
+        if(oDoc && oDoc.parseError && oDoc.parseError != 0){
+            /*moz*/
+            if(oDoc.documentElement.tagName == "parsererror"){
+                parseErrorText = oDoc.documentElement.firstChild.data;
+                parseErrorText += "\n" +  oDoc.documentElement.firstChild.nextSibling.firstChild.data;
+            }/*konq*/
+            else{
+                parseErrorText = Sarissa.getText(oDoc.documentElement);/*.getElementsByTagName("h1")[0], false) + "\n";
+                parseErrorText += Sarissa.getText(oDoc.documentElement.getElementsByTagName("body")[0], false) + "\n";
+                parseErrorText += Sarissa.getText(oDoc.documentElement.getElementsByTagName("pre")[0], false);*/
+            };
+        };
+        return parseErrorText;
+    };
+};
+Sarissa.getText = function(oNode, deep){
+    var s = "";
+    var nodes = oNode.childNodes;
+    for(var i=0; i < nodes.length; i++){
+        var node = nodes[i];
+        var nodeType = node.nodeType;
+        if(nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE){
+            s += node.data;
+        }else if(deep == true
+                    && (nodeType == Node.ELEMENT_NODE
+                        || nodeType == Node.DOCUMENT_NODE
+                        || nodeType == Node.DOCUMENT_FRAGMENT_NODE)){
+            s += Sarissa.getText(node, true);
+        };
+    };
+    return s;
+};
+if(window.XMLSerializer){
+    /**
+     * <p>Factory method to obtain the serialization of a DOM Node</p>
+     * @returns the serialized Node as an XML string
+     */
+    Sarissa.serialize = function(oDoc){
+        var s = null;
+        if(oDoc){
+            s = oDoc.innerHTML?oDoc.innerHTML:(new XMLSerializer()).serializeToString(oDoc);
+        };
+        return s;
+    };
+}else{
+    if(Sarissa.getDomDocument && (Sarissa.getDomDocument("","foo", null)).xml){
+        // see non-IE version
+        Sarissa.serialize = function(oDoc) {
+            var s = null;
+            if(oDoc){
+                s = oDoc.innerHTML?oDoc.innerHTML:oDoc.xml;
+            };
+            return s;
+        };
+        /**
+         * Utility class to serialize DOM Node objects to XML strings
+         * @constructor
+         */
+        XMLSerializer = function(){};
+        /**
+         * Serialize the given DOM Node to an XML string
+         * @param oNode the DOM Node to serialize
+         */
+        XMLSerializer.prototype.serializeToString = function(oNode) {
+            return oNode.xml;
+        };
+    };
+};
+
+/**
+ * strips tags from a markup string
+ */
+Sarissa.stripTags = function (s) {
+    return s.replace(/<[^>]+>/g,"");
+};
+/**
+ * <p>Deletes all child nodes of the given node</p>
+ * @argument oNode the Node to empty
+ */
+Sarissa.clearChildNodes = function(oNode) {
+    // need to check for firstChild due to opera 8 bug with hasChildNodes
+    while(oNode.firstChild){
+        oNode.removeChild(oNode.firstChild);
+    };
+};
+/**
+ * <p> Copies the childNodes of nodeFrom to nodeTo</p>
+ * <p> <b>Note:</b> The second object's original content is deleted before 
+ * the copy operation, unless you supply a true third parameter</p>
+ * @argument nodeFrom the Node to copy the childNodes from
+ * @argument nodeTo the Node to copy the childNodes to
+ * @argument bPreserveExisting whether to preserve the original content of nodeTo, default is false
+ */
+Sarissa.copyChildNodes = function(nodeFrom, nodeTo, bPreserveExisting) {
+    if((!nodeFrom) || (!nodeTo)){
+        throw "Both source and destination nodes must be provided";
+    };
+    if(!bPreserveExisting){
+        Sarissa.clearChildNodes(nodeTo);
+    };
+    var ownerDoc = nodeTo.nodeType == Node.DOCUMENT_NODE ? nodeTo : nodeTo.ownerDocument;
+    var nodes = nodeFrom.childNodes;
+    if(ownerDoc.importNode && (!_SARISSA_IS_IE)) {
+        for(var i=0;i < nodes.length;i++) {
+            nodeTo.appendChild(ownerDoc.importNode(nodes[i], true));
+        };
+    }
+    else{
+        for(var i=0;i < nodes.length;i++) {
+            nodeTo.appendChild(nodes[i].cloneNode(true));
+        };
+    };
+};
+
+/**
+ * <p> Moves the childNodes of nodeFrom to nodeTo</p>
+ * <p> <b>Note:</b> The second object's original content is deleted before 
+ * the move operation, unless you supply a true third parameter</p>
+ * @argument nodeFrom the Node to copy the childNodes from
+ * @argument nodeTo the Node to copy the childNodes to
+ * @argument bPreserveExisting whether to preserve the original content of nodeTo, default is
+ */ 
+Sarissa.moveChildNodes = function(nodeFrom, nodeTo, bPreserveExisting) {
+    if((!nodeFrom) || (!nodeTo)){
+        throw "Both source and destination nodes must be provided";
+    };
+    if(!bPreserveExisting){
+        Sarissa.clearChildNodes(nodeTo);
+    };
+    var nodes = nodeFrom.childNodes;
+    // if within the same doc, just move, else copy and delete
+    if(nodeFrom.ownerDocument == nodeTo.ownerDocument){
+        while(nodeFrom.firstChild){
+            nodeTo.appendChild(nodeFrom.firstChild);
+        };
+    }else{
+        var ownerDoc = nodeTo.nodeType == Node.DOCUMENT_NODE ? nodeTo : nodeTo.ownerDocument;
+        if(ownerDoc.importNode && (!_SARISSA_IS_IE)) {
+           for(var i=0;i < nodes.length;i++) {
+               nodeTo.appendChild(ownerDoc.importNode(nodes[i], true));
+           };
+        }else{
+           for(var i=0;i < nodes.length;i++) {
+               nodeTo.appendChild(nodes[i].cloneNode(true));
+           };
+        };
+        Sarissa.clearChildNodes(nodeFrom);
+    };
+};
+
+/** 
+ * <p>Serialize any object to an XML string. All properties are serialized using the property name
+ * as the XML element name. Array elements are rendered as <code>array-item</code> elements, 
+ * using their index/key as the value of the <code>key</code> attribute.</p>
+ * @argument anyObject the object to serialize
+ * @argument objectName a name for that object
+ * @return the XML serializationj of the given object as a string
+ */
+Sarissa.xmlize = function(anyObject, objectName, indentSpace){
+    indentSpace = indentSpace?indentSpace:'';
+    var s = indentSpace  + '<' + objectName + '>';
+    var isLeaf = false;
+    if(!(anyObject instanceof Object) || anyObject instanceof Number || anyObject instanceof String 
+        || anyObject instanceof Boolean || anyObject instanceof Date){
+        s += Sarissa.escape(""+anyObject);
+        isLeaf = true;
+    }else{
+        s += "\n";
+        var itemKey = '';
+        var isArrayItem = anyObject instanceof Array;
+        for(var name in anyObject){
+            s += Sarissa.xmlize(anyObject[name], (isArrayItem?"array-item key=\""+name+"\"":name), indentSpace + "   ");
+        };
+        s += indentSpace;
+    };
+    return s += (objectName.indexOf(' ')!=-1?"</array-item>\n":"</" + objectName + ">\n");
+};
+
+/** 
+ * Escape the given string chacters that correspond to the five predefined XML entities
+ * @param sXml the string to escape
+ */
+Sarissa.escape = function(sXml){
+    return sXml.replace(/&/g, "&amp;")
+        .replace(/</g, "&lt;")
+        .replace(/>/g, "&gt;")
+        .replace(/"/g, "&quot;")
+        .replace(/'/g, "&apos;");
+};
+
+/** 
+ * Unescape the given string. This turns the occurences of the predefined XML 
+ * entities to become the characters they represent correspond to the five predefined XML entities
+ * @param sXml the string to unescape
+ */
+Sarissa.unescape = function(sXml){
+    return sXml.replace(/&apos;/g,"'")
+        .replace(/&quot;/g,"\"")
+        .replace(/&gt;/g,">")
+        .replace(/&lt;/g,"<")
+        .replace(/&amp;/g,"&");
+};
+//   EOF
diff --git a/ajaxterm/ajaxterm/sarissa_dhtml.js b/ajaxterm/ajaxterm/sarissa_dhtml.js
new file mode 100644 (file)
index 0000000..2d85c81
--- /dev/null
@@ -0,0 +1,105 @@
+/**
+ * ====================================================================
+ * About
+ * ====================================================================
+ * Sarissa cross browser XML library - AJAX module
+ * @version 0.9.6.1
+ * @author: Copyright Manos Batsis, mailto: mbatsis at users full stop sourceforge full stop net
+ *
+ * This module contains some convinient AJAX tricks based on Sarissa 
+ *
+ * ====================================================================
+ * Licence
+ * ====================================================================
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 or
+ * the GNU Lesser General Public License version 2.1 as published by
+ * the Free Software Foundation (your choice between the two).
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License or GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * or GNU Lesser General Public License along with this program; if not,
+ * write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ * or visit http://www.gnu.org
+ *
+ */
+/**
+ * Update an element with response of a GET request on the given URL. 
+ * @addon
+ * @param sFromUrl the URL to make the request to
+ * @param oTargetElement the element to update
+ * @param xsltproc (optional) the transformer to use on the returned
+ *                  content before updating the target element with it
+ */
+Sarissa.updateContentFromURI = function(sFromUrl, oTargetElement, xsltproc) {
+    try{
+        oTargetElement.style.cursor = "wait";
+        var xmlhttp = new XMLHttpRequest();
+        xmlhttp.open("GET", sFromUrl);
+        function sarissa_dhtml_loadHandler() {
+            if (xmlhttp.readyState == 4) {
+                oTargetElement.style.cursor = "auto";
+                Sarissa.updateContentFromNode(xmlhttp.responseXML, oTargetElement, xsltproc);
+            };
+        };
+        xmlhttp.onreadystatechange = sarissa_dhtml_loadHandler;
+        xmlhttp.send(null);
+        oTargetElement.style.cursor = "auto";
+    }
+    catch(e){
+        oTargetElement.style.cursor = "auto";
+        throw e;
+    };
+};
+
+/**
+ * Update an element's content with the given DOM node.
+ * @addon
+ * @param sFromUrl the URL to make the request to
+ * @param oTargetElement the element to update
+ * @param xsltproc (optional) the transformer to use on the given 
+ *                  DOM node before updating the target element with it
+ */
+Sarissa.updateContentFromNode = function(oNode, oTargetElement, xsltproc) {
+    try {
+        oTargetElement.style.cursor = "wait";
+        Sarissa.clearChildNodes(oTargetElement);
+        // check for parsing errors
+        var ownerDoc = oNode.nodeType == Node.DOCUMENT_NODE?oNode:oNode.ownerDocument;
+        if(ownerDoc.parseError && ownerDoc.parseError != 0) {
+            var pre = document.createElement("pre");
+            pre.appendChild(document.createTextNode(Sarissa.getParseErrorText(ownerDoc)));
+            oTargetElement.appendChild(pre);
+        }
+        else {
+            // transform if appropriate
+            if(xsltproc) {
+                oNode = xsltproc.transformToDocument(oNode);
+            };
+            // be smart, maybe the user wants to display the source instead
+            if(oTargetElement.tagName.toLowerCase == "textarea" || oTargetElement.tagName.toLowerCase == "input") {
+                oTargetElement.value = Sarissa.serialize(oNode);
+            }
+            else {
+                // ok that was not smart; it was paranoid. Keep up the good work by trying to use DOM instead of innerHTML
+                if(oNode.nodeType == Node.DOCUMENT_NODE || oNode.ownerDocument.documentElement == oNode) {
+                    oTargetElement.innerHTML = Sarissa.serialize(oNode);
+                }
+                else{
+                    oTargetElement.appendChild(oTargetElement.ownerDocument.importNode(oNode, true));
+                };
+            };  
+        };
+    }
+    catch(e) {
+        throw e;
+    }
+    finally{
+        oTargetElement.style.cursor = "auto";
+    };
+};
+
diff --git a/ajaxterm/images/icon.gif b/ajaxterm/images/icon.gif
new file mode 100644 (file)
index 0000000..9b371fc
Binary files /dev/null and b/ajaxterm/images/icon.gif differ
diff --git a/ajaxterm/index.cgi b/ajaxterm/index.cgi
new file mode 100755 (executable)
index 0000000..ee7d27f
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/local/bin/perl
+# Start the Ajaxterm webserver on a random port, then print an iframe for
+# a URL that proxies to it
+
+BEGIN { push(@INC, ".."); };
+use WebminCore;
+use Socket;
+&init_config();
+
+&ui_print_header(undef, $text{'index_title'}, "", undef, 1, 1);
+
+# Check for python
+if (!&has_command("python")) {
+       &ui_print_endpage(&text('index_epython', "<tt>python</tt>"));
+       }
+
+# Pick a free port
+&get_miniserv_config(\%miniserv);
+$port = $miniserv{'port'} + 1;
+$proto = getprotobyname('tcp');
+socket(TEST, PF_INET, SOCK_STREAM, $proto) ||
+       &error("Socket failed : $!");
+setsockopt(TEST, SOL_SOCKET, SO_REUSEADDR, pack("l", 1));
+while(1) {
+       last if (bind(TEST, sockaddr_in($port, INADDR_ANY)));
+       $port++;
+       }
+close(TEST);
+
+# Run the Ajaxterm webserver
+system("cd $module_root_directory/ajaxterm ; python ajaxterm.py --port $port --log >/tmp/ajaxterm.out 2>&1 </dev/null &");
+
+# Wait for it to come up
+$try = 0;
+while(1) {
+       my $err;
+       &open_socket("localhost", $port, TEST2, \$err);
+       last if ($err);
+       $try++;
+       if ($try > 30) {
+               &error(&text('index_estart', 30, $port));
+               }
+       sleep(1);
+       }
+close(TEST2);
+
+# Show the iframe
+print "<iframe src=$gconfig{'webprefix'}/$module_name/proxy.cgi/$port/ ",
+      "width=100% height=90%></iframe>\n";
+
+&ui_print_footer("/", $text{'index'});
+
diff --git a/ajaxterm/module.info b/ajaxterm/module.info
new file mode 100644 (file)
index 0000000..cc40db9
--- /dev/null
@@ -0,0 +1,2 @@
+desc=AJAX Login
+name=ajaxterm
diff --git a/ajaxterm/proxy.cgi b/ajaxterm/proxy.cgi
new file mode 100755 (executable)
index 0000000..89c47e7
--- /dev/null
@@ -0,0 +1,47 @@
+#!/usr/local/bin/perl
+# Proxy an Ajaxterm request to the real port
+
+BEGIN { push(@INC, ".."); };
+use WebminCore;
+&init_config();
+
+# Parse out port
+$ENV{'PATH_INFO'} =~ /^\/(\d+)(.*)$/ ||
+       &error("Missing or invalid PATH_INFO");
+$port = $1;
+$path = $2;
+$| = 1;
+$meth = $ENV{'REQUEST_METHOD'};
+
+# Connect to the Ajaxterm server, send HTTP request
+$con = &make_http_connection("localhost", $port, 0, $meth, $path);
+&error($con) if (!ref($con));
+&write_http_connection($con, "Host: localhost\r\n");
+&write_http_connection($con, "User-agent: Webmin\r\n");
+$cl = $ENV{'CONTENT_LENGTH'};
+&write_http_connection($con, "Content-length: $cl\r\n") if ($cl);
+&write_http_connection($con, "Content-type: $ENV{'CONTENT_TYPE'}\r\n")
+        if ($ENV{'CONTENT_TYPE'});
+&write_http_connection($con, "\r\n");
+if ($cl) {
+        &read_fully(STDIN, \$post, $cl);
+        &write_http_connection($con, $post);
+        }
+
+# read back the headers
+$dummy = &read_http_connection($con);
+while(1) {
+        ($headline = &read_http_connection($con)) =~ s/\r|\n//g;
+        last if (!$headline);
+        $headline =~ /^(\S+):\s+(.*)$/ || &error("Bad header");
+        $header{lc($1)} = $2;
+        $headers .= $headline."\n";
+        }
+print $headers,"\n";
+
+# read back contents
+while($buf = &read_http_connection($con, 1024)) {
+       print $buf;
+        }
+&close_http_connection($con);
+