From 81f71efdcbeabd8a67054b6e9e432175ea47d785 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 Sep 2010 19:08:22 -0700 Subject: [PATCH 01/40] initial commit of ajaxterm --- tools/ajaxterm/README.txt | 120 +++ tools/ajaxterm/ajaxterm.1 | 35 + tools/ajaxterm/ajaxterm.css | 64 ++ tools/ajaxterm/ajaxterm.html | 19 + tools/ajaxterm/ajaxterm.js | 279 +++++ tools/ajaxterm/ajaxterm.py | 567 +++++++++++ tools/ajaxterm/configure | 32 + tools/ajaxterm/configure.ajaxterm.bin | 2 + tools/ajaxterm/configure.initd.debian | 33 + tools/ajaxterm/configure.initd.gentoo | 27 + tools/ajaxterm/configure.initd.redhat | 75 ++ tools/ajaxterm/configure.makefile | 20 + tools/ajaxterm/qweb.py | 1356 +++++++++++++++++++++++++ tools/ajaxterm/sarissa.js | 647 ++++++++++++ tools/ajaxterm/sarissa_dhtml.js | 105 ++ 15 files changed, 3381 insertions(+) create mode 100644 tools/ajaxterm/README.txt create mode 100644 tools/ajaxterm/ajaxterm.1 create mode 100644 tools/ajaxterm/ajaxterm.css create mode 100644 tools/ajaxterm/ajaxterm.html create mode 100644 tools/ajaxterm/ajaxterm.js create mode 100755 tools/ajaxterm/ajaxterm.py create mode 100755 tools/ajaxterm/configure create mode 100644 tools/ajaxterm/configure.ajaxterm.bin create mode 100644 tools/ajaxterm/configure.initd.debian create mode 100644 tools/ajaxterm/configure.initd.gentoo create mode 100644 tools/ajaxterm/configure.initd.redhat create mode 100644 tools/ajaxterm/configure.makefile create mode 100644 tools/ajaxterm/qweb.py create mode 100644 tools/ajaxterm/sarissa.js create mode 100644 tools/ajaxterm/sarissa_dhtml.js diff --git a/tools/ajaxterm/README.txt b/tools/ajaxterm/README.txt new file mode 100644 index 000000000000..4b0ae99afa2e --- /dev/null +++ b/tools/ajaxterm/README.txt @@ -0,0 +1,120 @@ += [http://antony.lesuisse.org/qweb/trac/wiki/AjaxTerm Ajaxterm] = + +Ajaxterm is a web based terminal. It was totally inspired and works almost +exactly like http://anyterm.org/ except it's much easier to install (see +comparaison with anyterm below). + +Ajaxterm written in python (and some AJAX javascript for client side) and depends only on python2.3 or better.[[BR]] +Ajaxterm is '''very simple to install''' on Linux, MacOS X, FreeBSD, Solaris, cygwin and any Unix that runs python2.3.[[BR]] +Ajaxterm was written by Antony Lesuisse (email: al AT udev.org), License Public Domain. + +Use the [/qweb/forum/viewforum.php?id=2 Forum], if you have any question or remark. + +== News == + + * 2006-10-29: v0.10 allow space in login, cgi launch fix, redhat init + * 2006-07-12: v0.9 change uid, daemon fix (Daniel Fischer) + * 2006-07-04: v0.8 add login support to ssh (Sven Geggus), change max width to 256 + * 2006-05-31: v0.7 minor fixes, daemon option + * 2006-05-23: v0.6 Applied debian and gentoo patches, renamed to Ajaxterm, default port 8022 + +== Download and Install == + + * Release: [/qweb/files/Ajaxterm-0.10.tar.gz Ajaxterm-0.10.tar.gz] + * Browse src: [/qweb/trac/browser/trunk/ajaxterm/ ajaxterm/] + +To install Ajaxterm issue the following commands: +{{{ +wget http://antony.lesuisse.org/qweb/files/Ajaxterm-0.10.tar.gz +tar zxvf Ajaxterm-0.10.tar.gz +cd Ajaxterm-0.10 +./ajaxterm.py +}}} +Then point your browser to this URL : http://localhost:8022/ + +== Screenshot == + +{{{ +#!html +
ajaxterm screenshot
+}}} + +== Documentation and Caveats == + + * Ajaxterm only support latin1, if you use Ubuntu or any LANG==en_US.UTF-8 distribution don't forget to "unset LANG". + + * If run as root ajaxterm will run /bin/login, otherwise it will run ssh + localhost. To use an other command use the -c option. + + * By default Ajaxterm only listen at 127.0.0.1:8022. For remote access, it is + strongly recommended to use '''https SSL/TLS''', and that is simple to + configure if you use the apache web server using mod_proxy.[[BR]][[BR]] + Using ssl will also speed up ajaxterm (probably because of keepalive).[[BR]][[BR]] + Here is an configuration example: + +{{{ + Listen 443 + NameVirtualHost *:443 + + + ServerName localhost + SSLEngine On + SSLCertificateKeyFile ssl/apache.pem + SSLCertificateFile ssl/apache.pem + + ProxyRequests Off + + Order deny,allow + Allow from all + + ProxyPass /ajaxterm/ http://localhost:8022/ + ProxyPassReverse /ajaxterm/ http://localhost:8022/ + +}}} + + * Using GET HTTP request seems to speed up ajaxterm, just click on GET in the + interface, but be warned that your keystrokes might be loggued (by apache or + any proxy). I usually enable it after the login. + + * Ajaxterm commandline usage: + +{{{ +usage: ajaxterm.py [options] + +options: + -h, --help show this help message and exit + -pPORT, --port=PORT Set the TCP port (default: 8022) + -cCMD, --command=CMD set the command (default: /bin/login or ssh localhost) + -l, --log log requests to stderr (default: quiet mode) + -d, --daemon run as daemon in the background + -PPIDFILE, --pidfile=PIDFILE + set the pidfile (default: /var/run/ajaxterm.pid) + -iINDEX_FILE, --index=INDEX_FILE + default index file (default: ajaxterm.html) + -uUID, --uid=UID Set the daemon's user id +}}} + + * Ajaxterm was first written as a demo for qweb (my web framework), but + actually doesn't use many features of qweb. + + * Compared to anyterm: + * There are no partial updates, ajaxterm updates either all the screen or + nothing. That make the code simpler and I also think it's faster. HTTP + replies are always gzencoded. When used in 80x25 mode, almost all of + them are below the 1500 bytes (size of an ethernet frame) and we just + replace the screen with the reply (no javascript string handling). + * Ajaxterm polls the server for updates with an exponentially growing + timeout when the screen hasn't changed. The timeout is also resetted as + soon as a key is pressed. Anyterm blocks on a pending request and use a + parallel connection for keypresses. The anyterm approch is better + when there aren't any keypress. + + * Ajaxterm files are released in the Public Domain, (except [http://sarissa.sourceforge.net/doc/ sarissa*] which are LGPL). + +== TODO == + + * insert mode ESC [ 4 h + * change size x,y from gui (sending signal) + * vt102 graphic codepage + * use innerHTML or prototype instead of sarissa + diff --git a/tools/ajaxterm/ajaxterm.1 b/tools/ajaxterm/ajaxterm.1 new file mode 100644 index 000000000000..46f2acb3393d --- /dev/null +++ b/tools/ajaxterm/ajaxterm.1 @@ -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 + +This manual page was written for the Debian system by +Julien Valroff (but may be used by others). + +.SH "REPORTING BUGS" +Report any bugs to the author: Antony Lesuisse + +.SH COPYRIGHT +Copyright Antony Lesuisse + +.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/tools/ajaxterm/ajaxterm.css b/tools/ajaxterm/ajaxterm.css new file mode 100644 index 000000000000..b9a5f877168e --- /dev/null +++ b/tools/ajaxterm/ajaxterm.css @@ -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/tools/ajaxterm/ajaxterm.html b/tools/ajaxterm/ajaxterm.html new file mode 100644 index 000000000000..9edf7596798a --- /dev/null +++ b/tools/ajaxterm/ajaxterm.html @@ -0,0 +1,19 @@ + + + + Ajaxterm + + + + + + + + +
+ + diff --git a/tools/ajaxterm/ajaxterm.js b/tools/ajaxterm/ajaxterm.js new file mode 100644 index 000000000000..07eca3b0b9bb --- /dev/null +++ b/tools/ajaxterm/ajaxterm.js @@ -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, more info'); + 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/tools/ajaxterm/ajaxterm.py b/tools/ajaxterm/ajaxterm.py new file mode 100755 index 000000000000..8b3af37812fc --- /dev/null +++ b/tools/ajaxterm/ajaxterm.py @@ -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.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+='%s'%(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='
%s
'%r + if self.last_html==r: + return '' + 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('') +# 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/tools/ajaxterm/configure b/tools/ajaxterm/configure new file mode 100755 index 000000000000..45391f484759 --- /dev/null +++ b/tools/ajaxterm/configure @@ -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/tools/ajaxterm/configure.ajaxterm.bin b/tools/ajaxterm/configure.ajaxterm.bin new file mode 100644 index 000000000000..4d1f5a98f080 --- /dev/null +++ b/tools/ajaxterm/configure.ajaxterm.bin @@ -0,0 +1,2 @@ +#!/bin/sh +PYTHONPATH=%(lib)s exec %(lib)s/ajaxterm.py $@ diff --git a/tools/ajaxterm/configure.initd.debian b/tools/ajaxterm/configure.initd.debian new file mode 100644 index 000000000000..901082707249 --- /dev/null +++ b/tools/ajaxterm/configure.initd.debian @@ -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/tools/ajaxterm/configure.initd.gentoo b/tools/ajaxterm/configure.initd.gentoo new file mode 100644 index 000000000000..ac28ef0b66cc --- /dev/null +++ b/tools/ajaxterm/configure.initd.gentoo @@ -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/tools/ajaxterm/configure.initd.redhat b/tools/ajaxterm/configure.initd.redhat new file mode 100644 index 000000000000..5c9788574fc7 --- /dev/null +++ b/tools/ajaxterm/configure.initd.redhat @@ -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/tools/ajaxterm/configure.makefile b/tools/ajaxterm/configure.makefile new file mode 100644 index 000000000000..6bd80853dd6a --- /dev/null +++ b/tools/ajaxterm/configure.makefile @@ -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/tools/ajaxterm/qweb.py b/tools/ajaxterm/qweb.py new file mode 100644 index 000000000000..20c5092300ab --- /dev/null +++ b/tools/ajaxterm/qweb.py @@ -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("%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([''%(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: + # + # + # Simple forms: + # + # + # + # + #