Merge lp:~anso/nova/ajaxterm into lp:~hudson-openstack/nova/trunk

Proposed by Anthony Young on 2011-01-03
Status: Merged
Approved by: Soren Hansen on 2011-01-12
Approved revision: 283
Merged at revision: 549
Proposed branch: lp:~anso/nova/ajaxterm
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 4122 lines (+3857/-2)
31 files modified
bin/nova-ajax-console-proxy (+137/-0)
contrib/nova.sh (+2/-0)
nova/api/ec2/cloud.py (+5/-0)
nova/api/openstack/servers.py (+9/-0)
nova/compute/api.py (+20/-1)
nova/compute/manager.py (+8/-0)
nova/flags.py (+8/-1)
nova/tests/test_cloud.py (+13/-0)
nova/tests/test_compute.py (+10/-0)
nova/utils.py (+5/-0)
nova/virt/fake.py (+3/-0)
nova/virt/libvirt.xml.template (+13/-0)
nova/virt/libvirt_conn.py (+45/-0)
nova/virt/xenapi/vmops.py (+5/-0)
nova/virt/xenapi_conn.py (+4/-0)
tools/ajaxterm/README.txt (+120/-0)
tools/ajaxterm/ajaxterm.1 (+35/-0)
tools/ajaxterm/ajaxterm.css (+64/-0)
tools/ajaxterm/ajaxterm.html (+25/-0)
tools/ajaxterm/ajaxterm.js (+279/-0)
tools/ajaxterm/ajaxterm.py (+586/-0)
tools/ajaxterm/configure (+32/-0)
tools/ajaxterm/configure.ajaxterm.bin (+2/-0)
tools/ajaxterm/configure.initd.debian (+33/-0)
tools/ajaxterm/configure.initd.gentoo (+27/-0)
tools/ajaxterm/configure.initd.redhat (+75/-0)
tools/ajaxterm/configure.makefile (+20/-0)
tools/ajaxterm/qweb.py (+1356/-0)
tools/ajaxterm/sarissa.js (+647/-0)
tools/ajaxterm/sarissa_dhtml.js (+105/-0)
tools/euca-get-ajax-console (+164/-0)
To merge this branch: bzr merge lp:~anso/nova/ajaxterm
Reviewer Review Type Date Requested Status
Devin Carlen (community) Approve on 2011-01-11
Eric Day (community) Needs Information on 2011-01-06
Vish Ishaya (community) 2011-01-03 Approve on 2011-01-05
Review via email: mp+45068@code.launchpad.net

Description of the change

This branch adds web based serial console access. Here is an overview of how it works (for libvirt):

1. User requests an ajax console for an instance_id (either through OS api, or tools/euca-get-ajax-console)
  a. api server calls compute worker to complete request
  b. compute worker parses an instance's xml to locate its pseudo terminal (/dev/pts/x)
  c. compute worker spawns an ajaxterm daemon, bound to a random port in a specified range. socat is used to connect to /dev/pts/x. Note that ajaxterm was modified in the following ways:
    i. dies after 5 minutes of inactivity
    ii. now requires token authentication. Previously it was trivial to hijack an ajaxterm
  d. compute worker returns ajaxterm connect information to the api server: port, host, token
  e. api server casts connect information to the nova-ajax-console-proxy (a new service)
  f. api server returns a url for the ajaxterm (eg. http://nova-ajax-console-proxy/?token=123)
2. User now has a url, and can paste it in a browser
  a. Browser sends request to https://nova-ajax-console-proxy/?token=123
  b. nova-ajax-console-proxy maps token to connect information
  c. nova-ajax-console-proxy constructs a proxy to the ajaxterm that is running on the host machine. This is now done with eventlet, though previously it was done using twisted
3. User interacts with console through web browser

NOTE: For this to work as expected, serial console login must be enabled in the instance. Instructions for how to do this on ubuntu can be found here: https://help.ubuntu.com/community/SerialConsoleHowto. Note that you must actively log out of the serial console when you are finished, otherwise the console will remain open even after the ajaxterm term session has ended.

Also note that nova.sh has been modified in this branch to launch nova-ajax-console-proxy.

To post a comment you must log in.
Vish Ishaya (vishvananda) wrote :

Couple small thing:

1. strings should be surrounded by _() for i18n

2. looks like you changed the memory on m1.tiny for testing, probably don't want to merge that in trunk.

review: Needs Fixing
Anthony Young (sleepsonthefloor) wrote :

Thanks vish - fixed the issues you mentioned, and did a bit more cleanup.

Vish Ishaya (vishvananda) wrote :

lgtm now.

review: Approve
Vish Ishaya (vishvananda) wrote :

although looks like it needs another trunk merge

Eric Day (eday) wrote :

nova/boto_extensions.py should go in contrib, no? Doesn't seem like nova/ is a good place for it.

review: Needs Information
Devin Carlen (devcamcar) wrote :

lgtm

review: Approve
OpenStack Infra (hudson-openstack) wrote :
Download full text (4.0 KiB)

The attempt to merge lp:~anso/nova/ajaxterm into lp:nova failed. Below is the output from the failed tests.

nova/compute/api.py:429:77: W291 trailing whitespace
                  'args': {'token': output['token'], 'host': output['host'],
                                                                            ^
    JCR: Trailing whitespace is superfluous.
    FBM: Except when it occurs as part of a blank line (i.e. the line is
         nothing but whitespace). According to Python docs[1] a line with only
         whitespace is considered a blank line, and is to be ignored. However,
         matching a blank line to its indentation level avoids mistakenly
         terminating a multi-line statement (e.g. class declaration) when
         pasting code into the standard Python interpreter.

         [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines

    The warning returned varies on whether the line itself is blank, for easier
    filtering for those who want to indent their blank lines.

    Okay: spam(1)
    W291: spam(1)\s
    W293: class Foo(object):\n \n bang = 12
nova/compute/api.py:430:25: E231 missing whitespace after ':'
                  'port':output['port']}})
                        ^
    JCR: Each comma, semicolon or colon should be followed by whitespace.

    Okay: [a, b]
    Okay: (3,)
    Okay: a[1:4]
    Okay: a[:4]
    Okay: a[1:]
    Okay: a[1:4:2]
    E231: ['a','b']
    E231: foo(bar,baz)
nova/tests/test_cloud.py:171:39: E202 whitespace before '}'
        kwargs = {'image_id': image_id }
                                      ^
    Avoid extraneous whitespace in the following situations:

    - Immediately inside parentheses, brackets or braces.

    - Immediately before a comma, semicolon, or colon.

    Okay: spam(ham[1], {eggs: 2})
    E201: spam( ham[1], {eggs: 2})
    E201: spam(ham[ 1], {eggs: 2})
    E201: spam(ham[1], { eggs: 2})
    E202: spam(ham[1], {eggs: 2} )
    E202: spam(ham[1 ], {eggs: 2})
    E202: spam(ham[1], {eggs: 2 })

    E203: if x == 4: print x, y; x, y = y , x
    E203: if x == 4: print x, y ; x, y = y, x
    E203: if x == 4 : print x, y; x, y = y, x
nova/virt/fake.py:295:1: E302 expected 2 blank lines, found 1
class FakeInstance(object):
^
    Separate top-level function and class definitions with two blank lines.

    Method definitions inside a class are separated by a single blank line.

    Extra blank lines may be used (sparingly) to separate groups of related
    functions. Blank lines may be omitted between a bunch of related
    one-liners (e.g. a set of dummy implementations).

    Use blank lines in functions, sparingly, to indicate logical sections.

    Okay: def a():\n pass\n\n\ndef b():\n pass
    Okay: def a():\n pass\n\n\n# Foo\n# Bar\n\ndef b():\n pass

    E301: class Foo:\n b = 0\n def bar():\n pass
    E302: def a():\n pass\n\ndef b(n):\n pass
    E303: def a():\n pass\n\n\n\ndef b(n):\n pass
    E303: def a():\n\n\n\n pass
    E304: @decorator\n\ndef a():\n pass
nova/virt/libvirt_conn.py:448:30: E231 missing whitespace after ','
            for i in xrange(0,100): # don't loop forever
                ...

Read more...

OpenStack Infra (hudson-openstack) wrote :

Attempt to merge into lp:nova failed due to conflicts:

text conflict in nova/virt/fake.py

lp:~anso/nova/ajaxterm updated on 2011-01-12
283. By Anthony Young on 2011-01-12

merge trunk, fix conflict

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'bin/nova-ajax-console-proxy'
2--- bin/nova-ajax-console-proxy 1970-01-01 00:00:00 +0000
3+++ bin/nova-ajax-console-proxy 2011-01-12 03:17:16 +0000
4@@ -0,0 +1,137 @@
5+#!/usr/bin/env python
6+# pylint: disable-msg=C0103
7+# vim: tabstop=4 shiftwidth=4 softtabstop=4
8+
9+# Copyright 2010 United States Government as represented by the
10+# Administrator of the National Aeronautics and Space Administration.
11+# All Rights Reserved.
12+#
13+# Licensed under the Apache License, Version 2.0 (the "License");
14+# you may not use this file except in compliance with the License.
15+# You may obtain a copy of the License at
16+#
17+# http://www.apache.org/licenses/LICENSE-2.0
18+#
19+# Unless required by applicable law or agreed to in writing, software
20+# distributed under the License is distributed on an "AS IS" BASIS,
21+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22+# See the License for the specific language governing permissions and
23+# limitations under the License.
24+
25+"""Ajax Console Proxy Server"""
26+
27+from eventlet import greenthread
28+from eventlet.green import urllib2
29+
30+import exceptions
31+import gettext
32+import logging
33+import os
34+import sys
35+import time
36+import urlparse
37+
38+# If ../nova/__init__.py exists, add ../ to Python search path, so that
39+# it will override what happens to be installed in /usr/(local/)lib/python...
40+possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
41+ os.pardir,
42+ os.pardir))
43+if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
44+ sys.path.insert(0, possible_topdir)
45+
46+gettext.install('nova', unicode=1)
47+
48+from nova import flags
49+from nova import log as logging
50+from nova import rpc
51+from nova import utils
52+from nova import wsgi
53+
54+FLAGS = flags.FLAGS
55+
56+flags.DEFINE_integer('ajax_console_idle_timeout', 300,
57+ 'Seconds before idle connection destroyed')
58+
59+LOG = logging.getLogger('nova.ajax_console_proxy')
60+LOG.setLevel(logging.DEBUG)
61+LOG.addHandler(logging.StreamHandler())
62+
63+
64+class AjaxConsoleProxy(object):
65+ tokens = {}
66+
67+ def __call__(self, env, start_response):
68+ try:
69+ req_url = '%s://%s%s?%s' % (env['wsgi.url_scheme'],
70+ env['HTTP_HOST'],
71+ env['PATH_INFO'],
72+ env['QUERY_STRING'])
73+ if 'HTTP_REFERER' in env:
74+ auth_url = env['HTTP_REFERER']
75+ else:
76+ auth_url = req_url
77+
78+ auth_params = urlparse.parse_qs(urlparse.urlparse(auth_url).query)
79+ parsed_url = urlparse.urlparse(req_url)
80+
81+ auth_info = AjaxConsoleProxy.tokens[auth_params['token'][0]]
82+ args = auth_info['args']
83+ auth_info['last_activity'] = time.time()
84+
85+ remote_url = ("http://%s:%s%s?token=%s" % (
86+ str(args['host']),
87+ str(args['port']),
88+ parsed_url.path,
89+ str(args['token'])))
90+
91+ opener = urllib2.urlopen(remote_url, env['wsgi.input'].read())
92+ body = opener.read()
93+ info = opener.info()
94+
95+ start_response("200 OK", info.dict.items())
96+ return body
97+ except (exceptions.KeyError):
98+ if env['PATH_INFO'] != '/favicon.ico':
99+ LOG.audit("Unauthorized request %s, %s"
100+ % (req_url, str(env)))
101+ start_response("401 NOT AUTHORIZED", [])
102+ return "Not Authorized"
103+ except Exception:
104+ start_response("500 ERROR", [])
105+ return "Server Error"
106+
107+ def register_listeners(self):
108+ class Callback:
109+ def __call__(self, data, message):
110+ if data['method'] == 'authorize_ajax_console':
111+ AjaxConsoleProxy.tokens[data['args']['token']] = \
112+ {'args': data['args'], 'last_activity': time.time()}
113+
114+ conn = rpc.Connection.instance(new=True)
115+ consumer = rpc.TopicConsumer(
116+ connection=conn,
117+ topic=FLAGS.ajax_console_proxy_topic)
118+ consumer.register_callback(Callback())
119+
120+ def delete_expired_tokens():
121+ now = time.time()
122+ to_delete = []
123+ for k, v in AjaxConsoleProxy.tokens.items():
124+ if now - v['last_activity'] > FLAGS.ajax_console_idle_timeout:
125+ to_delete.append(k)
126+
127+ for k in to_delete:
128+ del AjaxConsoleProxy.tokens[k]
129+
130+ utils.LoopingCall(consumer.fetch, auto_ack=True,
131+ enable_callbacks=True).start(0.1)
132+ utils.LoopingCall(delete_expired_tokens).start(1)
133+
134+if __name__ == '__main__':
135+ utils.default_flagfile()
136+ FLAGS(sys.argv)
137+ server = wsgi.Server()
138+ acp = AjaxConsoleProxy()
139+ acp.register_listeners()
140+ server.start(acp, FLAGS.ajax_console_proxy_port, host='0.0.0.0')
141+ server.wait()
142
143=== modified file 'contrib/nova.sh'
144--- contrib/nova.sh 2010-12-16 11:35:46 +0000
145+++ contrib/nova.sh 2011-01-12 03:17:16 +0000
146@@ -78,6 +78,7 @@
147 sudo apt-get install -y user-mode-linux kvm libvirt-bin
148 sudo apt-get install -y screen euca2ools vlan curl rabbitmq-server
149 sudo apt-get install -y lvm2 iscsitarget open-iscsi
150+ sudo apt-get install -y socat
151 echo "ISCSITARGET_ENABLE=true" | sudo tee /etc/default/iscsitarget
152 sudo /etc/init.d/iscsitarget restart
153 sudo modprobe kvm
154@@ -155,6 +156,7 @@
155 screen_it network "$NOVA_DIR/bin/nova-network"
156 screen_it scheduler "$NOVA_DIR/bin/nova-scheduler"
157 screen_it volume "$NOVA_DIR/bin/nova-volume"
158+ screen_it ajax_console_proxy "$NOVA_DIR/bin/nova-ajax-console-proxy"
159 screen_it test ". $NOVA_DIR/novarc"
160 screen -S nova -x
161 fi
162
163=== modified file 'nova/api/ec2/cloud.py'
164--- nova/api/ec2/cloud.py 2011-01-11 23:03:15 +0000
165+++ nova/api/ec2/cloud.py 2011-01-12 03:17:16 +0000
166@@ -501,6 +501,11 @@
167 "Timestamp": now,
168 "output": base64.b64encode(output)}
169
170+ def get_ajax_console(self, context, instance_id, **kwargs):
171+ ec2_id = instance_id[0]
172+ internal_id = ec2_id_to_id(ec2_id)
173+ return self.compute_api.get_ajax_console(context, internal_id)
174+
175 def describe_volumes(self, context, volume_id=None, **kwargs):
176 volumes = self.volume_api.get_all(context)
177 # NOTE(vish): volume_id is an optional list of volume ids to filter by.
178
179=== modified file 'nova/api/openstack/servers.py'
180--- nova/api/openstack/servers.py 2011-01-11 06:47:35 +0000
181+++ nova/api/openstack/servers.py 2011-01-12 03:17:16 +0000
182@@ -283,6 +283,15 @@
183 return faults.Fault(exc.HTTPUnprocessableEntity())
184 return exc.HTTPAccepted()
185
186+ def get_ajax_console(self, req, id):
187+ """ Returns a url to an instance's ajaxterm console. """
188+ try:
189+ self.compute_api.get_ajax_console(req.environ['nova.context'],
190+ int(id))
191+ except exception.NotFound:
192+ return faults.Fault(exc.HTTPNotFound())
193+ return exc.HTTPAccepted()
194+
195 def diagnostics(self, req, id):
196 """Permit Admins to retrieve server diagnostics."""
197 ctxt = req.environ["nova.context"]
198
199=== modified file 'nova/compute/api.py'
200--- nova/compute/api.py 2011-01-11 06:47:35 +0000
201+++ nova/compute/api.py 2011-01-12 03:17:16 +0000
202@@ -414,7 +414,26 @@
203 rpc.cast(context,
204 self.db.queue_get_for(context, FLAGS.compute_topic, host),
205 {"method": "unrescue_instance",
206- "args": {"instance_id": instance_id}})
207+ "args": {"instance_id": instance['id']}})
208+
209+ def get_ajax_console(self, context, instance_id):
210+ """Get a url to an AJAX Console"""
211+
212+ instance = self.get(context, instance_id)
213+
214+ output = rpc.call(context,
215+ '%s.%s' % (FLAGS.compute_topic,
216+ instance['host']),
217+ {'method': 'get_ajax_console',
218+ 'args': {'instance_id': instance['id']}})
219+
220+ rpc.cast(context, '%s' % FLAGS.ajax_console_proxy_topic,
221+ {'method': 'authorize_ajax_console',
222+ 'args': {'token': output['token'], 'host': output['host'],
223+ 'port': output['port']}})
224+
225+ return {'url': '%s?token=%s' % (FLAGS.ajax_console_proxy_url,
226+ output['token'])}
227
228 def lock(self, context, instance_id):
229 """
230
231=== modified file 'nova/compute/manager.py'
232--- nova/compute/manager.py 2011-01-10 17:37:06 +0000
233+++ nova/compute/manager.py 2011-01-12 03:17:16 +0000
234@@ -471,6 +471,14 @@
235 return self.driver.get_console_output(instance_ref)
236
237 @exception.wrap_exception
238+ def get_ajax_console(self, context, instance_id):
239+ """Return connection information for an ajax console"""
240+ context = context.elevated()
241+ logging.debug(_("instance %s: getting ajax console"), instance_id)
242+ instance_ref = self.db.instance_get(context, instance_id)
243+
244+ return self.driver.get_ajax_console(instance_ref)
245+
246 @checks_instance_lock
247 def attach_volume(self, context, instance_id, volume_id, mountpoint):
248 """Attach a volume to an instance."""
249
250=== modified file 'nova/flags.py'
251--- nova/flags.py 2011-01-11 06:47:35 +0000
252+++ nova/flags.py 2011-01-12 03:17:16 +0000
253@@ -234,7 +234,14 @@
254 'the topic scheduler nodes listen on')
255 DEFINE_string('volume_topic', 'volume', 'the topic volume nodes listen on')
256 DEFINE_string('network_topic', 'network', 'the topic network nodes listen on')
257-
258+DEFINE_string('ajax_console_proxy_topic', 'ajax_proxy',
259+ 'the topic ajax proxy nodes listen on')
260+DEFINE_string('ajax_console_proxy_url',
261+ 'http://127.0.0.1:8000',
262+ 'location of ajax console proxy, \
263+ in the form "http://127.0.0.1:8000"')
264+DEFINE_string('ajax_console_proxy_port',
265+ 8000, 'port that ajax_console_proxy binds')
266 DEFINE_bool('verbose', False, 'show debug output')
267 DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit')
268 DEFINE_bool('fake_network', False,
269
270=== modified file 'nova/tests/test_cloud.py'
271--- nova/tests/test_cloud.py 2011-01-10 07:01:10 +0000
272+++ nova/tests/test_cloud.py 2011-01-12 03:17:16 +0000
273@@ -167,6 +167,19 @@
274 greenthread.sleep(0.3)
275 rv = self.cloud.terminate_instances(self.context, [instance_id])
276
277+ def test_ajax_console(self):
278+ kwargs = {'image_id': image_id}
279+ rv = yield self.cloud.run_instances(self.context, **kwargs)
280+ instance_id = rv['instancesSet'][0]['instanceId']
281+ output = yield self.cloud.get_console_output(context=self.context,
282+ instance_id=[instance_id])
283+ self.assertEquals(b64decode(output['output']),
284+ 'http://fakeajaxconsole.com/?token=FAKETOKEN')
285+ # TODO(soren): We need this until we can stop polling in the rpc code
286+ # for unit tests.
287+ greenthread.sleep(0.3)
288+ rv = yield self.cloud.terminate_instances(self.context, [instance_id])
289+
290 def test_key_generation(self):
291 result = self._create_key('test')
292 private_key = result['private_key']
293
294=== modified file 'nova/tests/test_compute.py'
295--- nova/tests/test_compute.py 2011-01-07 14:46:17 +0000
296+++ nova/tests/test_compute.py 2011-01-12 03:17:16 +0000
297@@ -169,6 +169,16 @@
298 self.assert_(console)
299 self.compute.terminate_instance(self.context, instance_id)
300
301+ def test_ajax_console(self):
302+ """Make sure we can get console output from instance"""
303+ instance_id = self._create_instance()
304+ self.compute.run_instance(self.context, instance_id)
305+
306+ console = self.compute.get_ajax_console(self.context,
307+ instance_id)
308+ self.assert_(console)
309+ self.compute.terminate_instance(self.context, instance_id)
310+
311 def test_run_instance_existing(self):
312 """Ensure failure when running an instance that already exists"""
313 instance_id = self._create_instance()
314
315=== modified file 'nova/utils.py'
316--- nova/utils.py 2011-01-10 02:08:54 +0000
317+++ nova/utils.py 2011-01-12 03:17:16 +0000
318@@ -153,6 +153,11 @@
319 return os.path.join(os.path.dirname(__file__), s)
320
321
322+def novadir():
323+ import nova
324+ return os.path.abspath(nova.__file__).split('nova/__init__.pyc')[0]
325+
326+
327 def default_flagfile(filename='nova.conf'):
328 for arg in sys.argv:
329 if arg.find('flagfile') != -1:
330
331=== modified file 'nova/virt/fake.py'
332--- nova/virt/fake.py 2011-01-06 01:45:46 +0000
333+++ nova/virt/fake.py 2011-01-12 03:17:16 +0000
334@@ -289,6 +289,9 @@
335 def get_console_output(self, instance):
336 return 'FAKE CONSOLE OUTPUT'
337
338+ def get_ajax_console(self, instance):
339+ return 'http://fakeajaxconsole.com/?token=FAKETOKEN'
340+
341 def get_console_pool_info(self, console_type):
342 return {'address': '127.0.0.1',
343 'username': 'fakeuser',
344
345=== modified file 'nova/virt/libvirt.xml.template'
346--- nova/virt/libvirt.xml.template 2010-12-23 21:41:54 +0000
347+++ nova/virt/libvirt.xml.template 2011-01-12 03:17:16 +0000
348@@ -71,9 +71,22 @@
349 #end if
350 </filterref>
351 </interface>
352+
353+ <!-- The order is significant here. File must be defined first -->
354 <serial type="file">
355 <source path='${basepath}/console.log'/>
356 <target port='1'/>
357 </serial>
358+
359+ <console type='pty' tty='/dev/pts/2'>
360+ <source path='/dev/pts/2'/>
361+ <target port='0'/>
362+ </console>
363+
364+ <serial type='pty'>
365+ <source path='/dev/pts/2'/>
366+ <target port='0'/>
367+ </serial>
368+
369 </devices>
370 </domain>
371
372=== modified file 'nova/virt/libvirt_conn.py'
373--- nova/virt/libvirt_conn.py 2011-01-11 19:49:18 +0000
374+++ nova/virt/libvirt_conn.py 2011-01-12 03:17:16 +0000
375@@ -38,6 +38,11 @@
376
377 import os
378 import shutil
379+import random
380+import subprocess
381+import uuid
382+from xml.dom import minidom
383+
384
385 from eventlet import greenthread
386 from eventlet import event
387@@ -86,6 +91,9 @@
388 flags.DEFINE_bool('allow_project_net_traffic',
389 True,
390 'Whether to allow in project network traffic')
391+flags.DEFINE_string('ajaxterm_portrange',
392+ '10000-12000',
393+ 'Range of ports that ajaxterm should randomly try to bind')
394 flags.DEFINE_string('firewall_driver',
395 'nova.virt.libvirt_conn.IptablesFirewallDriver',
396 'Firewall driver (defaults to iptables)')
397@@ -433,6 +441,43 @@
398
399 return self._dump_file(fpath)
400
401+ @exception.wrap_exception
402+ def get_ajax_console(self, instance):
403+ def get_open_port():
404+ start_port, end_port = FLAGS.ajaxterm_portrange.split("-")
405+ for i in xrange(0, 100): # don't loop forever
406+ port = random.randint(int(start_port), int(end_port))
407+ # netcat will exit with 0 only if the port is in use,
408+ # so a nonzero return value implies it is unused
409+ cmd = 'netcat 0.0.0.0 %s -w 1 </dev/null || echo free' % (port)
410+ stdout, stderr = utils.execute(cmd)
411+ if stdout.strip() == 'free':
412+ return port
413+ raise Exception(_('Unable to find an open port'))
414+
415+ def get_pty_for_instance(instance_name):
416+ virt_dom = self._conn.lookupByName(instance_name)
417+ xml = virt_dom.XMLDesc(0)
418+ dom = minidom.parseString(xml)
419+
420+ for serial in dom.getElementsByTagName('serial'):
421+ if serial.getAttribute('type') == 'pty':
422+ source = serial.getElementsByTagName('source')[0]
423+ return source.getAttribute('path')
424+
425+ port = get_open_port()
426+ token = str(uuid.uuid4())
427+ host = instance['host']
428+
429+ ajaxterm_cmd = 'sudo socat - %s' \
430+ % get_pty_for_instance(instance['name'])
431+
432+ cmd = '%s/tools/ajaxterm/ajaxterm.py --command "%s" -t %s -p %s' \
433+ % (utils.novadir(), ajaxterm_cmd, token, port)
434+
435+ subprocess.Popen(cmd, shell=True)
436+ return {'token': token, 'host': host, 'port': port}
437+
438 def _create_image(self, inst, libvirt_xml, prefix='', disk_images=None):
439 # syntactic nicety
440 basepath = lambda fname = '', prefix = prefix: os.path.join(
441
442=== modified file 'nova/virt/xenapi/vmops.py'
443--- nova/virt/xenapi/vmops.py 2011-01-11 06:47:35 +0000
444+++ nova/virt/xenapi/vmops.py 2011-01-12 03:17:16 +0000
445@@ -284,6 +284,11 @@
446 # TODO: implement this to fix pylint!
447 return 'FAKE CONSOLE OUTPUT of instance'
448
449+ def get_ajax_console(self, instance):
450+ """Return link to instance's ajax console"""
451+ # TODO: implement this!
452+ return 'http://fakeajaxconsole/fake_url'
453+
454 def list_from_xenstore(self, vm, path):
455 """Runs the xenstore-ls command to get a listing of all records
456 from 'path' downward. Returns a dict with the sub-paths as keys,
457
458=== modified file 'nova/virt/xenapi_conn.py'
459--- nova/virt/xenapi_conn.py 2011-01-11 12:24:58 +0000
460+++ nova/virt/xenapi_conn.py 2011-01-12 03:17:16 +0000
461@@ -181,6 +181,10 @@
462 """Return snapshot of console"""
463 return self._vmops.get_console_output(instance)
464
465+ def get_ajax_console(self, instance):
466+ """Return link to instance's ajax console"""
467+ return self._vmops.get_ajax_console(instance)
468+
469 def attach_volume(self, instance_name, device_path, mountpoint):
470 """Attach volume storage to VM instance"""
471 return self._volumeops.attach_volume(instance_name,
472
473=== added directory 'tools/ajaxterm'
474=== added file 'tools/ajaxterm/README.txt'
475--- tools/ajaxterm/README.txt 1970-01-01 00:00:00 +0000
476+++ tools/ajaxterm/README.txt 2011-01-12 03:17:16 +0000
477@@ -0,0 +1,120 @@
478+= [http://antony.lesuisse.org/qweb/trac/wiki/AjaxTerm Ajaxterm] =
479+
480+Ajaxterm is a web based terminal. It was totally inspired and works almost
481+exactly like http://anyterm.org/ except it's much easier to install (see
482+comparaison with anyterm below).
483+
484+Ajaxterm written in python (and some AJAX javascript for client side) and depends only on python2.3 or better.[[BR]]
485+Ajaxterm is '''very simple to install''' on Linux, MacOS X, FreeBSD, Solaris, cygwin and any Unix that runs python2.3.[[BR]]
486+Ajaxterm was written by Antony Lesuisse (email: al AT udev.org), License Public Domain.
487+
488+Use the [/qweb/forum/viewforum.php?id=2 Forum], if you have any question or remark.
489+
490+== News ==
491+
492+ * 2006-10-29: v0.10 allow space in login, cgi launch fix, redhat init
493+ * 2006-07-12: v0.9 change uid, daemon fix (Daniel Fischer)
494+ * 2006-07-04: v0.8 add login support to ssh (Sven Geggus), change max width to 256
495+ * 2006-05-31: v0.7 minor fixes, daemon option
496+ * 2006-05-23: v0.6 Applied debian and gentoo patches, renamed to Ajaxterm, default port 8022
497+
498+== Download and Install ==
499+
500+ * Release: [/qweb/files/Ajaxterm-0.10.tar.gz Ajaxterm-0.10.tar.gz]
501+ * Browse src: [/qweb/trac/browser/trunk/ajaxterm/ ajaxterm/]
502+
503+To install Ajaxterm issue the following commands:
504+{{{
505+wget http://antony.lesuisse.org/qweb/files/Ajaxterm-0.10.tar.gz
506+tar zxvf Ajaxterm-0.10.tar.gz
507+cd Ajaxterm-0.10
508+./ajaxterm.py
509+}}}
510+Then point your browser to this URL : http://localhost:8022/
511+
512+== Screenshot ==
513+
514+{{{
515+#!html
516+<center><img src="/qweb/trac/attachment/wiki/AjaxTerm/scr.png?format=raw" alt="ajaxterm screenshot" style=""/></center>
517+}}}
518+
519+== Documentation and Caveats ==
520+
521+ * Ajaxterm only support latin1, if you use Ubuntu or any LANG==en_US.UTF-8 distribution don't forget to "unset LANG".
522+
523+ * If run as root ajaxterm will run /bin/login, otherwise it will run ssh
524+ localhost. To use an other command use the -c option.
525+
526+ * By default Ajaxterm only listen at 127.0.0.1:8022. For remote access, it is
527+ strongly recommended to use '''https SSL/TLS''', and that is simple to
528+ configure if you use the apache web server using mod_proxy.[[BR]][[BR]]
529+ Using ssl will also speed up ajaxterm (probably because of keepalive).[[BR]][[BR]]
530+ Here is an configuration example:
531+
532+{{{
533+ Listen 443
534+ NameVirtualHost *:443
535+
536+ <VirtualHost *:443>
537+ ServerName localhost
538+ SSLEngine On
539+ SSLCertificateKeyFile ssl/apache.pem
540+ SSLCertificateFile ssl/apache.pem
541+
542+ ProxyRequests Off
543+ <Proxy *>
544+ Order deny,allow
545+ Allow from all
546+ </Proxy>
547+ ProxyPass /ajaxterm/ http://localhost:8022/
548+ ProxyPassReverse /ajaxterm/ http://localhost:8022/
549+ </VirtualHost>
550+}}}
551+
552+ * Using GET HTTP request seems to speed up ajaxterm, just click on GET in the
553+ interface, but be warned that your keystrokes might be loggued (by apache or
554+ any proxy). I usually enable it after the login.
555+
556+ * Ajaxterm commandline usage:
557+
558+{{{
559+usage: ajaxterm.py [options]
560+
561+options:
562+ -h, --help show this help message and exit
563+ -pPORT, --port=PORT Set the TCP port (default: 8022)
564+ -cCMD, --command=CMD set the command (default: /bin/login or ssh localhost)
565+ -l, --log log requests to stderr (default: quiet mode)
566+ -d, --daemon run as daemon in the background
567+ -PPIDFILE, --pidfile=PIDFILE
568+ set the pidfile (default: /var/run/ajaxterm.pid)
569+ -iINDEX_FILE, --index=INDEX_FILE
570+ default index file (default: ajaxterm.html)
571+ -uUID, --uid=UID Set the daemon's user id
572+}}}
573+
574+ * Ajaxterm was first written as a demo for qweb (my web framework), but
575+ actually doesn't use many features of qweb.
576+
577+ * Compared to anyterm:
578+ * There are no partial updates, ajaxterm updates either all the screen or
579+ nothing. That make the code simpler and I also think it's faster. HTTP
580+ replies are always gzencoded. When used in 80x25 mode, almost all of
581+ them are below the 1500 bytes (size of an ethernet frame) and we just
582+ replace the screen with the reply (no javascript string handling).
583+ * Ajaxterm polls the server for updates with an exponentially growing
584+ timeout when the screen hasn't changed. The timeout is also resetted as
585+ soon as a key is pressed. Anyterm blocks on a pending request and use a
586+ parallel connection for keypresses. The anyterm approch is better
587+ when there aren't any keypress.
588+
589+ * Ajaxterm files are released in the Public Domain, (except [http://sarissa.sourceforge.net/doc/ sarissa*] which are LGPL).
590+
591+== TODO ==
592+
593+ * insert mode ESC [ 4 h
594+ * change size x,y from gui (sending signal)
595+ * vt102 graphic codepage
596+ * use innerHTML or prototype instead of sarissa
597+
598
599=== added file 'tools/ajaxterm/ajaxterm.1'
600--- tools/ajaxterm/ajaxterm.1 1970-01-01 00:00:00 +0000
601+++ tools/ajaxterm/ajaxterm.1 2011-01-12 03:17:16 +0000
602@@ -0,0 +1,35 @@
603+.TH ajaxterm "1" "May 2006" "ajaxterm 0.5" "User commands"
604+.SH NAME
605+ajaxterm \- Web based terminal written in python
606+
607+.SH DESCRITPION
608+\fBajaxterm\fR is a web based terminal written in python and some AJAX
609+javascript for client side.
610+It can use almost any web browser and even works through firewalls.
611+
612+.SH USAGE
613+\fBajaxterm.py\fR [options]
614+
615+.SH OPTIONS
616+A summary of the options supported by \fBajaxterm\fR is included below.
617+ \fB-h, --help\fR show this help message and exit
618+ \fB-pPORT, --port=PORT\fR Set the TCP port (default: 8022)
619+ \fB-cCMD, --command=CMD\fR set the command (default: /bin/login or ssh localhost)
620+ \fB-l, --log\fR log requests to stderr (default: quiet mode)
621+
622+.SH AUTHOR
623+Antony Lesuisse <al@udev.org>
624+
625+This manual page was written for the Debian system by
626+Julien Valroff <julien@kirya.net> (but may be used by others).
627+
628+.SH "REPORTING BUGS"
629+Report any bugs to the author: Antony Lesuisse <al@udev.org>
630+
631+.SH COPYRIGHT
632+Copyright Antony Lesuisse <al@udev.org>
633+
634+.SH SEE ALSO
635+- \fBajaxterm\fR wiki page: http://antony.lesuisse.org/qweb/trac/wiki/AjaxTerm
636+.br
637+- \fBajaxterm\fR forum: http://antony.lesuisse.org/qweb/forum/viewforum.php?id=2
638
639=== added file 'tools/ajaxterm/ajaxterm.css'
640--- tools/ajaxterm/ajaxterm.css 1970-01-01 00:00:00 +0000
641+++ tools/ajaxterm/ajaxterm.css 2011-01-12 03:17:16 +0000
642@@ -0,0 +1,64 @@
643+pre.stat {
644+ margin: 0px;
645+ padding: 4px;
646+ display: block;
647+ font-family: monospace;
648+ white-space: pre;
649+ background-color: black;
650+ border-top: 1px solid black;
651+ color: white;
652+}
653+pre.stat span {
654+ padding: 0px;
655+}
656+pre.stat .on {
657+ background-color: #080;
658+ font-weight: bold;
659+ color: white;
660+ cursor: pointer;
661+}
662+pre.stat .off {
663+ background-color: #888;
664+ font-weight: bold;
665+ color: white;
666+ cursor: pointer;
667+}
668+pre.term {
669+ margin: 0px;
670+ padding: 4px;
671+ display: block;
672+ font-family: monospace;
673+ white-space: pre;
674+ background-color: black;
675+ border-top: 1px solid white;
676+ color: #eee;
677+}
678+pre.term span.f0 { color: #000; }
679+pre.term span.f1 { color: #b00; }
680+pre.term span.f2 { color: #0b0; }
681+pre.term span.f3 { color: #bb0; }
682+pre.term span.f4 { color: #00b; }
683+pre.term span.f5 { color: #b0b; }
684+pre.term span.f6 { color: #0bb; }
685+pre.term span.f7 { color: #bbb; }
686+pre.term span.f8 { color: #666; }
687+pre.term span.f9 { color: #f00; }
688+pre.term span.f10 { color: #0f0; }
689+pre.term span.f11 { color: #ff0; }
690+pre.term span.f12 { color: #00f; }
691+pre.term span.f13 { color: #f0f; }
692+pre.term span.f14 { color: #0ff; }
693+pre.term span.f15 { color: #fff; }
694+pre.term span.b0 { background-color: #000; }
695+pre.term span.b1 { background-color: #b00; }
696+pre.term span.b2 { background-color: #0b0; }
697+pre.term span.b3 { background-color: #bb0; }
698+pre.term span.b4 { background-color: #00b; }
699+pre.term span.b5 { background-color: #b0b; }
700+pre.term span.b6 { background-color: #0bb; }
701+pre.term span.b7 { background-color: #bbb; }
702+
703+body { background-color: #888; }
704+#term {
705+ float: left;
706+}
707
708=== added file 'tools/ajaxterm/ajaxterm.html'
709--- tools/ajaxterm/ajaxterm.html 1970-01-01 00:00:00 +0000
710+++ tools/ajaxterm/ajaxterm.html 2011-01-12 03:17:16 +0000
711@@ -0,0 +1,25 @@
712+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
713+<html>
714+<head>
715+ <title>Ajaxterm</title>
716+ <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
717+ <link rel="stylesheet" type="text/css" href="ajaxterm.css"/>
718+ <script type="text/javascript" src="sarissa.js"></script>
719+ <script type="text/javascript" src="sarissa_dhtml.js"></script>
720+ <script type="text/javascript" src="ajaxterm.js"></script>
721+ <script type="text/javascript">
722+ /*
723+ ajaxterm.py creates a random session_id to demultiplex multiple connections,
724+ and to add a layer of security - in its shipping form, ajaxterm accepted any session_id
725+ and was susceptible to an easy exploit
726+ */
727+ SESSION_ID = '$session_id';
728+ window.onload=function() {
729+ t=ajaxterm.Terminal("term",80,25);
730+ };
731+ </script>
732+</head>
733+<body>
734+<div id="term"></div>
735+</body>
736+</html>
737
738=== added file 'tools/ajaxterm/ajaxterm.js'
739--- tools/ajaxterm/ajaxterm.js 1970-01-01 00:00:00 +0000
740+++ tools/ajaxterm/ajaxterm.js 2011-01-12 03:17:16 +0000
741@@ -0,0 +1,279 @@
742+ajaxterm={};
743+ajaxterm.Terminal_ctor=function(id,width,height) {
744+ var ie=0;
745+ if(window.ActiveXObject)
746+ ie=1;
747+ var sid=""+SESSION_ID;
748+ var query0="s="+sid+"&w="+width+"&h="+height;
749+ var query1=query0+"&c=1&k=";
750+ var buf="";
751+ var timeout;
752+ var error_timeout;
753+ var keybuf=[];
754+ var sending=0;
755+ var rmax=1;
756+
757+ var div=document.getElementById(id);
758+ var dstat=document.createElement('pre');
759+ var sled=document.createElement('span');
760+ var opt_get=document.createElement('a');
761+ var opt_color=document.createElement('a');
762+ var opt_paste=document.createElement('a');
763+ var sdebug=document.createElement('span');
764+ var dterm=document.createElement('div');
765+
766+ function debug(s) {
767+ sdebug.innerHTML=s;
768+ }
769+ function error() {
770+ sled.className='off';
771+ debug("Connection lost timeout ts:"+((new Date).getTime()));
772+ }
773+ function opt_add(opt,name) {
774+ opt.className='off';
775+ opt.innerHTML=' '+name+' ';
776+ dstat.appendChild(opt);
777+ dstat.appendChild(document.createTextNode(' '));
778+ }
779+ function do_get(event) {
780+ opt_get.className=(opt_get.className=='off')?'on':'off';
781+ debug('GET '+opt_get.className);
782+ }
783+ function do_color(event) {
784+ var o=opt_color.className=(opt_color.className=='off')?'on':'off';
785+ if(o=='on')
786+ query1=query0+"&c=1&k=";
787+ else
788+ query1=query0+"&k=";
789+ debug('Color '+opt_color.className);
790+ }
791+ function mozilla_clipboard() {
792+ // mozilla sucks
793+ try {
794+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
795+ } catch (err) {
796+ debug('Access denied, <a href="http://kb.mozillazine.org/Granting_JavaScript_access_to_the_clipboard" target="_blank">more info</a>');
797+ return undefined;
798+ }
799+ var clip = Components.classes["@mozilla.org/widget/clipboard;1"].createInstance(Components.interfaces.nsIClipboard);
800+ var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
801+ if (!clip || !trans) {
802+ return undefined;
803+ }
804+ trans.addDataFlavor("text/unicode");
805+ clip.getData(trans,clip.kGlobalClipboard);
806+ var str=new Object();
807+ var strLength=new Object();
808+ try {
809+ trans.getTransferData("text/unicode",str,strLength);
810+ } catch(err) {
811+ return "";
812+ }
813+ if (str) {
814+ str=str.value.QueryInterface(Components.interfaces.nsISupportsString);
815+ }
816+ if (str) {
817+ return str.data.substring(0,strLength.value / 2);
818+ } else {
819+ return "";
820+ }
821+ }
822+ function do_paste(event) {
823+ var p=undefined;
824+ if (window.clipboardData) {
825+ p=window.clipboardData.getData("Text");
826+ } else if(window.netscape) {
827+ p=mozilla_clipboard();
828+ }
829+ if (p) {
830+ debug('Pasted');
831+ queue(encodeURIComponent(p));
832+ } else {
833+ }
834+ }
835+ function update() {
836+// debug("ts: "+((new Date).getTime())+" rmax:"+rmax);
837+ if(sending==0) {
838+ sending=1;
839+ sled.className='on';
840+ var r=new XMLHttpRequest();
841+ var send="";
842+ while(keybuf.length>0) {
843+ send+=keybuf.pop();
844+ }
845+ var query=query1+send;
846+ if(opt_get.className=='on') {
847+ r.open("GET","u?"+query,true);
848+ if(ie) {
849+ r.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
850+ }
851+ } else {
852+ r.open("POST","u",true);
853+ }
854+ r.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
855+ r.onreadystatechange = function () {
856+// debug("xhr:"+((new Date).getTime())+" state:"+r.readyState+" status:"+r.status+" statusText:"+r.statusText);
857+ if (r.readyState==4) {
858+ if(r.status==200) {
859+ window.clearTimeout(error_timeout);
860+ de=r.responseXML.documentElement;
861+ if(de.tagName=="pre") {
862+ if(ie) {
863+ Sarissa.updateContentFromNode(de, dterm);
864+ } else {
865+ Sarissa.updateContentFromNode(de, dterm);
866+// old=div.firstChild;
867+// div.replaceChild(de,old);
868+ }
869+ rmax=100;
870+ } else {
871+ rmax*=2;
872+ if(rmax>2000)
873+ rmax=2000;
874+ }
875+ sending=0;
876+ sled.className='off';
877+ timeout=window.setTimeout(update,rmax);
878+ } else {
879+ debug("Connection error status:"+r.status);
880+ }
881+ }
882+ }
883+ error_timeout=window.setTimeout(error,5000);
884+ if(opt_get.className=='on') {
885+ r.send(null);
886+ } else {
887+ r.send(query);
888+ }
889+ }
890+ }
891+ function queue(s) {
892+ keybuf.unshift(s);
893+ if(sending==0) {
894+ window.clearTimeout(timeout);
895+ timeout=window.setTimeout(update,1);
896+ }
897+ }
898+ function keypress(ev) {
899+ if (!ev) var ev=window.event;
900+// s="kp keyCode="+ev.keyCode+" which="+ev.which+" shiftKey="+ev.shiftKey+" ctrlKey="+ev.ctrlKey+" altKey="+ev.altKey;
901+// debug(s);
902+// return false;
903+// else { if (!ev.ctrlKey || ev.keyCode==17) { return; }
904+ var kc;
905+ var k="";
906+ if (ev.keyCode)
907+ kc=ev.keyCode;
908+ if (ev.which)
909+ kc=ev.which;
910+ if (ev.altKey) {
911+ if (kc>=65 && kc<=90)
912+ kc+=32;
913+ if (kc>=97 && kc<=122) {
914+ k=String.fromCharCode(27)+String.fromCharCode(kc);
915+ }
916+ } else if (ev.ctrlKey) {
917+ if (kc>=65 && kc<=90) k=String.fromCharCode(kc-64); // Ctrl-A..Z
918+ else if (kc>=97 && kc<=122) k=String.fromCharCode(kc-96); // Ctrl-A..Z
919+ else if (kc==54) k=String.fromCharCode(30); // Ctrl-^
920+ else if (kc==109) k=String.fromCharCode(31); // Ctrl-_
921+ else if (kc==219) k=String.fromCharCode(27); // Ctrl-[
922+ else if (kc==220) k=String.fromCharCode(28); // Ctrl-\
923+ else if (kc==221) k=String.fromCharCode(29); // Ctrl-]
924+ else if (kc==219) k=String.fromCharCode(29); // Ctrl-]
925+ else if (kc==219) k=String.fromCharCode(0); // Ctrl-@
926+ } else if (ev.which==0) {
927+ if (kc==9) k=String.fromCharCode(9); // Tab
928+ else if (kc==8) k=String.fromCharCode(127); // Backspace
929+ else if (kc==27) k=String.fromCharCode(27); // Escape
930+ else {
931+ if (kc==33) k="[5~"; // PgUp
932+ else if (kc==34) k="[6~"; // PgDn
933+ else if (kc==35) k="[4~"; // End
934+ else if (kc==36) k="[1~"; // Home
935+ else if (kc==37) k="[D"; // Left
936+ else if (kc==38) k="[A"; // Up
937+ else if (kc==39) k="[C"; // Right
938+ else if (kc==40) k="[B"; // Down
939+ else if (kc==45) k="[2~"; // Ins
940+ else if (kc==46) k="[3~"; // Del
941+ else if (kc==112) k="[[A"; // F1
942+ else if (kc==113) k="[[B"; // F2
943+ else if (kc==114) k="[[C"; // F3
944+ else if (kc==115) k="[[D"; // F4
945+ else if (kc==116) k="[[E"; // F5
946+ else if (kc==117) k="[17~"; // F6
947+ else if (kc==118) k="[18~"; // F7
948+ else if (kc==119) k="[19~"; // F8
949+ else if (kc==120) k="[20~"; // F9
950+ else if (kc==121) k="[21~"; // F10
951+ else if (kc==122) k="[23~"; // F11
952+ else if (kc==123) k="[24~"; // F12
953+ if (k.length) {
954+ k=String.fromCharCode(27)+k;
955+ }
956+ }
957+ } else {
958+ if (kc==8)
959+ k=String.fromCharCode(127); // Backspace
960+ else
961+ k=String.fromCharCode(kc);
962+ }
963+ if(k.length) {
964+// queue(encodeURIComponent(k));
965+ if(k=="+") {
966+ queue("%2B");
967+ } else {
968+ queue(escape(k));
969+ }
970+ }
971+ ev.cancelBubble=true;
972+ if (ev.stopPropagation) ev.stopPropagation();
973+ if (ev.preventDefault) ev.preventDefault();
974+ return false;
975+ }
976+ function keydown(ev) {
977+ if (!ev) var ev=window.event;
978+ if (ie) {
979+// s="kd keyCode="+ev.keyCode+" which="+ev.which+" shiftKey="+ev.shiftKey+" ctrlKey="+ev.ctrlKey+" altKey="+ev.altKey;
980+// debug(s);
981+ 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,
982+ 113:1,114:1,115:1,116:1,117:1,118:1,119:1,120:1,121:1,122:1,123:1};
983+ if (o[ev.keyCode] || ev.ctrlKey || ev.altKey) {
984+ ev.which=0;
985+ return keypress(ev);
986+ }
987+ }
988+ }
989+ function init() {
990+ sled.appendChild(document.createTextNode('\xb7'));
991+ sled.className='off';
992+ dstat.appendChild(sled);
993+ dstat.appendChild(document.createTextNode(' '));
994+ opt_add(opt_color,'Colors');
995+ opt_color.className='on';
996+ opt_add(opt_get,'GET');
997+ opt_add(opt_paste,'Paste');
998+ dstat.appendChild(sdebug);
999+ dstat.className='stat';
1000+ div.appendChild(dstat);
1001+ div.appendChild(dterm);
1002+ if(opt_color.addEventListener) {
1003+ opt_get.addEventListener('click',do_get,true);
1004+ opt_color.addEventListener('click',do_color,true);
1005+ opt_paste.addEventListener('click',do_paste,true);
1006+ } else {
1007+ opt_get.attachEvent("onclick", do_get);
1008+ opt_color.attachEvent("onclick", do_color);
1009+ opt_paste.attachEvent("onclick", do_paste);
1010+ }
1011+ document.onkeypress=keypress;
1012+ document.onkeydown=keydown;
1013+ timeout=window.setTimeout(update,100);
1014+ }
1015+ init();
1016+}
1017+ajaxterm.Terminal=function(id,width,height) {
1018+ return new this.Terminal_ctor(id,width,height);
1019+}
1020+
1021
1022=== added file 'tools/ajaxterm/ajaxterm.py'
1023--- tools/ajaxterm/ajaxterm.py 1970-01-01 00:00:00 +0000
1024+++ tools/ajaxterm/ajaxterm.py 2011-01-12 03:17:16 +0000
1025@@ -0,0 +1,586 @@
1026+#!/usr/bin/env python
1027+
1028+""" Ajaxterm """
1029+
1030+import array,cgi,fcntl,glob,mimetypes,optparse,os,pty,random,re,signal,select,sys,threading,time,termios,struct,pwd
1031+
1032+os.chdir(os.path.normpath(os.path.dirname(__file__)))
1033+# Optional: Add QWeb in sys path
1034+sys.path[0:0]=glob.glob('../../python')
1035+
1036+import qweb
1037+import string, subprocess, uuid
1038+
1039+global g_server
1040+TIMEOUT=300
1041+
1042+class Terminal:
1043+ def __init__(self,width=80,height=24):
1044+ self.width=width
1045+ self.height=height
1046+ self.init()
1047+ self.reset()
1048+ def init(self):
1049+ self.esc_seq={
1050+ "\x00": None,
1051+ "\x05": self.esc_da,
1052+ "\x07": None,
1053+ "\x08": self.esc_0x08,
1054+ "\x09": self.esc_0x09,
1055+ "\x0a": self.esc_0x0a,
1056+ "\x0b": self.esc_0x0a,
1057+ "\x0c": self.esc_0x0a,
1058+ "\x0d": self.esc_0x0d,
1059+ "\x0e": None,
1060+ "\x0f": None,
1061+ "\x1b#8": None,
1062+ "\x1b=": None,
1063+ "\x1b>": None,
1064+ "\x1b(0": None,
1065+ "\x1b(A": None,
1066+ "\x1b(B": None,
1067+ "\x1b[c": self.esc_da,
1068+ "\x1b[0c": self.esc_da,
1069+ "\x1b]R": None,
1070+ "\x1b7": self.esc_save,
1071+ "\x1b8": self.esc_restore,
1072+ "\x1bD": None,
1073+ "\x1bE": None,
1074+ "\x1bH": None,
1075+ "\x1bM": self.esc_ri,
1076+ "\x1bN": None,
1077+ "\x1bO": None,
1078+ "\x1bZ": self.esc_da,
1079+ "\x1ba": None,
1080+ "\x1bc": self.reset,
1081+ "\x1bn": None,
1082+ "\x1bo": None,
1083+ }
1084+ for k,v in self.esc_seq.items():
1085+ if v==None:
1086+ self.esc_seq[k]=self.esc_ignore
1087+ # regex
1088+ d={
1089+ r'\[\??([0-9;]*)([@ABCDEFGHJKLMPXacdefghlmnqrstu`])' : self.csi_dispatch,
1090+ r'\]([^\x07]+)\x07' : self.esc_ignore,
1091+ }
1092+ self.esc_re=[]
1093+ for k,v in d.items():
1094+ self.esc_re.append((re.compile('\x1b'+k),v))
1095+ # define csi sequences
1096+ self.csi_seq={
1097+ '@': (self.csi_at,[1]),
1098+ '`': (self.csi_G,[1]),
1099+ 'J': (self.csi_J,[0]),
1100+ 'K': (self.csi_K,[0]),
1101+ }
1102+ for i in [i[4] for i in dir(self) if i.startswith('csi_') and len(i)==5]:
1103+ if not self.csi_seq.has_key(i):
1104+ self.csi_seq[i]=(getattr(self,'csi_'+i),[1])
1105+ # Init 0-256 to latin1 and html translation table
1106+ self.trl1=""
1107+ for i in range(256):
1108+ if i<32:
1109+ self.trl1+=" "
1110+ elif i<127 or i>160:
1111+ self.trl1+=chr(i)
1112+ else:
1113+ self.trl1+="?"
1114+ self.trhtml=""
1115+ for i in range(256):
1116+ if i==0x0a or (i>32 and i<127) or i>160:
1117+ self.trhtml+=chr(i)
1118+ elif i<=32:
1119+ self.trhtml+="\xa0"
1120+ else:
1121+ self.trhtml+="?"
1122+ def reset(self,s=""):
1123+ self.scr=array.array('i',[0x000700]*(self.width*self.height))
1124+ self.st=0
1125+ self.sb=self.height-1
1126+ self.cx_bak=self.cx=0
1127+ self.cy_bak=self.cy=0
1128+ self.cl=0
1129+ self.sgr=0x000700
1130+ self.buf=""
1131+ self.outbuf=""
1132+ self.last_html=""
1133+ def peek(self,y1,x1,y2,x2):
1134+ return self.scr[self.width*y1+x1:self.width*y2+x2]
1135+ def poke(self,y,x,s):
1136+ pos=self.width*y+x
1137+ self.scr[pos:pos+len(s)]=s
1138+ def zero(self,y1,x1,y2,x2):
1139+ w=self.width*(y2-y1)+x2-x1+1
1140+ z=array.array('i',[0x000700]*w)
1141+ self.scr[self.width*y1+x1:self.width*y2+x2+1]=z
1142+ def scroll_up(self,y1,y2):
1143+ self.poke(y1,0,self.peek(y1+1,0,y2,self.width))
1144+ self.zero(y2,0,y2,self.width-1)
1145+ def scroll_down(self,y1,y2):
1146+ self.poke(y1+1,0,self.peek(y1,0,y2-1,self.width))
1147+ self.zero(y1,0,y1,self.width-1)
1148+ def scroll_right(self,y,x):
1149+ self.poke(y,x+1,self.peek(y,x,y,self.width))
1150+ self.zero(y,x,y,x)
1151+ def cursor_down(self):
1152+ if self.cy>=self.st and self.cy<=self.sb:
1153+ self.cl=0
1154+ q,r=divmod(self.cy+1,self.sb+1)
1155+ if q:
1156+ self.scroll_up(self.st,self.sb)
1157+ self.cy=self.sb
1158+ else:
1159+ self.cy=r
1160+ def cursor_right(self):
1161+ q,r=divmod(self.cx+1,self.width)
1162+ if q:
1163+ self.cl=1
1164+ else:
1165+ self.cx=r
1166+ def echo(self,c):
1167+ if self.cl:
1168+ self.cursor_down()
1169+ self.cx=0
1170+ self.scr[(self.cy*self.width)+self.cx]=self.sgr|ord(c)
1171+ self.cursor_right()
1172+ def esc_0x08(self,s):
1173+ self.cx=max(0,self.cx-1)
1174+ def esc_0x09(self,s):
1175+ x=self.cx+8
1176+ q,r=divmod(x,8)
1177+ self.cx=(q*8)%self.width
1178+ def esc_0x0a(self,s):
1179+ self.cursor_down()
1180+ def esc_0x0d(self,s):
1181+ self.cl=0
1182+ self.cx=0
1183+ def esc_save(self,s):
1184+ self.cx_bak=self.cx
1185+ self.cy_bak=self.cy
1186+ def esc_restore(self,s):
1187+ self.cx=self.cx_bak
1188+ self.cy=self.cy_bak
1189+ self.cl=0
1190+ def esc_da(self,s):
1191+ self.outbuf="\x1b[?6c"
1192+ def esc_ri(self,s):
1193+ self.cy=max(self.st,self.cy-1)
1194+ if self.cy==self.st:
1195+ self.scroll_down(self.st,self.sb)
1196+ def esc_ignore(self,*s):
1197+ pass
1198+# print "term:ignore: %s"%repr(s)
1199+ def csi_dispatch(self,seq,mo):
1200+ # CSI sequences
1201+ s=mo.group(1)
1202+ c=mo.group(2)
1203+ f=self.csi_seq.get(c,None)
1204+ if f:
1205+ try:
1206+ l=[min(int(i),1024) for i in s.split(';') if len(i)<4]
1207+ except ValueError:
1208+ l=[]
1209+ if len(l)==0:
1210+ l=f[1]
1211+ f[0](l)
1212+# else:
1213+# print 'csi ignore',c,l
1214+ def csi_at(self,l):
1215+ for i in range(l[0]):
1216+ self.scroll_right(self.cy,self.cx)
1217+ def csi_A(self,l):
1218+ self.cy=max(self.st,self.cy-l[0])
1219+ def csi_B(self,l):
1220+ self.cy=min(self.sb,self.cy+l[0])
1221+ def csi_C(self,l):
1222+ self.cx=min(self.width-1,self.cx+l[0])
1223+ self.cl=0
1224+ def csi_D(self,l):
1225+ self.cx=max(0,self.cx-l[0])
1226+ self.cl=0
1227+ def csi_E(self,l):
1228+ self.csi_B(l)
1229+ self.cx=0
1230+ self.cl=0
1231+ def csi_F(self,l):
1232+ self.csi_A(l)
1233+ self.cx=0
1234+ self.cl=0
1235+ def csi_G(self,l):
1236+ self.cx=min(self.width,l[0])-1
1237+ def csi_H(self,l):
1238+ if len(l)<2: l=[1,1]
1239+ self.cx=min(self.width,l[1])-1
1240+ self.cy=min(self.height,l[0])-1
1241+ self.cl=0
1242+ def csi_J(self,l):
1243+ if l[0]==0:
1244+ self.zero(self.cy,self.cx,self.height-1,self.width-1)
1245+ elif l[0]==1:
1246+ self.zero(0,0,self.cy,self.cx)
1247+ elif l[0]==2:
1248+ self.zero(0,0,self.height-1,self.width-1)
1249+ def csi_K(self,l):
1250+ if l[0]==0:
1251+ self.zero(self.cy,self.cx,self.cy,self.width-1)
1252+ elif l[0]==1:
1253+ self.zero(self.cy,0,self.cy,self.cx)
1254+ elif l[0]==2:
1255+ self.zero(self.cy,0,self.cy,self.width-1)
1256+ def csi_L(self,l):
1257+ for i in range(l[0]):
1258+ if self.cy<self.sb:
1259+ self.scroll_down(self.cy,self.sb)
1260+ def csi_M(self,l):
1261+ if self.cy>=self.st and self.cy<=self.sb:
1262+ for i in range(l[0]):
1263+ self.scroll_up(self.cy,self.sb)
1264+ def csi_P(self,l):
1265+ w,cx,cy=self.width,self.cx,self.cy
1266+ end=self.peek(cy,cx,cy,w)
1267+ self.csi_K([0])
1268+ self.poke(cy,cx,end[l[0]:])
1269+ def csi_X(self,l):
1270+ self.zero(self.cy,self.cx,self.cy,self.cx+l[0])
1271+ def csi_a(self,l):
1272+ self.csi_C(l)
1273+ def csi_c(self,l):
1274+ #'\x1b[?0c' 0-8 cursor size
1275+ pass
1276+ def csi_d(self,l):
1277+ self.cy=min(self.height,l[0])-1
1278+ def csi_e(self,l):
1279+ self.csi_B(l)
1280+ def csi_f(self,l):
1281+ self.csi_H(l)
1282+ def csi_h(self,l):
1283+ if l[0]==4:
1284+ pass
1285+# print "insert on"
1286+ def csi_l(self,l):
1287+ if l[0]==4:
1288+ pass
1289+# print "insert off"
1290+ def csi_m(self,l):
1291+ for i in l:
1292+ if i==0 or i==39 or i==49 or i==27:
1293+ self.sgr=0x000700
1294+ elif i==1:
1295+ self.sgr=(self.sgr|0x000800)
1296+ elif i==7:
1297+ self.sgr=0x070000
1298+ elif i>=30 and i<=37:
1299+ c=i-30
1300+ self.sgr=(self.sgr&0xff08ff)|(c<<8)
1301+ elif i>=40 and i<=47:
1302+ c=i-40
1303+ self.sgr=(self.sgr&0x00ffff)|(c<<16)
1304+# else:
1305+# print "CSI sgr ignore",l,i
1306+# print 'sgr: %r %x'%(l,self.sgr)
1307+ def csi_r(self,l):
1308+ if len(l)<2: l=[0,self.height]
1309+ self.st=min(self.height-1,l[0]-1)
1310+ self.sb=min(self.height-1,l[1]-1)
1311+ self.sb=max(self.st,self.sb)
1312+ def csi_s(self,l):
1313+ self.esc_save(0)
1314+ def csi_u(self,l):
1315+ self.esc_restore(0)
1316+ def escape(self):
1317+ e=self.buf
1318+ if len(e)>32:
1319+# print "error %r"%e
1320+ self.buf=""
1321+ elif e in self.esc_seq:
1322+ self.esc_seq[e](e)
1323+ self.buf=""
1324+ else:
1325+ for r,f in self.esc_re:
1326+ mo=r.match(e)
1327+ if mo:
1328+ f(e,mo)
1329+ self.buf=""
1330+ break
1331+# if self.buf=='': print "ESC %r\n"%e
1332+ def write(self,s):
1333+ for i in s:
1334+ if len(self.buf) or (i in self.esc_seq):
1335+ self.buf+=i
1336+ self.escape()
1337+ elif i == '\x1b':
1338+ self.buf+=i
1339+ else:
1340+ self.echo(i)
1341+ def read(self):
1342+ b=self.outbuf
1343+ self.outbuf=""
1344+ return b
1345+ def dump(self):
1346+ r=''
1347+ for i in self.scr:
1348+ r+=chr(i&255)
1349+ return r
1350+ def dumplatin1(self):
1351+ return self.dump().translate(self.trl1)
1352+ def dumphtml(self,color=1):
1353+ h=self.height
1354+ w=self.width
1355+ r=""
1356+ span=""
1357+ span_bg,span_fg=-1,-1
1358+ for i in range(h*w):
1359+ q,c=divmod(self.scr[i],256)
1360+ if color:
1361+ bg,fg=divmod(q,256)
1362+ else:
1363+ bg,fg=0,7
1364+ if i==self.cy*w+self.cx:
1365+ bg,fg=1,7
1366+ if (bg!=span_bg or fg!=span_fg or i==h*w-1):
1367+ if len(span):
1368+ r+='<span class="f%d b%d">%s</span>'%(span_fg,span_bg,cgi.escape(span.translate(self.trhtml)))
1369+ span=""
1370+ span_bg,span_fg=bg,fg
1371+ span+=chr(c)
1372+ if i%w==w-1:
1373+ span+='\n'
1374+ r='<?xml version="1.0" encoding="ISO-8859-1"?><pre class="term">%s</pre>'%r
1375+ if self.last_html==r:
1376+ return '<?xml version="1.0"?><idem></idem>'
1377+ else:
1378+ self.last_html=r
1379+# print self
1380+ return r
1381+ def __repr__(self):
1382+ d=self.dumplatin1()
1383+ r=""
1384+ for i in range(self.height):
1385+ r+="|%s|\n"%d[self.width*i:self.width*(i+1)]
1386+ return r
1387+
1388+class SynchronizedMethod:
1389+ def __init__(self,lock,orig):
1390+ self.lock=lock
1391+ self.orig=orig
1392+ def __call__(self,*l):
1393+ self.lock.acquire()
1394+ r=self.orig(*l)
1395+ self.lock.release()
1396+ return r
1397+
1398+class Multiplex:
1399+ def __init__(self,cmd=None):
1400+ signal.signal(signal.SIGCHLD, signal.SIG_IGN)
1401+ self.cmd=cmd
1402+ self.proc={}
1403+ self.lock=threading.RLock()
1404+ self.thread=threading.Thread(target=self.loop)
1405+ self.alive=1
1406+ self.lastActivity=time.time()
1407+ # synchronize methods
1408+ for name in ['create','fds','proc_read','proc_write','dump','die','run']:
1409+ orig=getattr(self,name)
1410+ setattr(self,name,SynchronizedMethod(self.lock,orig))
1411+ self.thread.start()
1412+ def create(self,w=80,h=25):
1413+ pid,fd=pty.fork()
1414+ if pid==0:
1415+ try:
1416+ fdl=[int(i) for i in os.listdir('/proc/self/fd')]
1417+ except OSError:
1418+ fdl=range(256)
1419+ for i in [i for i in fdl if i>2]:
1420+ try:
1421+ os.close(i)
1422+ except OSError:
1423+ pass
1424+ if self.cmd:
1425+ cmd=['/bin/sh','-c',self.cmd]
1426+ elif os.getuid()==0:
1427+ cmd=['/bin/login']
1428+ else:
1429+ sys.stdout.write("Login: ")
1430+ login=sys.stdin.readline().strip()
1431+ if re.match('^[0-9A-Za-z-_. ]+$',login):
1432+ cmd=['ssh']
1433+ cmd+=['-oPreferredAuthentications=keyboard-interactive,password']
1434+ cmd+=['-oNoHostAuthenticationForLocalhost=yes']
1435+ cmd+=['-oLogLevel=FATAL']
1436+ cmd+=['-F/dev/null','-l',login,'localhost']
1437+ else:
1438+ os._exit(0)
1439+ env={}
1440+ env["COLUMNS"]=str(w)
1441+ env["LINES"]=str(h)
1442+ env["TERM"]="linux"
1443+ env["PATH"]=os.environ['PATH']
1444+ os.execvpe(cmd[0],cmd,env)
1445+ else:
1446+ fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
1447+ # python bug http://python.org/sf/1112949 on amd64
1448+ fcntl.ioctl(fd, struct.unpack('i',struct.pack('I',termios.TIOCSWINSZ))[0], struct.pack("HHHH",h,w,0,0))
1449+ self.proc[fd]={'pid':pid,'term':Terminal(w,h),'buf':'','time':time.time()}
1450+ return fd
1451+ def die(self):
1452+ self.alive=0
1453+ def run(self):
1454+ return self.alive
1455+ def fds(self):
1456+ return self.proc.keys()
1457+ def proc_kill(self,fd):
1458+ if fd in self.proc:
1459+ self.proc[fd]['time']=0
1460+ t=time.time()
1461+ for i in self.proc.keys():
1462+ t0=self.proc[i]['time']
1463+ if (t-t0)>TIMEOUT:
1464+ try:
1465+ os.close(i)
1466+ os.kill(self.proc[i]['pid'],signal.SIGTERM)
1467+ except (IOError,OSError):
1468+ pass
1469+ del self.proc[i]
1470+ def proc_read(self,fd):
1471+ try:
1472+ t=self.proc[fd]['term']
1473+ t.write(os.read(fd,65536))
1474+ reply=t.read()
1475+ if reply:
1476+ os.write(fd,reply)
1477+ self.proc[fd]['time']=time.time()
1478+ except (KeyError,IOError,OSError):
1479+ self.proc_kill(fd)
1480+ def proc_write(self,fd,s):
1481+ try:
1482+ os.write(fd,s)
1483+ except (IOError,OSError):
1484+ self.proc_kill(fd)
1485+ def dump(self,fd,color=1):
1486+ try:
1487+ return self.proc[fd]['term'].dumphtml(color)
1488+ except KeyError:
1489+ return False
1490+ def loop(self):
1491+ while self.run():
1492+ fds=self.fds()
1493+ i,o,e=select.select(fds, [], [], 1.0)
1494+ if time.time() - self.lastActivity > TIMEOUT:
1495+ global g_server
1496+ g_server.shutdown()
1497+ for fd in i:
1498+ self.proc_read(fd)
1499+ if len(i):
1500+ time.sleep(0.002)
1501+ for i in self.proc.keys():
1502+ try:
1503+ os.close(i)
1504+ os.kill(self.proc[i]['pid'],signal.SIGTERM)
1505+ except (IOError,OSError):
1506+ pass
1507+
1508+class AjaxTerm:
1509+ def __init__(self,cmd=None,index_file='ajaxterm.html',token=None):
1510+ self.files={}
1511+ self.token=token
1512+ for i in ['css','html','js']:
1513+ for j in glob.glob('*.%s'%i):
1514+ self.files[j]=file(j).read()
1515+ self.files['index']=file(index_file).read()
1516+ self.mime = mimetypes.types_map.copy()
1517+ self.mime['.html']= 'text/html; charset=UTF-8'
1518+ self.multi = Multiplex(cmd)
1519+ self.session = {}
1520+ def __call__(self, environ, start_response):
1521+ req = qweb.QWebRequest(environ, start_response,session=None)
1522+ if req.PATH_INFO.endswith('/u'):
1523+ s=req.REQUEST["s"]
1524+ k=req.REQUEST["k"]
1525+ c=req.REQUEST["c"]
1526+ w=req.REQUEST.int("w")
1527+ h=req.REQUEST.int("h")
1528+ if s in self.session:
1529+ term=self.session[s]
1530+ else:
1531+ raise Exception('Not Authorized')
1532+ # The original code below was insecure, because it allowed unauthorized sessions to be created
1533+ # if not (w>2 and w<256 and h>2 and h<100):
1534+ # w,h=80,25
1535+ # term=self.session[s]=self.multi.create(w,h)
1536+ if k:
1537+ self.multi.proc_write(term,k)
1538+ time.sleep(0.002)
1539+ self.multi.lastActivity = time.time();
1540+ dump=self.multi.dump(term,c)
1541+ req.response_headers['Content-Type']='text/xml'
1542+ if isinstance(dump,str):
1543+ req.write(dump)
1544+ req.response_gzencode=1
1545+ else:
1546+ del self.session[s]
1547+ req.write('<?xml version="1.0"?><idem></idem>')
1548+# print "sessions %r"%self.session
1549+ else:
1550+ n=os.path.basename(req.PATH_INFO)
1551+ if n in self.files:
1552+ req.response_headers['Content-Type'] = self.mime.get(os.path.splitext(n)[1].lower(), 'application/octet-stream')
1553+ req.write(self.files[n])
1554+ elif req.REQUEST['token'] == self.token:
1555+ req.response_headers['Content-Type'] = 'text/html; charset=UTF-8'
1556+ session_id = str(uuid.uuid4())
1557+ req.write(string.Template(self.files['index']).substitute(session_id=session_id))
1558+ term=self.session[session_id]=self.multi.create(80,25)
1559+ else:
1560+ raise Exception("Not Authorized")
1561+ return req
1562+
1563+def main():
1564+ parser = optparse.OptionParser()
1565+ parser.add_option("-p", "--port", dest="port", default="8022", help="Set the TCP port (default: 8022)")
1566+ parser.add_option("-c", "--command", dest="cmd", default=None,help="set the command (default: /bin/login or ssh 0.0.0.0)")
1567+ parser.add_option("-l", "--log", action="store_true", dest="log",default=0,help="log requests to stderr (default: quiet mode)")
1568+ parser.add_option("-d", "--daemon", action="store_true", dest="daemon", default=0, help="run as daemon in the background")
1569+ parser.add_option("-P", "--pidfile",dest="pidfile",default="/var/run/ajaxterm.pid",help="set the pidfile (default: /var/run/ajaxterm.pid)")
1570+ parser.add_option("-i", "--index", dest="index_file", default="ajaxterm.html",help="default index file (default: ajaxterm.html)")
1571+ parser.add_option("-u", "--uid", dest="uid", help="Set the daemon's user id")
1572+ parser.add_option("-t", "--token", dest="token", help="Set authorization token")
1573+ (o, a) = parser.parse_args()
1574+ if o.daemon:
1575+ pid=os.fork()
1576+ if pid == 0:
1577+ #os.setsid() ?
1578+ os.setpgrp()
1579+ nullin = file('/dev/null', 'r')
1580+ nullout = file('/dev/null', 'w')
1581+ os.dup2(nullin.fileno(), sys.stdin.fileno())
1582+ os.dup2(nullout.fileno(), sys.stdout.fileno())
1583+ os.dup2(nullout.fileno(), sys.stderr.fileno())
1584+ if os.getuid()==0 and o.uid:
1585+ try:
1586+ os.setuid(int(o.uid))
1587+ except:
1588+ os.setuid(pwd.getpwnam(o.uid).pw_uid)
1589+ else:
1590+ try:
1591+ file(o.pidfile,'w+').write(str(pid)+'\n')
1592+ except:
1593+ pass
1594+ print 'AjaxTerm at http://0.0.0.0:%s/ pid: %d' % (o.port,pid)
1595+ sys.exit(0)
1596+ else:
1597+ print 'AjaxTerm at http://0.0.0.0:%s/' % o.port
1598+ at=AjaxTerm(o.cmd,o.index_file,o.token)
1599+# f=lambda:os.system('firefox http://localhost:%s/&'%o.port)
1600+# qweb.qweb_wsgi_autorun(at,ip='localhost',port=int(o.port),threaded=0,log=o.log,callback_ready=None)
1601+ try:
1602+ global g_server
1603+ g_server = qweb.QWebWSGIServer(at,ip='0.0.0.0',port=int(o.port),threaded=0,log=o.log)
1604+ g_server.serve_forever()
1605+ except KeyboardInterrupt,e:
1606+ sys.excepthook(*sys.exc_info())
1607+ at.multi.die()
1608+
1609+if __name__ == '__main__':
1610+ main()
1611+
1612
1613=== added file 'tools/ajaxterm/configure'
1614--- tools/ajaxterm/configure 1970-01-01 00:00:00 +0000
1615+++ tools/ajaxterm/configure 2011-01-12 03:17:16 +0000
1616@@ -0,0 +1,32 @@
1617+#!/usr/bin/env python
1618+
1619+import optparse,os
1620+
1621+parser = optparse.OptionParser()
1622+parser.add_option("", "--prefix", dest="prefix",default="/usr/local",help="installation prefix (default: /usr/local)")
1623+parser.add_option("", "--confdir", dest="confdir", default="/etc",help="configuration files directory prefix (default: /etc)")
1624+parser.add_option("", "--port", dest="port", default="8022", help="set the listening TCP port (default: 8022)")
1625+parser.add_option("", "--command", dest="cmd", default=None,help="set the command (default: /bin/login or ssh localhost)")
1626+(o, a) = parser.parse_args()
1627+
1628+print "Configuring prefix=",o.prefix," port=",o.port
1629+
1630+etc=o.confdir
1631+port=o.port
1632+cmd=o.cmd
1633+bin=os.path.join(o.prefix,"bin")
1634+lib=os.path.join(o.prefix,"share/ajaxterm")
1635+man=os.path.join(o.prefix,"share/man/man1")
1636+
1637+file("ajaxterm.bin","w").write(file("configure.ajaxterm.bin").read()%locals())
1638+file("Makefile","w").write(file("configure.makefile").read()%locals())
1639+
1640+if os.path.isfile("/etc/gentoo-release"):
1641+ file("ajaxterm.initd","w").write(file("configure.initd.gentoo").read()%locals())
1642+elif os.path.isfile("/etc/fedora-release") or os.path.isfile("/etc/redhat-release"):
1643+ file("ajaxterm.initd","w").write(file("configure.initd.redhat").read()%locals())
1644+else:
1645+ file("ajaxterm.initd","w").write(file("configure.initd.debian").read()%locals())
1646+
1647+os.system("chmod a+x ajaxterm.bin")
1648+os.system("chmod a+x ajaxterm.initd")
1649
1650=== added file 'tools/ajaxterm/configure.ajaxterm.bin'
1651--- tools/ajaxterm/configure.ajaxterm.bin 1970-01-01 00:00:00 +0000
1652+++ tools/ajaxterm/configure.ajaxterm.bin 2011-01-12 03:17:16 +0000
1653@@ -0,0 +1,2 @@
1654+#!/bin/sh
1655+PYTHONPATH=%(lib)s exec %(lib)s/ajaxterm.py $@
1656
1657=== added file 'tools/ajaxterm/configure.initd.debian'
1658--- tools/ajaxterm/configure.initd.debian 1970-01-01 00:00:00 +0000
1659+++ tools/ajaxterm/configure.initd.debian 2011-01-12 03:17:16 +0000
1660@@ -0,0 +1,33 @@
1661+#!/bin/sh
1662+
1663+PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin
1664+DAEMON=%(bin)s/ajaxterm
1665+PORT=%(port)s
1666+PIDFILE=/var/run/ajaxterm.pid
1667+
1668+[ -x "$DAEMON" ] || exit 0
1669+
1670+#. /lib/lsb/init-functions
1671+
1672+case "$1" in
1673+ start)
1674+ echo "Starting ajaxterm on port $PORT"
1675+ start-stop-daemon --start --pidfile $PIDFILE --exec $DAEMON -- --daemon --port=$PORT --uid=nobody || return 2
1676+ ;;
1677+ stop)
1678+ echo "Stopping ajaxterm"
1679+ start-stop-daemon --stop --pidfile $PIDFILE
1680+ rm -f $PIDFILE
1681+ ;;
1682+ restart|force-reload)
1683+ $0 stop
1684+ sleep 1
1685+ $0 start
1686+ ;;
1687+ *)
1688+ echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2
1689+ exit 3
1690+ ;;
1691+esac
1692+
1693+:
1694
1695=== added file 'tools/ajaxterm/configure.initd.gentoo'
1696--- tools/ajaxterm/configure.initd.gentoo 1970-01-01 00:00:00 +0000
1697+++ tools/ajaxterm/configure.initd.gentoo 2011-01-12 03:17:16 +0000
1698@@ -0,0 +1,27 @@
1699+#!/sbin/runscript
1700+
1701+# AjaxTerm Gentoo script, 08 May 2006 Mark Gillespie
1702+
1703+DAEMON=%(bin)s/ajaxterm
1704+PORT=%(port)s
1705+PIDFILE=/var/run/ajaxterm.pid
1706+
1707+depend()
1708+{
1709+ need net
1710+}
1711+
1712+start()
1713+{
1714+ ebegin "Starting AjaxTerm on port $PORT"
1715+ start-stop-daemon --start --pidfile $PIDFILE --exec $DAEMON -- --daemon --port=$PORT --uid=nobody
1716+ eend $?
1717+}
1718+
1719+stop()
1720+{
1721+ ebegin "Stopping AjaxTerm"
1722+ start-stop-daemon --stop --pidfile $PIDFILE
1723+ rm -f $PIDFILE
1724+ eend $?
1725+}
1726
1727=== added file 'tools/ajaxterm/configure.initd.redhat'
1728--- tools/ajaxterm/configure.initd.redhat 1970-01-01 00:00:00 +0000
1729+++ tools/ajaxterm/configure.initd.redhat 2011-01-12 03:17:16 +0000
1730@@ -0,0 +1,75 @@
1731+#
1732+# ajaxterm Startup script for ajaxterm
1733+#
1734+# chkconfig: - 99 99
1735+# description: Ajaxterm is a yadda yadda yadda
1736+# processname: ajaxterm
1737+# pidfile: /var/run/ajaxterm.pid
1738+# version: 1.0 Kevin Reichhart - ajaxterminit at lastname dot org
1739+
1740+# Source function library.
1741+. /etc/rc.d/init.d/functions
1742+
1743+if [ -f /etc/sysconfig/ajaxterm ]; then
1744+ . /etc/sysconfig/ajaxterm
1745+fi
1746+
1747+ajaxterm=/usr/local/bin/ajaxterm
1748+prog=ajaxterm
1749+pidfile=${PIDFILE-/var/run/ajaxterm.pid}
1750+lockfile=${LOCKFILE-/var/lock/subsys/ajaxterm}
1751+port=${PORT-8022}
1752+user=${xUSER-nobody}
1753+RETVAL=0
1754+
1755+
1756+start() {
1757+ echo -n $"Starting $prog: "
1758+ daemon $ajaxterm --daemon --port=$port --uid=$user $OPTIONS
1759+ RETVAL=$?
1760+ echo
1761+ [ $RETVAL = 0 ] && touch ${lockfile}
1762+ return $RETVAL
1763+}
1764+stop() {
1765+ echo -n $"Stopping $prog: "
1766+ killproc $ajaxterm
1767+ RETVAL=$?
1768+ echo
1769+ [ $RETVAL = 0 ] && rm -f ${lockfile} ${pidfile}
1770+}
1771+reload() {
1772+ echo -n $"Reloading $prog: "
1773+ killproc $ajaxterm -HUP
1774+ RETVAL=$?
1775+ echo
1776+}
1777+
1778+# See how we were called.
1779+case "$1" in
1780+ start)
1781+ start
1782+ ;;
1783+ stop)
1784+ stop
1785+ ;;
1786+ status)
1787+ status python ajaxterm
1788+ RETVAL=$?
1789+ ;;
1790+ restart)
1791+ stop
1792+ start
1793+ ;;
1794+ condrestart)
1795+ if [ -f ${pidfile} ] ; then
1796+ stop
1797+ start
1798+ fi
1799+ ;;
1800+ *)
1801+ echo $"Usage: $prog {start|stop|restart|condrestart}"
1802+ exit 1
1803+esac
1804+
1805+exit $RETVAL
1806
1807=== added file 'tools/ajaxterm/configure.makefile'
1808--- tools/ajaxterm/configure.makefile 1970-01-01 00:00:00 +0000
1809+++ tools/ajaxterm/configure.makefile 2011-01-12 03:17:16 +0000
1810@@ -0,0 +1,20 @@
1811+build:
1812+ true
1813+
1814+install:
1815+ install -d "%(bin)s"
1816+ install -d "%(lib)s"
1817+ install ajaxterm.bin "%(bin)s/ajaxterm"
1818+ install ajaxterm.initd "%(etc)s/init.d/ajaxterm"
1819+ install -m 644 ajaxterm.css ajaxterm.html ajaxterm.js qweb.py sarissa.js sarissa_dhtml.js "%(lib)s"
1820+ install -m 755 ajaxterm.py "%(lib)s"
1821+ gzip --best -c ajaxterm.1 > ajaxterm.1.gz
1822+ install -d "%(man)s"
1823+ install ajaxterm.1.gz "%(man)s"
1824+
1825+clean:
1826+ rm ajaxterm.bin
1827+ rm ajaxterm.initd
1828+ rm ajaxterm.1.gz
1829+ rm Makefile
1830+
1831
1832=== added file 'tools/ajaxterm/qweb.py'
1833--- tools/ajaxterm/qweb.py 1970-01-01 00:00:00 +0000
1834+++ tools/ajaxterm/qweb.py 2011-01-12 03:17:16 +0000
1835@@ -0,0 +1,1356 @@
1836+#!/usr/bin/python2.3
1837+#
1838+# vim:set et ts=4 fdc=0 fdn=2 fdl=0:
1839+#
1840+# There are no blank lines between blocks beacause i use folding from:
1841+# http://www.vim.org/scripts/script.php?script_id=515
1842+#
1843+
1844+"""= QWeb Framework =
1845+
1846+== What is QWeb ? ==
1847+
1848+QWeb is a python based [http://www.python.org/doc/peps/pep-0333/ WSGI]
1849+compatible web framework, it provides an infratructure to quickly build web
1850+applications consisting of:
1851+
1852+ * A lightweight request handler (QWebRequest)
1853+ * An xml templating engine (QWebXml and QWebHtml)
1854+ * A simple name based controler (qweb_control)
1855+ * A standalone WSGI Server (QWebWSGIServer)
1856+ * A cgi and fastcgi WSGI wrapper (taken from flup)
1857+ * A startup function that starts cgi, factgi or standalone according to the
1858+ evironement (qweb_autorun).
1859+
1860+QWeb applications are runnable in standalone mode (from commandline), via
1861+FastCGI, Regular CGI or by any python WSGI compliant server.
1862+
1863+QWeb doesn't provide any database access but it integrates nicely with ORMs
1864+such as SQLObject, SQLAlchemy or plain DB-API.
1865+
1866+Written by Antony Lesuisse (email al AT udev.org)
1867+
1868+Homepage: http://antony.lesuisse.org/qweb/trac/
1869+
1870+Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum]
1871+
1872+== Quick Start (for Linux, MacOS X and cygwin) ==
1873+
1874+Make sure you have at least python 2.3 installed and run the following commands:
1875+
1876+{{{
1877+$ wget http://antony.lesuisse.org/qweb/files/QWeb-0.7.tar.gz
1878+$ tar zxvf QWeb-0.7.tar.gz
1879+$ cd QWeb-0.7/examples/blog
1880+$ ./blog.py
1881+}}}
1882+
1883+And point your browser to http://localhost:8080/
1884+
1885+You may also try AjaxTerm which uses qweb request handler.
1886+
1887+== Download ==
1888+
1889+ * Version 0.7:
1890+ * Source [/qweb/files/QWeb-0.7.tar.gz QWeb-0.7.tar.gz]
1891+ * Python 2.3 Egg [/qweb/files/QWeb-0.7-py2.3.egg QWeb-0.7-py2.3.egg]
1892+ * Python 2.4 Egg [/qweb/files/QWeb-0.7-py2.4.egg QWeb-0.7-py2.4.egg]
1893+
1894+ * [/qweb/trac/browser Browse the source repository]
1895+
1896+== Documentation ==
1897+
1898+ * [/qweb/trac/browser/trunk/README.txt?format=raw Read the included documentation]
1899+ * QwebTemplating
1900+
1901+== Mailin-list ==
1902+
1903+ * Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum]
1904+ * 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]
1905+
1906+QWeb Components:
1907+----------------
1908+
1909+QWeb also feature a simple components api, that enables developers to easily
1910+produces reusable components.
1911+
1912+Default qweb components:
1913+
1914+ - qweb_static:
1915+ A qweb component to serve static content from the filesystem or from
1916+ zipfiles.
1917+
1918+ - qweb_dbadmin:
1919+ scaffolding for sqlobject
1920+
1921+License
1922+-------
1923+qweb/fcgi.py wich is BSD-like from saddi.com.
1924+Everything else is put in the public domain.
1925+
1926+
1927+TODO
1928+----
1929+ Announce QWeb to python-announce-list@python.org web-sig@python.org
1930+ qweb_core
1931+ rename request methods into
1932+ request_save_files
1933+ response_404
1934+ response_redirect
1935+ response_download
1936+ request callback_generator, callback_function ?
1937+ wsgi callback_server_local
1938+ xml tags explicitly call render_attributes(t_att)?
1939+ priority form-checkbox over t-value (for t-option)
1940+
1941+"""
1942+
1943+import BaseHTTPServer,SocketServer,Cookie
1944+import cgi,datetime,email,email.Message,errno,gzip,os,random,re,socket,sys,tempfile,time,types,urllib,urlparse,xml.dom
1945+try:
1946+ import cPickle as pickle
1947+except ImportError:
1948+ import pickle
1949+try:
1950+ import cStringIO as StringIO
1951+except ImportError:
1952+ import StringIO
1953+
1954+#----------------------------------------------------------
1955+# Qweb Xml t-raw t-esc t-if t-foreach t-set t-call t-trim
1956+#----------------------------------------------------------
1957+class QWebEval:
1958+ def __init__(self,data):
1959+ self.data=data
1960+ def __getitem__(self,expr):
1961+ if self.data.has_key(expr):
1962+ return self.data[expr]
1963+ r=None
1964+ try:
1965+ r=eval(expr,self.data)
1966+ except NameError,e:
1967+ pass
1968+ except AttributeError,e:
1969+ pass
1970+ except Exception,e:
1971+ print "qweb: expression error '%s' "%expr,e
1972+ if self.data.has_key("__builtins__"):
1973+ del self.data["__builtins__"]
1974+ return r
1975+ def eval_object(self,expr):
1976+ return self[expr]
1977+ def eval_str(self,expr):
1978+ if expr=="0":
1979+ return self.data[0]
1980+ if isinstance(self[expr],unicode):
1981+ return self[expr].encode("utf8")
1982+ return str(self[expr])
1983+ def eval_format(self,expr):
1984+ try:
1985+ return str(expr%self)
1986+ except:
1987+ return "qweb: format error '%s' "%expr
1988+# if isinstance(r,unicode):
1989+# return r.encode("utf8")
1990+ def eval_bool(self,expr):
1991+ if self.eval_object(expr):
1992+ return 1
1993+ else:
1994+ return 0
1995+class QWebXml:
1996+ """QWeb Xml templating engine
1997+
1998+ The templating engine use a very simple syntax, "magic" xml attributes, to
1999+ produce any kind of texutal output (even non-xml).
2000+
2001+ QWebXml:
2002+ the template engine core implements the basic magic attributes:
2003+
2004+ t-att t-raw t-esc t-if t-foreach t-set t-call t-trim
2005+
2006+ """
2007+ def __init__(self,x=None,zipname=None):
2008+ self.node=xml.dom.Node
2009+ self._t={}
2010+ self._render_tag={}
2011+ prefix='render_tag_'
2012+ for i in [j for j in dir(self) if j.startswith(prefix)]:
2013+ name=i[len(prefix):].replace('_','-')
2014+ self._render_tag[name]=getattr(self.__class__,i)
2015+
2016+ self._render_att={}
2017+ prefix='render_att_'
2018+ for i in [j for j in dir(self) if j.startswith(prefix)]:
2019+ name=i[len(prefix):].replace('_','-')
2020+ self._render_att[name]=getattr(self.__class__,i)
2021+
2022+ if x!=None:
2023+ if zipname!=None:
2024+ import zipfile
2025+ zf=zipfile.ZipFile(zipname, 'r')
2026+ self.add_template(zf.read(x))
2027+ else:
2028+ self.add_template(x)
2029+ def register_tag(self,tag,func):
2030+ self._render_tag[tag]=func
2031+ def add_template(self,x):
2032+ if hasattr(x,'documentElement'):
2033+ dom=x
2034+ elif x.startswith("<?xml"):
2035+ import xml.dom.minidom
2036+ dom=xml.dom.minidom.parseString(x)
2037+ else:
2038+ import xml.dom.minidom
2039+ dom=xml.dom.minidom.parse(x)
2040+ for n in dom.documentElement.childNodes:
2041+ if n.nodeName=="t":
2042+ self._t[str(n.getAttribute("t-name"))]=n
2043+ def get_template(self,name):
2044+ return self._t[name]
2045+
2046+ def eval_object(self,expr,v):
2047+ return QWebEval(v).eval_object(expr)
2048+ def eval_str(self,expr,v):
2049+ return QWebEval(v).eval_str(expr)
2050+ def eval_format(self,expr,v):
2051+ return QWebEval(v).eval_format(expr)
2052+ def eval_bool(self,expr,v):
2053+ return QWebEval(v).eval_bool(expr)
2054+
2055+ def render(self,tname,v={},out=None):
2056+ if self._t.has_key(tname):
2057+ return self.render_node(self._t[tname],v)
2058+ else:
2059+ return 'qweb: template "%s" not found'%tname
2060+ def render_node(self,e,v):
2061+ r=""
2062+ if e.nodeType==self.node.TEXT_NODE or e.nodeType==self.node.CDATA_SECTION_NODE:
2063+ r=e.data.encode("utf8")
2064+ elif e.nodeType==self.node.ELEMENT_NODE:
2065+ pre=""
2066+ g_att=""
2067+ t_render=None
2068+ t_att={}
2069+ for (an,av) in e.attributes.items():
2070+ an=str(an)
2071+ if isinstance(av,types.UnicodeType):
2072+ av=av.encode("utf8")
2073+ else:
2074+ av=av.nodeValue.encode("utf8")
2075+ if an.startswith("t-"):
2076+ for i in self._render_att:
2077+ if an[2:].startswith(i):
2078+ g_att+=self._render_att[i](self,e,an,av,v)
2079+ break
2080+ else:
2081+ if self._render_tag.has_key(an[2:]):
2082+ t_render=an[2:]
2083+ t_att[an[2:]]=av
2084+ else:
2085+ g_att+=' %s="%s"'%(an,cgi.escape(av,1));
2086+ if t_render:
2087+ if self._render_tag.has_key(t_render):
2088+ r=self._render_tag[t_render](self,e,t_att,g_att,v)
2089+ else:
2090+ r=self.render_element(e,g_att,v,pre,t_att.get("trim",0))
2091+ return r
2092+ def render_element(self,e,g_att,v,pre="",trim=0):
2093+ g_inner=[]
2094+ for n in e.childNodes:
2095+ g_inner.append(self.render_node(n,v))
2096+ name=str(e.nodeName)
2097+ inner="".join(g_inner)
2098+ if trim==0:
2099+ pass
2100+ elif trim=='left':
2101+ inner=inner.lstrip()
2102+ elif trim=='right':
2103+ inner=inner.rstrip()
2104+ elif trim=='both':
2105+ inner=inner.strip()
2106+ if name=="t":
2107+ return inner
2108+ elif len(inner):
2109+ return "<%s%s>%s%s</%s>"%(name,g_att,pre,inner,name)
2110+ else:
2111+ return "<%s%s/>"%(name,g_att)
2112+
2113+ # Attributes
2114+ def render_att_att(self,e,an,av,v):
2115+ if an.startswith("t-attf-"):
2116+ att,val=an[7:],self.eval_format(av,v)
2117+ elif an.startswith("t-att-"):
2118+ att,val=(an[6:],self.eval_str(av,v))
2119+ else:
2120+ att,val=self.eval_object(av,v)
2121+ return ' %s="%s"'%(att,cgi.escape(val,1))
2122+
2123+ # Tags
2124+ def render_tag_raw(self,e,t_att,g_att,v):
2125+ return self.eval_str(t_att["raw"],v)
2126+ def render_tag_rawf(self,e,t_att,g_att,v):
2127+ return self.eval_format(t_att["rawf"],v)
2128+ def render_tag_esc(self,e,t_att,g_att,v):
2129+ return cgi.escape(self.eval_str(t_att["esc"],v))
2130+ def render_tag_escf(self,e,t_att,g_att,v):
2131+ return cgi.escape(self.eval_format(t_att["escf"],v))
2132+ def render_tag_foreach(self,e,t_att,g_att,v):
2133+ expr=t_att["foreach"]
2134+ enum=self.eval_object(expr,v)
2135+ if enum!=None:
2136+ var=t_att.get('as',expr).replace('.','_')
2137+ d=v.copy()
2138+ size=-1
2139+ if isinstance(enum,types.ListType):
2140+ size=len(enum)
2141+ elif isinstance(enum,types.TupleType):
2142+ size=len(enum)
2143+ elif hasattr(enum,'count'):
2144+ size=enum.count()
2145+ d["%s_size"%var]=size
2146+ d["%s_all"%var]=enum
2147+ index=0
2148+ ru=[]
2149+ for i in enum:
2150+ d["%s_value"%var]=i
2151+ d["%s_index"%var]=index
2152+ d["%s_first"%var]=index==0
2153+ d["%s_even"%var]=index%2
2154+ d["%s_odd"%var]=(index+1)%2
2155+ d["%s_last"%var]=index+1==size
2156+ if index%2:
2157+ d["%s_parity"%var]='odd'
2158+ else:
2159+ d["%s_parity"%var]='even'
2160+ if isinstance(i,types.DictType):
2161+ d.update(i)
2162+ else:
2163+ d[var]=i
2164+ ru.append(self.render_element(e,g_att,d))
2165+ index+=1
2166+ return "".join(ru)
2167+ else:
2168+ return "qweb: t-foreach %s not found."%expr
2169+ def render_tag_if(self,e,t_att,g_att,v):
2170+ if self.eval_bool(t_att["if"],v):
2171+ return self.render_element(e,g_att,v)
2172+ else:
2173+ return ""
2174+ def render_tag_call(self,e,t_att,g_att,v):
2175+ # TODO t-prefix
2176+ if t_att.has_key("import"):
2177+ d=v
2178+ else:
2179+ d=v.copy()
2180+ d[0]=self.render_element(e,g_att,d)
2181+ return self.render(t_att["call"],d)
2182+ def render_tag_set(self,e,t_att,g_att,v):
2183+ if t_att.has_key("eval"):
2184+ v[t_att["set"]]=self.eval_object(t_att["eval"],v)
2185+ else:
2186+ v[t_att["set"]]=self.render_element(e,g_att,v)
2187+ return ""
2188+
2189+#----------------------------------------------------------
2190+# QWeb HTML (+deprecated QWebFORM and QWebOLD)
2191+#----------------------------------------------------------
2192+class QWebURL:
2193+ """ URL helper
2194+ assert req.PATH_INFO== "/site/admin/page_edit"
2195+ u = QWebURL(root_path="/site/",req_path=req.PATH_INFO)
2196+ s=u.url2_href("user/login",{'a':'1'})
2197+ assert s=="../user/login?a=1"
2198+
2199+ """
2200+ def __init__(self, root_path="/", req_path="/",defpath="",defparam={}):
2201+ self.defpath=defpath
2202+ self.defparam=defparam
2203+ self.root_path=root_path
2204+ self.req_path=req_path
2205+ self.req_list=req_path.split("/")[:-1]
2206+ self.req_len=len(self.req_list)
2207+ def decode(self,s):
2208+ h={}
2209+ for k,v in cgi.parse_qsl(s,1):
2210+ h[k]=v
2211+ return h
2212+ def encode(self,h):
2213+ return urllib.urlencode(h.items())
2214+ def request(self,req):
2215+ return req.REQUEST
2216+ def copy(self,path=None,param=None):
2217+ npath=self.defpath
2218+ if path:
2219+ npath=path
2220+ nparam=self.defparam.copy()
2221+ if param:
2222+ nparam.update(param)
2223+ return QWebURL(self.root_path,self.req_path,npath,nparam)
2224+ def path(self,path=''):
2225+ if not path:
2226+ path=self.defpath
2227+ pl=(self.root_path+path).split('/')
2228+ i=0
2229+ for i in range(min(len(pl), self.req_len)):
2230+ if pl[i]!=self.req_list[i]:
2231+ break
2232+ else:
2233+ i+=1
2234+ dd=self.req_len-i
2235+ if dd<0:
2236+ dd=0
2237+ return '/'.join(['..']*dd+pl[i:])
2238+ def href(self,path='',arg={}):
2239+ p=self.path(path)
2240+ tmp=self.defparam.copy()
2241+ tmp.update(arg)
2242+ s=self.encode(tmp)
2243+ if len(s):
2244+ return p+"?"+s
2245+ else:
2246+ return p
2247+ def form(self,path='',arg={}):
2248+ p=self.path(path)
2249+ tmp=self.defparam.copy()
2250+ tmp.update(arg)
2251+ r=''.join(['<input type="hidden" name="%s" value="%s"/>'%(k,cgi.escape(str(v),1)) for k,v in tmp.items()])
2252+ return (p,r)
2253+class QWebField:
2254+ def __init__(self,name=None,default="",check=None):
2255+ self.name=name
2256+ self.default=default
2257+ self.check=check
2258+ # optional attributes
2259+ self.type=None
2260+ self.trim=1
2261+ self.required=1
2262+ self.cssvalid="form_valid"
2263+ self.cssinvalid="form_invalid"
2264+ # set by addfield
2265+ self.form=None
2266+ # set by processing
2267+ self.input=None
2268+ self.css=None
2269+ self.value=None
2270+ self.valid=None
2271+ self.invalid=None
2272+ self.validate(1)
2273+ def validate(self,val=1,update=1):
2274+ if val:
2275+ self.valid=1
2276+ self.invalid=0
2277+ self.css=self.cssvalid
2278+ else:
2279+ self.valid=0
2280+ self.invalid=1
2281+ self.css=self.cssinvalid
2282+ if update and self.form:
2283+ self.form.update()
2284+ def invalidate(self,update=1):
2285+ self.validate(0,update)
2286+class QWebForm:
2287+ class QWebFormF:
2288+ pass
2289+ def __init__(self,e=None,arg=None,default=None):
2290+ self.fields={}
2291+ # all fields have been submitted
2292+ self.submitted=False
2293+ self.missing=[]
2294+ # at least one field is invalid or missing
2295+ self.invalid=False
2296+ self.error=[]
2297+ # all fields have been submitted and are valid
2298+ self.valid=False
2299+ # fields under self.f for convenience
2300+ self.f=self.QWebFormF()
2301+ if e:
2302+ self.add_template(e)
2303+ # assume that the fields are done with the template
2304+ if default:
2305+ self.set_default(default,e==None)
2306+ if arg!=None:
2307+ self.process_input(arg)
2308+ def __getitem__(self,k):
2309+ return self.fields[k]
2310+ def set_default(self,default,add_missing=1):
2311+ for k,v in default.items():
2312+ if self.fields.has_key(k):
2313+ self.fields[k].default=str(v)
2314+ elif add_missing:
2315+ self.add_field(QWebField(k,v))
2316+ def add_field(self,f):
2317+ self.fields[f.name]=f
2318+ f.form=self
2319+ setattr(self.f,f.name,f)
2320+ def add_template(self,e):
2321+ att={}
2322+ for (an,av) in e.attributes.items():
2323+ an=str(an)
2324+ if an.startswith("t-"):
2325+ att[an[2:]]=av.encode("utf8")
2326+ for i in ["form-text", "form-password", "form-radio", "form-checkbox", "form-select","form-textarea"]:
2327+ if att.has_key(i):
2328+ name=att[i].split(".")[-1]
2329+ default=att.get("default","")
2330+ check=att.get("check",None)
2331+ f=QWebField(name,default,check)
2332+ if i=="form-textarea":
2333+ f.type="textarea"
2334+ f.trim=0
2335+ if i=="form-checkbox":
2336+ f.type="checkbox"
2337+ f.required=0
2338+ self.add_field(f)
2339+ for n in e.childNodes:
2340+ if n.nodeType==n.ELEMENT_NODE:
2341+ self.add_template(n)
2342+ def process_input(self,arg):
2343+ for f in self.fields.values():
2344+ if arg.has_key(f.name):
2345+ f.input=arg[f.name]
2346+ f.value=f.input
2347+ if f.trim:
2348+ f.input=f.input.strip()
2349+ f.validate(1,False)
2350+ if f.check==None:
2351+ continue
2352+ elif callable(f.check):
2353+ pass
2354+ elif isinstance(f.check,str):
2355+ v=f.check
2356+ if f.check=="email":
2357+ v=r"/^[^@#!& ]+@[A-Za-z0-9-][.A-Za-z0-9-]{0,64}\.[A-Za-z]{2,5}$/"
2358+ if f.check=="date":
2359+ v=r"/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/"
2360+ if not re.match(v[1:-1],f.input):
2361+ f.validate(0,False)
2362+ else:
2363+ f.value=f.default
2364+ self.update()
2365+ def validate_all(self,val=1):
2366+ for f in self.fields.values():
2367+ f.validate(val,0)
2368+ self.update()
2369+ def invalidate_all(self):
2370+ self.validate_all(0)
2371+ def update(self):
2372+ self.submitted=True
2373+ self.valid=True
2374+ self.errors=[]
2375+ for f in self.fields.values():
2376+ if f.required and f.input==None:
2377+ self.submitted=False
2378+ self.valid=False
2379+ self.missing.append(f.name)
2380+ if f.invalid:
2381+ self.valid=False
2382+ self.error.append(f.name)
2383+ # invalid have been submitted and
2384+ self.invalid=self.submitted and self.valid==False
2385+ def collect(self):
2386+ d={}
2387+ for f in self.fields.values():
2388+ d[f.name]=f.value
2389+ return d
2390+class QWebURLEval(QWebEval):
2391+ def __init__(self,data):
2392+ QWebEval.__init__(self,data)
2393+ def __getitem__(self,expr):
2394+ r=QWebEval.__getitem__(self,expr)
2395+ if isinstance(r,str):
2396+ return urllib.quote_plus(r)
2397+ else:
2398+ return r
2399+class QWebHtml(QWebXml):
2400+ """QWebHtml
2401+ QWebURL:
2402+ QWebField:
2403+ QWebForm:
2404+ QWebHtml:
2405+ an extended template engine, with a few utility class to easily produce
2406+ HTML, handle URLs and process forms, it adds the following magic attributes:
2407+
2408+ t-href t-action t-form-text t-form-password t-form-textarea t-form-radio
2409+ t-form-checkbox t-form-select t-option t-selected t-checked t-pager
2410+
2411+ # explication URL:
2412+ # v['tableurl']=QWebUrl({p=afdmin,saar=,orderby=,des=,mlink;meta_active=})
2413+ # t-href="tableurl?desc=1"
2414+ #
2415+ # explication FORM: t-if="form.valid()"
2416+ # Foreach i
2417+ # email: <input type="text" t-esc-name="i" t-esc-value="form[i].value" t-esc-class="form[i].css"/>
2418+ # <input type="radio" name="spamtype" t-esc-value="i" t-selected="i==form.f.spamtype.value"/>
2419+ # <option t-esc-value="cc" t-selected="cc==form.f.country.value"><t t-esc="cname"></option>
2420+ # Simple forms:
2421+ # <input t-form-text="form.email" t-check="email"/>
2422+ # <input t-form-password="form.email" t-check="email"/>
2423+ # <input t-form-radio="form.email" />
2424+ # <input t-form-checkbox="form.email" />
2425+ # <textarea t-form-textarea="form.email" t-check="email"/>
2426+ # <select t-form-select="form.email"/>
2427+ # <option t-value="1">
2428+ # <input t-form-radio="form.spamtype" t-value="1"/> Cars
2429+ # <input t-form-radio="form.spamtype" t-value="2"/> Sprt
2430+ """
2431+ # QWebForm from a template
2432+ def form(self,tname,arg=None,default=None):
2433+ form=QWebForm(self._t[tname],arg,default)
2434+ return form
2435+
2436+ # HTML Att
2437+ def eval_url(self,av,v):
2438+ s=QWebURLEval(v).eval_format(av)
2439+ a=s.split('?',1)
2440+ arg={}
2441+ if len(a)>1:
2442+ for k,v in cgi.parse_qsl(a[1],1):
2443+ arg[k]=v
2444+ b=a[0].split('/',1)
2445+ path=''
2446+ if len(b)>1:
2447+ path=b[1]
2448+ u=b[0]
2449+ return u,path,arg
2450+ def render_att_url_(self,e,an,av,v):
2451+ u,path,arg=self.eval_url(av,v)
2452+ if not isinstance(v.get(u,0),QWebURL):
2453+ out='qweb: missing url %r %r %r'%(u,path,arg)
2454+ else:
2455+ out=v[u].href(path,arg)
2456+ return ' %s="%s"'%(an[6:],cgi.escape(out,1))
2457+ def render_att_href(self,e,an,av,v):
2458+ return self.render_att_url_(e,"t-url-href",av,v)
2459+ def render_att_checked(self,e,an,av,v):
2460+ if self.eval_bool(av,v):
2461+ return ' %s="%s"'%(an[2:],an[2:])
2462+ else:
2463+ return ''
2464+ def render_att_selected(self,e,an,av,v):
2465+ return self.render_att_checked(e,an,av,v)
2466+
2467+ # HTML Tags forms
2468+ def render_tag_rawurl(self,e,t_att,g_att,v):
2469+ u,path,arg=self.eval_url(t_att["rawurl"],v)
2470+ return v[u].href(path,arg)
2471+ def render_tag_escurl(self,e,t_att,g_att,v):
2472+ u,path,arg=self.eval_url(t_att["escurl"],v)
2473+ return cgi.escape(v[u].href(path,arg))
2474+ def render_tag_action(self,e,t_att,g_att,v):
2475+ u,path,arg=self.eval_url(t_att["action"],v)
2476+ if not isinstance(v.get(u,0),QWebURL):
2477+ action,input=('qweb: missing url %r %r %r'%(u,path,arg),'')
2478+ else:
2479+ action,input=v[u].form(path,arg)
2480+ g_att+=' action="%s"'%action
2481+ return self.render_element(e,g_att,v,input)
2482+ def render_tag_form_text(self,e,t_att,g_att,v):
2483+ f=self.eval_object(t_att["form-text"],v)
2484+ g_att+=' type="text" name="%s" value="%s" class="%s"'%(f.name,cgi.escape(f.value,1),f.css)
2485+ return self.render_element(e,g_att,v)
2486+ def render_tag_form_password(self,e,t_att,g_att,v):
2487+ f=self.eval_object(t_att["form-password"],v)
2488+ g_att+=' type="password" name="%s" value="%s" class="%s"'%(f.name,cgi.escape(f.value,1),f.css)
2489+ return self.render_element(e,g_att,v)
2490+ def render_tag_form_textarea(self,e,t_att,g_att,v):
2491+ type="textarea"
2492+ f=self.eval_object(t_att["form-textarea"],v)
2493+ g_att+=' name="%s" class="%s"'%(f.name,f.css)
2494+ r="<%s%s>%s</%s>"%(type,g_att,cgi.escape(f.value,1),type)
2495+ return r
2496+ def render_tag_form_radio(self,e,t_att,g_att,v):
2497+ f=self.eval_object(t_att["form-radio"],v)
2498+ val=t_att["value"]
2499+ g_att+=' type="radio" name="%s" value="%s"'%(f.name,val)
2500+ if f.value==val:
2501+ g_att+=' checked="checked"'
2502+ return self.render_element(e,g_att,v)
2503+ def render_tag_form_checkbox(self,e,t_att,g_att,v):
2504+ f=self.eval_object(t_att["form-checkbox"],v)
2505+ val=t_att["value"]
2506+ g_att+=' type="checkbox" name="%s" value="%s"'%(f.name,val)
2507+ if f.value==val:
2508+ g_att+=' checked="checked"'
2509+ return self.render_element(e,g_att,v)
2510+ def render_tag_form_select(self,e,t_att,g_att,v):
2511+ f=self.eval_object(t_att["form-select"],v)
2512+ g_att+=' name="%s" class="%s"'%(f.name,f.css)
2513+ return self.render_element(e,g_att,v)
2514+ def render_tag_option(self,e,t_att,g_att,v):
2515+ f=self.eval_object(e.parentNode.getAttribute("t-form-select"),v)
2516+ val=t_att["option"]
2517+ g_att+=' value="%s"'%(val)
2518+ if f.value==val:
2519+ g_att+=' selected="selected"'
2520+ return self.render_element(e,g_att,v)
2521+
2522+ # HTML Tags others
2523+ def render_tag_pager(self,e,t_att,g_att,v):
2524+ pre=t_att["pager"]
2525+ total=int(self.eval_str(t_att["total"],v))
2526+ start=int(self.eval_str(t_att["start"],v))
2527+ step=int(self.eval_str(t_att.get("step","100"),v))
2528+ scope=int(self.eval_str(t_att.get("scope","5"),v))
2529+ # Compute Pager
2530+ p=pre+"_"
2531+ d={}
2532+ d[p+"tot_size"]=total
2533+ d[p+"tot_page"]=tot_page=total/step
2534+ d[p+"win_start0"]=total and start
2535+ d[p+"win_start1"]=total and start+1
2536+ d[p+"win_end0"]=max(0,min(start+step-1,total-1))
2537+ d[p+"win_end1"]=min(start+step,total)
2538+ d[p+"win_page0"]=win_page=start/step
2539+ d[p+"win_page1"]=win_page+1
2540+ d[p+"prev"]=(win_page!=0)
2541+ d[p+"prev_start"]=(win_page-1)*step
2542+ d[p+"next"]=(tot_page>=win_page+1)
2543+ d[p+"next_start"]=(win_page+1)*step
2544+ l=[]
2545+ begin=win_page-scope
2546+ end=win_page+scope
2547+ if begin<0:
2548+ end-=begin
2549+ if end>tot_page:
2550+ begin-=(end-tot_page)
2551+ i=max(0,begin)
2552+ while i<=min(end,tot_page) and total!=step:
2553+ l.append( { p+"page0":i, p+"page1":i+1, p+"start":i*step, p+"sel":(win_page==i) })
2554+ i+=1
2555+ d[p+"active"]=len(l)>1
2556+ d[p+"list"]=l
2557+ # Update v
2558+ v.update(d)
2559+ return ""
2560+
2561+#----------------------------------------------------------
2562+# QWeb Simple Controller
2563+#----------------------------------------------------------
2564+def qweb_control(self,jump='main',p=[]):
2565+ """ qweb_control(self,jump='main',p=[]):
2566+ A simple function to handle the controler part of your application. It
2567+ dispatch the control to the jump argument, while ensuring that prefix
2568+ function have been called.
2569+
2570+ qweb_control replace '/' to '_' and strip '_' from the jump argument.
2571+
2572+ name1
2573+ name1_name2
2574+ name1_name2_name3
2575+
2576+ """
2577+ jump=jump.replace('/','_').strip('_')
2578+ if not hasattr(self,jump):
2579+ return 0
2580+ done={}
2581+ todo=[]
2582+ while 1:
2583+ if jump!=None:
2584+ tmp=""
2585+ todo=[]
2586+ for i in jump.split("_"):
2587+ tmp+=i+"_";
2588+ if not done.has_key(tmp[:-1]):
2589+ todo.append(tmp[:-1])
2590+ jump=None
2591+ elif len(todo):
2592+ i=todo.pop(0)
2593+ done[i]=1
2594+ if hasattr(self,i):
2595+ f=getattr(self,i)
2596+ r=f(*p)
2597+ if isinstance(r,types.StringType):
2598+ jump=r
2599+ else:
2600+ break
2601+ return 1
2602+
2603+#----------------------------------------------------------
2604+# QWeb WSGI Request handler
2605+#----------------------------------------------------------
2606+class QWebSession(dict):
2607+ def __init__(self,environ,**kw):
2608+ dict.__init__(self)
2609+ default={
2610+ "path" : tempfile.gettempdir(),
2611+ "cookie_name" : "QWEBSID",
2612+ "cookie_lifetime" : 0,
2613+ "cookie_path" : '/',
2614+ "cookie_domain" : '',
2615+ "limit_cache" : 1,
2616+ "probability" : 0.01,
2617+ "maxlifetime" : 3600,
2618+ "disable" : 0,
2619+ }
2620+ for k,v in default.items():
2621+ setattr(self,'session_%s'%k,kw.get(k,v))
2622+ # Try to find session
2623+ self.session_found_cookie=0
2624+ self.session_found_url=0
2625+ self.session_found=0
2626+ self.session_orig=""
2627+ # Try cookie
2628+ c=Cookie.SimpleCookie()
2629+ c.load(environ.get('HTTP_COOKIE', ''))
2630+ if c.has_key(self.session_cookie_name):
2631+ sid=c[self.session_cookie_name].value[:64]
2632+ if re.match('[a-f0-9]+$',sid) and self.session_load(sid):
2633+ self.session_id=sid
2634+ self.session_found_cookie=1
2635+ self.session_found=1
2636+ # Try URL
2637+ if not self.session_found_cookie:
2638+ mo=re.search('&%s=([a-f0-9]+)'%self.session_cookie_name,environ.get('QUERY_STRING',''))
2639+ if mo and self.session_load(mo.group(1)):
2640+ self.session_id=mo.group(1)
2641+ self.session_found_url=1
2642+ self.session_found=1
2643+ # New session
2644+ if not self.session_found:
2645+ self.session_id='%032x'%random.randint(1,2**128)
2646+ self.session_trans_sid="&amp;%s=%s"%(self.session_cookie_name,self.session_id)
2647+ # Clean old session
2648+ if random.random() < self.session_probability:
2649+ self.session_clean()
2650+ def session_get_headers(self):
2651+ h=[]
2652+ if (not self.session_disable) and (len(self) or len(self.session_orig)):
2653+ self.session_save()
2654+ if not self.session_found_cookie:
2655+ c=Cookie.SimpleCookie()
2656+ c[self.session_cookie_name] = self.session_id
2657+ c[self.session_cookie_name]['path'] = self.session_cookie_path
2658+ if self.session_cookie_domain:
2659+ c[self.session_cookie_name]['domain'] = self.session_cookie_domain
2660+# if self.session_cookie_lifetime:
2661+# c[self.session_cookie_name]['expires'] = TODO date localtime or not, datetime.datetime(1970, 1, 1)
2662+ h.append(("Set-Cookie", c[self.session_cookie_name].OutputString()))
2663+ if self.session_limit_cache:
2664+ h.append(('Cache-Control','no-store, no-cache, must-revalidate, post-check=0, pre-check=0'))
2665+ h.append(('Expires','Thu, 19 Nov 1981 08:52:00 GMT'))
2666+ h.append(('Pragma','no-cache'))
2667+ return h
2668+ def session_load(self,sid):
2669+ fname=os.path.join(self.session_path,'qweb_sess_%s'%sid)
2670+ try:
2671+ orig=file(fname).read()
2672+ d=pickle.loads(orig)
2673+ except:
2674+ return
2675+ self.session_orig=orig
2676+ self.update(d)
2677+ return 1
2678+ def session_save(self):
2679+ if not os.path.isdir(self.session_path):
2680+ os.makedirs(self.session_path)
2681+ fname=os.path.join(self.session_path,'qweb_sess_%s'%self.session_id)
2682+ try:
2683+ oldtime=os.path.getmtime(fname)
2684+ except OSError,IOError:
2685+ oldtime=0
2686+ dump=pickle.dumps(self.copy())
2687+ if (dump != self.session_orig) or (time.time() > oldtime+self.session_maxlifetime/4):
2688+ tmpname=os.path.join(self.session_path,'qweb_sess_%s_%x'%(self.session_id,random.randint(1,2**32)))
2689+ f=file(tmpname,'wb')
2690+ f.write(dump)
2691+ f.close()
2692+ if sys.platform=='win32' and os.path.isfile(fname):
2693+ os.remove(fname)
2694+ os.rename(tmpname,fname)
2695+ def session_clean(self):
2696+ t=time.time()
2697+ try:
2698+ for i in [os.path.join(self.session_path,i) for i in os.listdir(self.session_path) if i.startswith('qweb_sess_')]:
2699+ if (t > os.path.getmtime(i)+self.session_maxlifetime):
2700+ os.unlink(i)
2701+ except OSError,IOError:
2702+ pass
2703+class QWebSessionMem(QWebSession):
2704+ def session_load(self,sid):
2705+ global _qweb_sessions
2706+ if not "_qweb_sessions" in globals():
2707+ _qweb_sessions={}
2708+ if _qweb_sessions.has_key(sid):
2709+ self.session_orig=_qweb_sessions[sid]
2710+ self.update(self.session_orig)
2711+ return 1
2712+ def session_save(self):
2713+ global _qweb_sessions
2714+ if not "_qweb_sessions" in globals():
2715+ _qweb_sessions={}
2716+ _qweb_sessions[self.session_id]=self.copy()
2717+class QWebSessionService:
2718+ def __init__(self, wsgiapp, url_rewrite=0):
2719+ self.wsgiapp=wsgiapp
2720+ self.url_rewrite_tags="a=href,area=href,frame=src,form=,fieldset="
2721+ def __call__(self, environ, start_response):
2722+ # TODO
2723+ # use QWebSession to provide environ["qweb.session"]
2724+ return self.wsgiapp(environ,start_response)
2725+class QWebDict(dict):
2726+ def __init__(self,*p):
2727+ dict.__init__(self,*p)
2728+ def __getitem__(self,key):
2729+ return self.get(key,"")
2730+ def int(self,key):
2731+ try:
2732+ return int(self.get(key,"0"))
2733+ except ValueError:
2734+ return 0
2735+class QWebListDict(dict):
2736+ def __init__(self,*p):
2737+ dict.__init__(self,*p)
2738+ def __getitem__(self,key):
2739+ return self.get(key,[])
2740+ def appendlist(self,key,val):
2741+ if self.has_key(key):
2742+ self[key].append(val)
2743+ else:
2744+ self[key]=[val]
2745+ def get_qwebdict(self):
2746+ d=QWebDict()
2747+ for k,v in self.items():
2748+ d[k]=v[-1]
2749+ return d
2750+class QWebRequest:
2751+ """QWebRequest a WSGI request handler.
2752+
2753+ QWebRequest is a WSGI request handler that feature GET, POST and POST
2754+ multipart methods, handles cookies and headers and provide a dict-like
2755+ SESSION Object (either on the filesystem or in memory).
2756+
2757+ It is constructed with the environ and start_response WSGI arguments:
2758+
2759+ req=qweb.QWebRequest(environ, start_response)
2760+
2761+ req has the folowing attributes :
2762+
2763+ req.environ standard WSGI dict (CGI and wsgi ones)
2764+
2765+ Some CGI vars as attributes from environ for convenience:
2766+
2767+ req.SCRIPT_NAME
2768+ req.PATH_INFO
2769+ req.REQUEST_URI
2770+
2771+ Some computed value (also for convenience)
2772+
2773+ req.FULL_URL full URL recontructed (http://host/query)
2774+ req.FULL_PATH (URL path before ?querystring)
2775+
2776+ Dict constructed from querystring and POST datas, PHP-like.
2777+
2778+ req.GET contains GET vars
2779+ req.POST contains POST vars
2780+ req.REQUEST contains merge of GET and POST
2781+ req.FILES contains uploaded files
2782+ req.GET_LIST req.POST_LIST req.REQUEST_LIST req.FILES_LIST multiple arguments versions
2783+ req.debug() returns an HTML dump of those vars
2784+
2785+ A dict-like session object.
2786+
2787+ req.SESSION the session start when the dict is not empty.
2788+
2789+ Attribute for handling the response
2790+
2791+ req.response_headers dict-like to set headers
2792+ req.response_cookies a SimpleCookie to set cookies
2793+ req.response_status a string to set the status like '200 OK'
2794+
2795+ req.write() to write to the buffer
2796+
2797+ req itselfs is an iterable object with the buffer, it will also also call
2798+ start_response automatically before returning anything via the iterator.
2799+
2800+ To make it short, it means that you may use
2801+
2802+ return req
2803+
2804+ at the end of your request handling to return the reponse to any WSGI
2805+ application server.
2806+ """
2807+ #
2808+ # This class contains part ripped from colubrid (with the permission of
2809+ # mitsuhiko) see http://wsgiarea.pocoo.org/colubrid/
2810+ #
2811+ # - the class HttpHeaders
2812+ # - the method load_post_data (tuned version)
2813+ #
2814+ class HttpHeaders(object):
2815+ def __init__(self):
2816+ self.data = [('Content-Type', 'text/html')]
2817+ def __setitem__(self, key, value):
2818+ self.set(key, value)
2819+ def __delitem__(self, key):
2820+ self.remove(key)
2821+ def __contains__(self, key):
2822+ key = key.lower()
2823+ for k, v in self.data:
2824+ if k.lower() == key:
2825+ return True
2826+ return False
2827+ def add(self, key, value):
2828+ self.data.append((key, value))
2829+ def remove(self, key, count=-1):
2830+ removed = 0
2831+ data = []
2832+ for _key, _value in self.data:
2833+ if _key.lower() != key.lower():
2834+ if count > -1:
2835+ if removed >= count:
2836+ break
2837+ else:
2838+ removed += 1
2839+ data.append((_key, _value))
2840+ self.data = data
2841+ def clear(self):
2842+ self.data = []
2843+ def set(self, key, value):
2844+ self.remove(key)
2845+ self.add(key, value)
2846+ def get(self, key=False, httpformat=False):
2847+ if not key:
2848+ result = self.data
2849+ else:
2850+ result = []
2851+ for _key, _value in self.data:
2852+ if _key.lower() == key.lower():
2853+ result.append((_key, _value))
2854+ if httpformat:
2855+ return '\n'.join(['%s: %s' % item for item in result])
2856+ return result
2857+ def load_post_data(self,environ,POST,FILES):
2858+ length = int(environ['CONTENT_LENGTH'])
2859+ DATA = environ['wsgi.input'].read(length)
2860+ if environ.get('CONTENT_TYPE', '').startswith('multipart'):
2861+ lines = ['Content-Type: %s' % environ.get('CONTENT_TYPE', '')]
2862+ for key, value in environ.items():
2863+ if key.startswith('HTTP_'):
2864+ lines.append('%s: %s' % (key, value))
2865+ raw = '\r\n'.join(lines) + '\r\n\r\n' + DATA
2866+ msg = email.message_from_string(raw)
2867+ for sub in msg.get_payload():
2868+ if not isinstance(sub, email.Message.Message):
2869+ continue
2870+ name_dict = cgi.parse_header(sub['Content-Disposition'])[1]
2871+ if 'filename' in name_dict:
2872+ # Nested MIME Messages are not supported'
2873+ if type([]) == type(sub.get_payload()):
2874+ continue
2875+ if not name_dict['filename'].strip():
2876+ continue
2877+ filename = name_dict['filename']
2878+ # why not keep all the filename? because IE always send 'C:\documents and settings\blub\blub.png'
2879+ filename = filename[filename.rfind('\\') + 1:]
2880+ if 'Content-Type' in sub:
2881+ content_type = sub['Content-Type']
2882+ else:
2883+ content_type = None
2884+ s = { "name":filename, "type":content_type, "data":sub.get_payload() }
2885+ FILES.appendlist(name_dict['name'], s)
2886+ else:
2887+ POST.appendlist(name_dict['name'], sub.get_payload())
2888+ else:
2889+ POST.update(cgi.parse_qs(DATA,keep_blank_values=1))
2890+ return DATA
2891+
2892+ def __init__(self,environ,start_response,session=QWebSession):
2893+ self.environ=environ
2894+ self.start_response=start_response
2895+ self.buffer=[]
2896+
2897+ self.SCRIPT_NAME = environ.get('SCRIPT_NAME', '')
2898+ self.PATH_INFO = environ.get('PATH_INFO', '')
2899+ # extensions:
2900+ self.FULL_URL = environ['FULL_URL'] = self.get_full_url(environ)
2901+ # REQUEST_URI is optional, fake it if absent
2902+ if not environ.has_key("REQUEST_URI"):
2903+ environ["REQUEST_URI"]=urllib.quote(self.SCRIPT_NAME+self.PATH_INFO)
2904+ if environ.get('QUERY_STRING'):
2905+ environ["REQUEST_URI"]+='?'+environ['QUERY_STRING']
2906+ self.REQUEST_URI = environ["REQUEST_URI"]
2907+ # full quote url path before the ?
2908+ self.FULL_PATH = environ['FULL_PATH'] = self.REQUEST_URI.split('?')[0]
2909+
2910+ self.request_cookies=Cookie.SimpleCookie()
2911+ self.request_cookies.load(environ.get('HTTP_COOKIE', ''))
2912+
2913+ self.response_started=False
2914+ self.response_gzencode=False
2915+ self.response_cookies=Cookie.SimpleCookie()
2916+ # to delete a cookie use: c[key]['expires'] = datetime.datetime(1970, 1, 1)
2917+ self.response_headers=self.HttpHeaders()
2918+ self.response_status="200 OK"
2919+
2920+ self.php=None
2921+ if self.environ.has_key("php"):
2922+ self.php=environ["php"]
2923+ self.SESSION=self.php._SESSION
2924+ self.GET=self.php._GET
2925+ self.POST=self.php._POST
2926+ self.REQUEST=self.php._ARG
2927+ self.FILES=self.php._FILES
2928+ else:
2929+ if isinstance(session,QWebSession):
2930+ self.SESSION=session
2931+ elif session:
2932+ self.SESSION=session(environ)
2933+ else:
2934+ self.SESSION=None
2935+ self.GET_LIST=QWebListDict(cgi.parse_qs(environ.get('QUERY_STRING', ''),keep_blank_values=1))
2936+ self.POST_LIST=QWebListDict()
2937+ self.FILES_LIST=QWebListDict()
2938+ self.REQUEST_LIST=QWebListDict(self.GET_LIST)
2939+ if environ['REQUEST_METHOD'] == 'POST':
2940+ self.DATA=self.load_post_data(environ,self.POST_LIST,self.FILES_LIST)
2941+ self.REQUEST_LIST.update(self.POST_LIST)
2942+ self.GET=self.GET_LIST.get_qwebdict()
2943+ self.POST=self.POST_LIST.get_qwebdict()
2944+ self.FILES=self.FILES_LIST.get_qwebdict()
2945+ self.REQUEST=self.REQUEST_LIST.get_qwebdict()
2946+ def get_full_url(environ):
2947+ # taken from PEP 333
2948+ if 'FULL_URL' in environ:
2949+ return environ['FULL_URL']
2950+ url = environ['wsgi.url_scheme']+'://'
2951+ if environ.get('HTTP_HOST'):
2952+ url += environ['HTTP_HOST']
2953+ else:
2954+ url += environ['SERVER_NAME']
2955+ if environ['wsgi.url_scheme'] == 'https':
2956+ if environ['SERVER_PORT'] != '443':
2957+ url += ':' + environ['SERVER_PORT']
2958+ else:
2959+ if environ['SERVER_PORT'] != '80':
2960+ url += ':' + environ['SERVER_PORT']
2961+ if environ.has_key('REQUEST_URI'):
2962+ url += environ['REQUEST_URI']
2963+ else:
2964+ url += urllib.quote(environ.get('SCRIPT_NAME', ''))
2965+ url += urllib.quote(environ.get('PATH_INFO', ''))
2966+ if environ.get('QUERY_STRING'):
2967+ url += '?' + environ['QUERY_STRING']
2968+ return url
2969+ get_full_url=staticmethod(get_full_url)
2970+ def save_files(self):
2971+ for k,v in self.FILES.items():
2972+ if not v.has_key("tmp_file"):
2973+ f=tempfile.NamedTemporaryFile()
2974+ f.write(v["data"])
2975+ f.flush()
2976+ v["tmp_file"]=f
2977+ v["tmp_name"]=f.name
2978+ def debug(self):
2979+ body=''
2980+ for name,d in [
2981+ ("GET",self.GET), ("POST",self.POST), ("REQUEST",self.REQUEST), ("FILES",self.FILES),
2982+ ("GET_LIST",self.GET_LIST), ("POST_LIST",self.POST_LIST), ("REQUEST_LIST",self.REQUEST_LIST), ("FILES_LIST",self.FILES_LIST),
2983+ ("SESSION",self.SESSION), ("environ",self.environ),
2984+ ]:
2985+ body+='<table border="1" width="100%" align="center">\n'
2986+ body+='<tr><th colspan="2" align="center">%s</th></tr>\n'%name
2987+ keys=d.keys()
2988+ keys.sort()
2989+ body+=''.join(['<tr><td>%s</td><td>%s</td></tr>\n'%(k,cgi.escape(repr(d[k]))) for k in keys])
2990+ body+='</table><br><br>\n\n'
2991+ return body
2992+ def write(self,s):
2993+ self.buffer.append(s)
2994+ def echo(self,*s):
2995+ self.buffer.extend([str(i) for i in s])
2996+ def response(self):
2997+ if not self.response_started:
2998+ if not self.php:
2999+ for k,v in self.FILES.items():
3000+ if v.has_key("tmp_file"):
3001+ try:
3002+ v["tmp_file"].close()
3003+ except OSError:
3004+ pass
3005+ if self.response_gzencode and self.environ.get('HTTP_ACCEPT_ENCODING','').find('gzip')!=-1:
3006+ zbuf=StringIO.StringIO()
3007+ zfile=gzip.GzipFile(mode='wb', fileobj=zbuf)
3008+ zfile.write(''.join(self.buffer))
3009+ zfile.close()
3010+ zbuf=zbuf.getvalue()
3011+ self.buffer=[zbuf]
3012+ self.response_headers['Content-Encoding']="gzip"
3013+ self.response_headers['Content-Length']=str(len(zbuf))
3014+ headers = self.response_headers.get()
3015+ if isinstance(self.SESSION, QWebSession):
3016+ headers.extend(self.SESSION.session_get_headers())
3017+ headers.extend([('Set-Cookie', self.response_cookies[i].OutputString()) for i in self.response_cookies])
3018+ self.start_response(self.response_status, headers)
3019+ self.response_started=True
3020+ return self.buffer
3021+ def __iter__(self):
3022+ return self.response().__iter__()
3023+ def http_redirect(self,url,permanent=1):
3024+ if permanent:
3025+ self.response_status="301 Moved Permanently"
3026+ else:
3027+ self.response_status="302 Found"
3028+ self.response_headers["Location"]=url
3029+ def http_404(self,msg="<h1>404 Not Found</h1>"):
3030+ self.response_status="404 Not Found"
3031+ if msg:
3032+ self.write(msg)
3033+ def http_download(self,fname,fstr,partial=0):
3034+# allow fstr to be a file-like object
3035+# if parital:
3036+# say accept ranages
3037+# parse range headers...
3038+# if range:
3039+# header("HTTP/1.1 206 Partial Content");
3040+# header("Content-Range: bytes $offset-".($fsize-1)."/".$fsize);
3041+# header("Content-Length: ".($fsize-$offset));
3042+# fseek($fd,$offset);
3043+# else:
3044+ self.response_headers["Content-Type"]="application/octet-stream"
3045+ self.response_headers["Content-Disposition"]="attachment; filename=\"%s\""%fname
3046+ self.response_headers["Content-Transfer-Encoding"]="binary"
3047+ self.response_headers["Content-Length"]="%d"%len(fstr)
3048+ self.write(fstr)
3049+
3050+#----------------------------------------------------------
3051+# QWeb WSGI HTTP Server to run any WSGI app
3052+# autorun, run an app as FCGI or CGI otherwise launch the server
3053+#----------------------------------------------------------
3054+class QWebWSGIHandler(BaseHTTPServer.BaseHTTPRequestHandler):
3055+ def log_message(self,*p):
3056+ if self.server.log:
3057+ return BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,*p)
3058+ def address_string(self):
3059+ return self.client_address[0]
3060+ def start_response(self,status,headers):
3061+ l=status.split(' ',1)
3062+ self.send_response(int(l[0]),l[1])
3063+ ctype_sent=0
3064+ for i in headers:
3065+ if i[0].lower()=="content-type":
3066+ ctype_sent=1
3067+ self.send_header(*i)
3068+ if not ctype_sent:
3069+ self.send_header("Content-type", "text/html")
3070+ self.end_headers()
3071+ return self.write
3072+ def write(self,data):
3073+ try:
3074+ self.wfile.write(data)
3075+ except (socket.error, socket.timeout),e:
3076+ print e
3077+ def bufferon(self):
3078+ if not getattr(self,'wfile_buf',0):
3079+ self.wfile_buf=1
3080+ self.wfile_bak=self.wfile
3081+ self.wfile=StringIO.StringIO()
3082+ def bufferoff(self):
3083+ if self.wfile_buf:
3084+ buf=self.wfile
3085+ self.wfile=self.wfile_bak
3086+ self.write(buf.getvalue())
3087+ self.wfile_buf=0
3088+ def serve(self,type):
3089+ path_info, parameters, query = urlparse.urlparse(self.path)[2:5]
3090+ environ = {
3091+ 'wsgi.version': (1,0),
3092+ 'wsgi.url_scheme': 'http',
3093+ 'wsgi.input': self.rfile,
3094+ 'wsgi.errors': sys.stderr,
3095+ 'wsgi.multithread': 0,
3096+ 'wsgi.multiprocess': 0,
3097+ 'wsgi.run_once': 0,
3098+ 'REQUEST_METHOD': self.command,
3099+ 'SCRIPT_NAME': '',
3100+ 'QUERY_STRING': query,
3101+ 'CONTENT_TYPE': self.headers.get('Content-Type', ''),
3102+ 'CONTENT_LENGTH': self.headers.get('Content-Length', ''),
3103+ 'REMOTE_ADDR': self.client_address[0],
3104+ 'REMOTE_PORT': str(self.client_address[1]),
3105+ 'SERVER_NAME': self.server.server_address[0],
3106+ 'SERVER_PORT': str(self.server.server_address[1]),
3107+ 'SERVER_PROTOCOL': self.request_version,
3108+ # extention
3109+ 'FULL_PATH': self.path,
3110+ 'qweb.mode': 'standalone',
3111+ }
3112+ if path_info:
3113+ environ['PATH_INFO'] = urllib.unquote(path_info)
3114+ for key, value in self.headers.items():
3115+ environ['HTTP_' + key.upper().replace('-', '_')] = value
3116+ # Hack to avoid may TCP packets
3117+ self.bufferon()
3118+ appiter=self.server.wsgiapp(environ, self.start_response)
3119+ for data in appiter:
3120+ self.write(data)
3121+ self.bufferoff()
3122+ self.bufferoff()
3123+ def do_GET(self):
3124+ self.serve('GET')
3125+ def do_POST(self):
3126+ self.serve('GET')
3127+class QWebWSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
3128+ """ QWebWSGIServer
3129+ qweb_wsgi_autorun(wsgiapp,ip='127.0.0.1',port=8080,threaded=1)
3130+ A WSGI HTTP server threaded or not and a function to automatically run your
3131+ app according to the environement (either standalone, CGI or FastCGI).
3132+
3133+ This feature is called QWeb autorun. If you want to To use it on your
3134+ application use the following lines at the end of the main application
3135+ python file:
3136+
3137+ if __name__ == '__main__':
3138+ qweb.qweb_wsgi_autorun(your_wsgi_app)
3139+
3140+ this function will select the approriate running mode according to the
3141+ calling environement (http-server, FastCGI or CGI).
3142+ """
3143+ def __init__(self, wsgiapp, ip, port, threaded=1, log=1):
3144+ BaseHTTPServer.HTTPServer.__init__(self, (ip, port), QWebWSGIHandler)
3145+ self.wsgiapp = wsgiapp
3146+ self.threaded = threaded
3147+ self.log = log
3148+ def process_request(self,*p):
3149+ if self.threaded:
3150+ return SocketServer.ThreadingMixIn.process_request(self,*p)
3151+ else:
3152+ return BaseHTTPServer.HTTPServer.process_request(self,*p)
3153+def qweb_wsgi_autorun(wsgiapp,ip='127.0.0.1',port=8080,threaded=1,log=1,callback_ready=None):
3154+ if sys.platform=='win32':
3155+ fcgi=0
3156+ else:
3157+ fcgi=1
3158+ sock = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM)
3159+ try:
3160+ sock.getpeername()
3161+ except socket.error, e:
3162+ if e[0] == errno.ENOTSOCK:
3163+ fcgi=0
3164+ if fcgi or os.environ.has_key('REQUEST_METHOD'):
3165+ import fcgi
3166+ fcgi.WSGIServer(wsgiapp,multithreaded=False).run()
3167+ else:
3168+ if log:
3169+ print 'Serving on %s:%d'%(ip,port)
3170+ s=QWebWSGIServer(wsgiapp,ip=ip,port=port,threaded=threaded,log=log)
3171+ if callback_ready:
3172+ callback_ready()
3173+ try:
3174+ s.serve_forever()
3175+ except KeyboardInterrupt,e:
3176+ sys.excepthook(*sys.exc_info())
3177+
3178+#----------------------------------------------------------
3179+# Qweb Documentation
3180+#----------------------------------------------------------
3181+def qweb_doc():
3182+ body=__doc__
3183+ for i in [QWebXml ,QWebHtml ,QWebForm ,QWebURL ,qweb_control ,QWebRequest ,QWebSession ,QWebWSGIServer ,qweb_wsgi_autorun]:
3184+ n=i.__name__
3185+ d=i.__doc__
3186+ body+='\n\n%s\n%s\n\n%s'%(n,'-'*len(n),d)
3187+ return body
3188+
3189+ print qweb_doc()
3190+
3191+#
3192
3193=== added file 'tools/ajaxterm/sarissa.js'
3194--- tools/ajaxterm/sarissa.js 1970-01-01 00:00:00 +0000
3195+++ tools/ajaxterm/sarissa.js 2011-01-12 03:17:16 +0000
3196@@ -0,0 +1,647 @@
3197+/**
3198+ * ====================================================================
3199+ * About
3200+ * ====================================================================
3201+ * Sarissa is an ECMAScript library acting as a cross-browser wrapper for native XML APIs.
3202+ * The library supports Gecko based browsers like Mozilla and Firefox,
3203+ * Internet Explorer (5.5+ with MSXML3.0+), Konqueror, Safari and a little of Opera
3204+ * @version 0.9.6.1
3205+ * @author: Manos Batsis, mailto: mbatsis at users full stop sourceforge full stop net
3206+ * ====================================================================
3207+ * Licence
3208+ * ====================================================================
3209+ * This program is free software; you can redistribute it and/or modify
3210+ * it under the terms of the GNU General Public License version 2 or
3211+ * the GNU Lesser General Public License version 2.1 as published by
3212+ * the Free Software Foundation (your choice between the two).
3213+ *
3214+ * This program is distributed in the hope that it will be useful,
3215+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3216+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3217+ * GNU General Public License or GNU Lesser General Public License for more details.
3218+ *
3219+ * You should have received a copy of the GNU General Public License
3220+ * or GNU Lesser General Public License along with this program; if not,
3221+ * write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
3222+ * or visit http://www.gnu.org
3223+ *
3224+ */
3225+/**
3226+ * <p>Sarissa is a utility class. Provides "static" methods for DOMDocument and
3227+ * XMLHTTP objects, DOM Node serializatrion to XML strings and other goodies.</p>
3228+ * @constructor
3229+ */
3230+function Sarissa(){};
3231+/** @private */
3232+Sarissa.PARSED_OK = "Document contains no parsing errors";
3233+/**
3234+ * Tells you whether transformNode and transformNodeToObject are available. This functionality
3235+ * is contained in sarissa_ieemu_xslt.js and is deprecated. If you want to control XSLT transformations
3236+ * use the XSLTProcessor
3237+ * @deprecated
3238+ * @type boolean
3239+ */
3240+Sarissa.IS_ENABLED_TRANSFORM_NODE = false;
3241+/**
3242+ * tells you whether XMLHttpRequest (or equivalent) is available
3243+ * @type boolean
3244+ */
3245+Sarissa.IS_ENABLED_XMLHTTP = false;
3246+/**
3247+ * tells you whether selectNodes/selectSingleNode is available
3248+ * @type boolean
3249+ */
3250+Sarissa.IS_ENABLED_SELECT_NODES = false;
3251+var _sarissa_iNsCounter = 0;
3252+var _SARISSA_IEPREFIX4XSLPARAM = "";
3253+var _SARISSA_HAS_DOM_IMPLEMENTATION = document.implementation && true;
3254+var _SARISSA_HAS_DOM_CREATE_DOCUMENT = _SARISSA_HAS_DOM_IMPLEMENTATION && document.implementation.createDocument;
3255+var _SARISSA_HAS_DOM_FEATURE = _SARISSA_HAS_DOM_IMPLEMENTATION && document.implementation.hasFeature;
3256+var _SARISSA_IS_MOZ = _SARISSA_HAS_DOM_CREATE_DOCUMENT && _SARISSA_HAS_DOM_FEATURE;
3257+var _SARISSA_IS_SAFARI = (navigator.userAgent && navigator.vendor && (navigator.userAgent.toLowerCase().indexOf("applewebkit") != -1 || navigator.vendor.indexOf("Apple") != -1));
3258+var _SARISSA_IS_IE = document.all && window.ActiveXObject && navigator.userAgent.toLowerCase().indexOf("msie") > -1 && navigator.userAgent.toLowerCase().indexOf("opera") == -1;
3259+if(!window.Node || !window.Node.ELEMENT_NODE){
3260+ 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};
3261+};
3262+
3263+// IE initialization
3264+if(_SARISSA_IS_IE){
3265+ // for XSLT parameter names, prefix needed by IE
3266+ _SARISSA_IEPREFIX4XSLPARAM = "xsl:";
3267+ // used to store the most recent ProgID available out of the above
3268+ var _SARISSA_DOM_PROGID = "";
3269+ var _SARISSA_XMLHTTP_PROGID = "";
3270+ /**
3271+ * Called when the Sarissa_xx.js file is parsed, to pick most recent
3272+ * ProgIDs for IE, then gets destroyed.
3273+ * @param idList an array of MSXML PROGIDs from which the most recent will be picked for a given object
3274+ * @param enabledList an array of arrays where each array has two items; the index of the PROGID for which a certain feature is enabled
3275+ */
3276+ pickRecentProgID = function (idList, enabledList){
3277+ // found progID flag
3278+ var bFound = false;
3279+ for(var i=0; i < idList.length && !bFound; i++){
3280+ try{
3281+ var oDoc = new ActiveXObject(idList[i]);
3282+ o2Store = idList[i];
3283+ bFound = true;
3284+ for(var j=0;j<enabledList.length;j++)
3285+ if(i <= enabledList[j][1])
3286+ Sarissa["IS_ENABLED_"+enabledList[j][0]] = true;
3287+ }catch (objException){
3288+ // trap; try next progID
3289+ };
3290+ };
3291+ if (!bFound)
3292+ throw "Could not retreive a valid progID of Class: " + idList[idList.length-1]+". (original exception: "+e+")";
3293+ idList = null;
3294+ return o2Store;
3295+ };
3296+ // pick best available MSXML progIDs
3297+ _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]]);
3298+ _SARISSA_XMLHTTP_PROGID = pickRecentProgID(["Msxml2.XMLHTTP.5.0", "Msxml2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP"], [["XMLHTTP", 4]]);
3299+ _SARISSA_THREADEDDOM_PROGID = pickRecentProgID(["Msxml2.FreeThreadedDOMDocument.5.0", "MSXML2.FreeThreadedDOMDocument.4.0", "MSXML2.FreeThreadedDOMDocument.3.0"]);
3300+ _SARISSA_XSLTEMPLATE_PROGID = pickRecentProgID(["Msxml2.XSLTemplate.5.0", "Msxml2.XSLTemplate.4.0", "MSXML2.XSLTemplate.3.0"], [["XSLTPROC", 2]]);
3301+ // we dont need this anymore
3302+ pickRecentProgID = null;
3303+ //============================================
3304+ // Factory methods (IE)
3305+ //============================================
3306+ // see non-IE version
3307+ Sarissa.getDomDocument = function(sUri, sName){
3308+ var oDoc = new ActiveXObject(_SARISSA_DOM_PROGID);
3309+ // if a root tag name was provided, we need to load it in the DOM
3310+ // object
3311+ if (sName){
3312+ // if needed, create an artifical namespace prefix the way Moz
3313+ // does
3314+ if (sUri){
3315+ oDoc.loadXML("<a" + _sarissa_iNsCounter + ":" + sName + " xmlns:a" + _sarissa_iNsCounter + "=\"" + sUri + "\" />");
3316+ // don't use the same prefix again
3317+ ++_sarissa_iNsCounter;
3318+ }
3319+ else
3320+ oDoc.loadXML("<" + sName + "/>");
3321+ };
3322+ return oDoc;
3323+ };
3324+ // see non-IE version
3325+ Sarissa.getParseErrorText = function (oDoc) {
3326+ var parseErrorText = Sarissa.PARSED_OK;
3327+ if(oDoc.parseError != 0){
3328+ parseErrorText = "XML Parsing Error: " + oDoc.parseError.reason +
3329+ "\nLocation: " + oDoc.parseError.url +
3330+ "\nLine Number " + oDoc.parseError.line + ", Column " +
3331+ oDoc.parseError.linepos +
3332+ ":\n" + oDoc.parseError.srcText +
3333+ "\n";
3334+ for(var i = 0; i < oDoc.parseError.linepos;i++){
3335+ parseErrorText += "-";
3336+ };
3337+ parseErrorText += "^\n";
3338+ };
3339+ return parseErrorText;
3340+ };
3341+ // see non-IE version
3342+ Sarissa.setXpathNamespaces = function(oDoc, sNsSet) {
3343+ oDoc.setProperty("SelectionLanguage", "XPath");
3344+ oDoc.setProperty("SelectionNamespaces", sNsSet);
3345+ };
3346+ /**
3347+ * Basic implementation of Mozilla's XSLTProcessor for IE.
3348+ * Reuses the same XSLT stylesheet for multiple transforms
3349+ * @constructor
3350+ */
3351+ XSLTProcessor = function(){
3352+ this.template = new ActiveXObject(_SARISSA_XSLTEMPLATE_PROGID);
3353+ this.processor = null;
3354+ };
3355+ /**
3356+ * Impoprts the given XSLT DOM and compiles it to a reusable transform
3357+ * @argument xslDoc The XSLT DOMDocument to import
3358+ */
3359+ XSLTProcessor.prototype.importStylesheet = function(xslDoc){
3360+ // convert stylesheet to free threaded
3361+ var converted = new ActiveXObject(_SARISSA_THREADEDDOM_PROGID);
3362+ converted.loadXML(xslDoc.xml);
3363+ this.template.stylesheet = converted;
3364+ this.processor = this.template.createProcessor();
3365+ // (re)set default param values
3366+ this.paramsSet = new Array();
3367+ };
3368+ /**
3369+ * Transform the given XML DOM
3370+ * @argument sourceDoc The XML DOMDocument to transform
3371+ * @return The transformation result as a DOM Document
3372+ */
3373+ XSLTProcessor.prototype.transformToDocument = function(sourceDoc){
3374+ this.processor.input = sourceDoc;
3375+ var outDoc = new ActiveXObject(_SARISSA_DOM_PROGID);
3376+ this.processor.output = outDoc;
3377+ this.processor.transform();
3378+ return outDoc;
3379+ };
3380+ /**
3381+ * Set global XSLT parameter of the imported stylesheet
3382+ * @argument nsURI The parameter namespace URI
3383+ * @argument name The parameter base name
3384+ * @argument value The new parameter value
3385+ */
3386+ XSLTProcessor.prototype.setParameter = function(nsURI, name, value){
3387+ /* nsURI is optional but cannot be null */
3388+ if(nsURI){
3389+ this.processor.addParameter(name, value, nsURI);
3390+ }else{
3391+ this.processor.addParameter(name, value);
3392+ };
3393+ /* update updated params for getParameter */
3394+ if(!this.paramsSet[""+nsURI]){
3395+ this.paramsSet[""+nsURI] = new Array();
3396+ };
3397+ this.paramsSet[""+nsURI][name] = value;
3398+ };
3399+ /**
3400+ * Gets a parameter if previously set by setParameter. Returns null
3401+ * otherwise
3402+ * @argument name The parameter base name
3403+ * @argument value The new parameter value
3404+ * @return The parameter value if reviously set by setParameter, null otherwise
3405+ */
3406+ XSLTProcessor.prototype.getParameter = function(nsURI, name){
3407+ nsURI = nsURI || "";
3408+ if(nsURI in this.paramsSet && name in this.paramsSet[nsURI]){
3409+ return this.paramsSet[nsURI][name];
3410+ }else{
3411+ return null;
3412+ };
3413+ };
3414+}
3415+else{ /* end IE initialization, try to deal with real browsers now ;-) */
3416+ if(_SARISSA_HAS_DOM_CREATE_DOCUMENT){
3417+ /**
3418+ * <p>Ensures the document was loaded correctly, otherwise sets the
3419+ * parseError to -1 to indicate something went wrong. Internal use</p>
3420+ * @private
3421+ */
3422+ Sarissa.__handleLoad__ = function(oDoc){
3423+ if (!oDoc.documentElement || oDoc.documentElement.tagName == "parsererror")
3424+ oDoc.parseError = -1;
3425+ Sarissa.__setReadyState__(oDoc, 4);
3426+ };
3427+ /**
3428+ * <p>Attached by an event handler to the load event. Internal use.</p>
3429+ * @private
3430+ */
3431+ _sarissa_XMLDocument_onload = function(){
3432+ Sarissa.__handleLoad__(this);
3433+ };
3434+ /**
3435+ * <p>Sets the readyState property of the given DOM Document object.
3436+ * Internal use.</p>
3437+ * @private
3438+ * @argument oDoc the DOM Document object to fire the
3439+ * readystatechange event
3440+ * @argument iReadyState the number to change the readystate property to
3441+ */
3442+ Sarissa.__setReadyState__ = function(oDoc, iReadyState){
3443+ oDoc.readyState = iReadyState;
3444+ if (oDoc.onreadystatechange != null && typeof oDoc.onreadystatechange == "function")
3445+ oDoc.onreadystatechange();
3446+ };
3447+ Sarissa.getDomDocument = function(sUri, sName){
3448+ var oDoc = document.implementation.createDocument(sUri?sUri:"", sName?sName:"", null);
3449+ oDoc.addEventListener("load", _sarissa_XMLDocument_onload, false);
3450+ return oDoc;
3451+ };
3452+ if(false && window.XMLDocument){
3453+ /**
3454+ * <p>Emulate IE's onreadystatechange attribute</p>
3455+ */
3456+ XMLDocument.prototype.onreadystatechange = null;
3457+ /**
3458+ * <p>Emulates IE's readyState property, which always gives an integer from 0 to 4:</p>
3459+ * <ul><li>1 == LOADING,</li>
3460+ * <li>2 == LOADED,</li>
3461+ * <li>3 == INTERACTIVE,</li>
3462+ * <li>4 == COMPLETED</li></ul>
3463+ */
3464+ XMLDocument.prototype.readyState = 0;
3465+ /**
3466+ * <p>Emulate IE's parseError attribute</p>
3467+ */
3468+ XMLDocument.prototype.parseError = 0;
3469+
3470+ // NOTE: setting async to false will only work with documents
3471+ // called over HTTP (meaning a server), not the local file system,
3472+ // unless you are using Moz 1.4+.
3473+ // BTW the try>catch block is for 1.4; I haven't found a way to check if
3474+ // the property is implemented without
3475+ // causing an error and I dont want to use user agent stuff for that...
3476+ var _SARISSA_SYNC_NON_IMPLEMENTED = false;// ("async" in XMLDocument.prototype) ? false: true;
3477+ /**
3478+ * <p>Keeps a handle to the original load() method. Internal use and only
3479+ * if Mozilla version is lower than 1.4</p>
3480+ * @private
3481+ */
3482+ XMLDocument.prototype._sarissa_load = XMLDocument.prototype.load;
3483+
3484+ /**
3485+ * <p>Overrides the original load method to provide synchronous loading for
3486+ * Mozilla versions prior to 1.4, using an XMLHttpRequest object (if
3487+ * async is set to false)</p>
3488+ * @returns the DOM Object as it was before the load() call (may be empty)
3489+ */
3490+ XMLDocument.prototype.load = function(sURI) {
3491+ var oDoc = document.implementation.createDocument("", "", null);
3492+ Sarissa.copyChildNodes(this, oDoc);
3493+ this.parseError = 0;
3494+ Sarissa.__setReadyState__(this, 1);
3495+ try {
3496+ if(this.async == false && _SARISSA_SYNC_NON_IMPLEMENTED) {
3497+ var tmp = new XMLHttpRequest();
3498+ tmp.open("GET", sURI, false);
3499+ tmp.send(null);
3500+ Sarissa.__setReadyState__(this, 2);
3501+ Sarissa.copyChildNodes(tmp.responseXML, this);
3502+ Sarissa.__setReadyState__(this, 3);
3503+ }
3504+ else {
3505+ this._sarissa_load(sURI);
3506+ };
3507+ }
3508+ catch (objException) {
3509+ this.parseError = -1;
3510+ }
3511+ finally {
3512+ if(this.async == false){
3513+ Sarissa.__handleLoad__(this);
3514+ };
3515+ };
3516+ return oDoc;
3517+ };
3518+
3519+
3520+ }//if(window.XMLDocument)
3521+ else if(document.implementation && document.implementation.hasFeature && document.implementation.hasFeature('LS', '3.0')){
3522+ Document.prototype.async = true;
3523+ Document.prototype.onreadystatechange = null;
3524+ Document.prototype.parseError = 0;
3525+ Document.prototype.load = function(sURI) {
3526+ var parser = document.implementation.createLSParser(this.async ? document.implementation.MODE_ASYNCHRONOUS : document.implementation.MODE_SYNCHRONOUS, null);
3527+ if(this.async){
3528+ var self = this;
3529+ parser.addEventListener("load",
3530+ function(e) {
3531+ self.readyState = 4;
3532+ Sarissa.copyChildNodes(e.newDocument, self.documentElement, false);
3533+ self.onreadystatechange.call();
3534+ },
3535+ false);
3536+ };
3537+ try {
3538+ var oDoc = parser.parseURI(sURI);
3539+ }
3540+ catch(e){
3541+ this.parseError = -1;
3542+ };
3543+ if(!this.async)
3544+ Sarissa.copyChildNodes(oDoc, this.documentElement, false);
3545+ return oDoc;
3546+ };
3547+ /**
3548+ * <p>Factory method to obtain a new DOM Document object</p>
3549+ * @argument sUri the namespace of the root node (if any)
3550+ * @argument sUri the local name of the root node (if any)
3551+ * @returns a new DOM Document
3552+ */
3553+ Sarissa.getDomDocument = function(sUri, sName){
3554+ return document.implementation.createDocument(sUri?sUri:"", sName?sName:"", null);
3555+ };
3556+ };
3557+ };//if(_SARISSA_HAS_DOM_CREATE_DOCUMENT)
3558+};
3559+//==========================================
3560+// Common stuff
3561+//==========================================
3562+if(!window.DOMParser){
3563+ /*
3564+ * DOMParser is a utility class, used to construct DOMDocuments from XML strings
3565+ * @constructor
3566+ */
3567+ DOMParser = function() {
3568+ };
3569+ if(_SARISSA_IS_SAFARI){
3570+ /**
3571+ * Construct a new DOM Document from the given XMLstring
3572+ * @param sXml the given XML string
3573+ * @param contentType the content type of the document the given string represents (one of text/xml, application/xml, application/xhtml+xml).
3574+ * @return a new DOM Document from the given XML string
3575+ */
3576+ DOMParser.prototype.parseFromString = function(sXml, contentType){
3577+ if(contentType.toLowerCase() != "application/xml"){
3578+ throw "Cannot handle content type: \"" + contentType + "\"";
3579+ };
3580+ var xmlhttp = new XMLHttpRequest();
3581+ xmlhttp.open("GET", "data:text/xml;charset=utf-8," + encodeURIComponent(str), false);
3582+ xmlhttp.send(null);
3583+ return xmlhttp.responseXML;
3584+ };
3585+ }else if(Sarissa.getDomDocument && Sarissa.getDomDocument() && "loadXML" in Sarissa.getDomDocument()){
3586+ DOMParser.prototype.parseFromString = function(sXml, contentType){
3587+ var doc = Sarissa.getDomDocument();
3588+ doc.loadXML(sXml);
3589+ return doc;
3590+ };
3591+ };
3592+};
3593+
3594+if(window.XMLHttpRequest){
3595+ Sarissa.IS_ENABLED_XMLHTTP = true;
3596+}
3597+else if(_SARISSA_IS_IE){
3598+ /**
3599+ * Emulate XMLHttpRequest
3600+ * @constructor
3601+ */
3602+ XMLHttpRequest = function() {
3603+ return new ActiveXObject(_SARISSA_XMLHTTP_PROGID);
3604+ };
3605+ Sarissa.IS_ENABLED_XMLHTTP = true;
3606+};
3607+
3608+if(!window.document.importNode && _SARISSA_IS_IE){
3609+ try{
3610+ /**
3611+ * Implements importNode for the current window document in IE using innerHTML.
3612+ * Testing showed that DOM was multiple times slower than innerHTML for this,
3613+ * sorry folks. If you encounter trouble (who knows what IE does behind innerHTML)
3614+ * please gimme a call.
3615+ * @param oNode the Node to import
3616+ * @param bChildren whether to include the children of oNode
3617+ * @returns the imported node for further use
3618+ */
3619+ window.document.importNode = function(oNode, bChildren){
3620+ var importNode = document.createElement("div");
3621+ if(bChildren)
3622+ importNode.innerHTML = Sarissa.serialize(oNode);
3623+ else
3624+ importNode.innerHTML = Sarissa.serialize(oNode.cloneNode(false));
3625+ return importNode.firstChild;
3626+ };
3627+ }catch(e){};
3628+};
3629+if(!Sarissa.getParseErrorText){
3630+ /**
3631+ * <p>Returns a human readable description of the parsing error. Usefull
3632+ * for debugging. Tip: append the returned error string in a &lt;pre&gt;
3633+ * element if you want to render it.</p>
3634+ * <p>Many thanks to Christian Stocker for the initial patch.</p>
3635+ * @argument oDoc The target DOM document
3636+ * @returns The parsing error description of the target Document in
3637+ * human readable form (preformated text)
3638+ */
3639+ Sarissa.getParseErrorText = function (oDoc){
3640+ var parseErrorText = Sarissa.PARSED_OK;
3641+ if(oDoc && oDoc.parseError && oDoc.parseError != 0){
3642+ /*moz*/
3643+ if(oDoc.documentElement.tagName == "parsererror"){
3644+ parseErrorText = oDoc.documentElement.firstChild.data;
3645+ parseErrorText += "\n" + oDoc.documentElement.firstChild.nextSibling.firstChild.data;
3646+ }/*konq*/
3647+ else{
3648+ parseErrorText = Sarissa.getText(oDoc.documentElement);/*.getElementsByTagName("h1")[0], false) + "\n";
3649+ parseErrorText += Sarissa.getText(oDoc.documentElement.getElementsByTagName("body")[0], false) + "\n";
3650+ parseErrorText += Sarissa.getText(oDoc.documentElement.getElementsByTagName("pre")[0], false);*/
3651+ };
3652+ };
3653+ return parseErrorText;
3654+ };
3655+};
3656+Sarissa.getText = function(oNode, deep){
3657+ var s = "";
3658+ var nodes = oNode.childNodes;
3659+ for(var i=0; i < nodes.length; i++){
3660+ var node = nodes[i];
3661+ var nodeType = node.nodeType;
3662+ if(nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE){
3663+ s += node.data;
3664+ }else if(deep == true
3665+ && (nodeType == Node.ELEMENT_NODE
3666+ || nodeType == Node.DOCUMENT_NODE
3667+ || nodeType == Node.DOCUMENT_FRAGMENT_NODE)){
3668+ s += Sarissa.getText(node, true);
3669+ };
3670+ };
3671+ return s;
3672+};
3673+if(window.XMLSerializer){
3674+ /**
3675+ * <p>Factory method to obtain the serialization of a DOM Node</p>
3676+ * @returns the serialized Node as an XML string
3677+ */
3678+ Sarissa.serialize = function(oDoc){
3679+ var s = null;
3680+ if(oDoc){
3681+ s = oDoc.innerHTML?oDoc.innerHTML:(new XMLSerializer()).serializeToString(oDoc);
3682+ };
3683+ return s;
3684+ };
3685+}else{
3686+ if(Sarissa.getDomDocument && (Sarissa.getDomDocument("","foo", null)).xml){
3687+ // see non-IE version
3688+ Sarissa.serialize = function(oDoc) {
3689+ var s = null;
3690+ if(oDoc){
3691+ s = oDoc.innerHTML?oDoc.innerHTML:oDoc.xml;
3692+ };
3693+ return s;
3694+ };
3695+ /**
3696+ * Utility class to serialize DOM Node objects to XML strings
3697+ * @constructor
3698+ */
3699+ XMLSerializer = function(){};
3700+ /**
3701+ * Serialize the given DOM Node to an XML string
3702+ * @param oNode the DOM Node to serialize
3703+ */
3704+ XMLSerializer.prototype.serializeToString = function(oNode) {
3705+ return oNode.xml;
3706+ };
3707+ };
3708+};
3709+
3710+/**
3711+ * strips tags from a markup string
3712+ */
3713+Sarissa.stripTags = function (s) {
3714+ return s.replace(/<[^>]+>/g,"");
3715+};
3716+/**
3717+ * <p>Deletes all child nodes of the given node</p>
3718+ * @argument oNode the Node to empty
3719+ */
3720+Sarissa.clearChildNodes = function(oNode) {
3721+ // need to check for firstChild due to opera 8 bug with hasChildNodes
3722+ while(oNode.firstChild){
3723+ oNode.removeChild(oNode.firstChild);
3724+ };
3725+};
3726+/**
3727+ * <p> Copies the childNodes of nodeFrom to nodeTo</p>
3728+ * <p> <b>Note:</b> The second object's original content is deleted before
3729+ * the copy operation, unless you supply a true third parameter</p>
3730+ * @argument nodeFrom the Node to copy the childNodes from
3731+ * @argument nodeTo the Node to copy the childNodes to
3732+ * @argument bPreserveExisting whether to preserve the original content of nodeTo, default is false
3733+ */
3734+Sarissa.copyChildNodes = function(nodeFrom, nodeTo, bPreserveExisting) {
3735+ if((!nodeFrom) || (!nodeTo)){
3736+ throw "Both source and destination nodes must be provided";
3737+ };
3738+ if(!bPreserveExisting){
3739+ Sarissa.clearChildNodes(nodeTo);
3740+ };
3741+ var ownerDoc = nodeTo.nodeType == Node.DOCUMENT_NODE ? nodeTo : nodeTo.ownerDocument;
3742+ var nodes = nodeFrom.childNodes;
3743+ if(ownerDoc.importNode && (!_SARISSA_IS_IE)) {
3744+ for(var i=0;i < nodes.length;i++) {
3745+ nodeTo.appendChild(ownerDoc.importNode(nodes[i], true));
3746+ };
3747+ }
3748+ else{
3749+ for(var i=0;i < nodes.length;i++) {
3750+ nodeTo.appendChild(nodes[i].cloneNode(true));
3751+ };
3752+ };
3753+};
3754+
3755+/**
3756+ * <p> Moves the childNodes of nodeFrom to nodeTo</p>
3757+ * <p> <b>Note:</b> The second object's original content is deleted before
3758+ * the move operation, unless you supply a true third parameter</p>
3759+ * @argument nodeFrom the Node to copy the childNodes from
3760+ * @argument nodeTo the Node to copy the childNodes to
3761+ * @argument bPreserveExisting whether to preserve the original content of nodeTo, default is
3762+ */
3763+Sarissa.moveChildNodes = function(nodeFrom, nodeTo, bPreserveExisting) {
3764+ if((!nodeFrom) || (!nodeTo)){
3765+ throw "Both source and destination nodes must be provided";
3766+ };
3767+ if(!bPreserveExisting){
3768+ Sarissa.clearChildNodes(nodeTo);
3769+ };
3770+ var nodes = nodeFrom.childNodes;
3771+ // if within the same doc, just move, else copy and delete
3772+ if(nodeFrom.ownerDocument == nodeTo.ownerDocument){
3773+ while(nodeFrom.firstChild){
3774+ nodeTo.appendChild(nodeFrom.firstChild);
3775+ };
3776+ }else{
3777+ var ownerDoc = nodeTo.nodeType == Node.DOCUMENT_NODE ? nodeTo : nodeTo.ownerDocument;
3778+ if(ownerDoc.importNode && (!_SARISSA_IS_IE)) {
3779+ for(var i=0;i < nodes.length;i++) {
3780+ nodeTo.appendChild(ownerDoc.importNode(nodes[i], true));
3781+ };
3782+ }else{
3783+ for(var i=0;i < nodes.length;i++) {
3784+ nodeTo.appendChild(nodes[i].cloneNode(true));
3785+ };
3786+ };
3787+ Sarissa.clearChildNodes(nodeFrom);
3788+ };
3789+};
3790+
3791+/**
3792+ * <p>Serialize any object to an XML string. All properties are serialized using the property name
3793+ * as the XML element name. Array elements are rendered as <code>array-item</code> elements,
3794+ * using their index/key as the value of the <code>key</code> attribute.</p>
3795+ * @argument anyObject the object to serialize
3796+ * @argument objectName a name for that object
3797+ * @return the XML serializationj of the given object as a string
3798+ */
3799+Sarissa.xmlize = function(anyObject, objectName, indentSpace){
3800+ indentSpace = indentSpace?indentSpace:'';
3801+ var s = indentSpace + '<' + objectName + '>';
3802+ var isLeaf = false;
3803+ if(!(anyObject instanceof Object) || anyObject instanceof Number || anyObject instanceof String
3804+ || anyObject instanceof Boolean || anyObject instanceof Date){
3805+ s += Sarissa.escape(""+anyObject);
3806+ isLeaf = true;
3807+ }else{
3808+ s += "\n";
3809+ var itemKey = '';
3810+ var isArrayItem = anyObject instanceof Array;
3811+ for(var name in anyObject){
3812+ s += Sarissa.xmlize(anyObject[name], (isArrayItem?"array-item key=\""+name+"\"":name), indentSpace + " ");
3813+ };
3814+ s += indentSpace;
3815+ };
3816+ return s += (objectName.indexOf(' ')!=-1?"</array-item>\n":"</" + objectName + ">\n");
3817+};
3818+
3819+/**
3820+ * Escape the given string chacters that correspond to the five predefined XML entities
3821+ * @param sXml the string to escape
3822+ */
3823+Sarissa.escape = function(sXml){
3824+ return sXml.replace(/&/g, "&amp;")
3825+ .replace(/</g, "&lt;")
3826+ .replace(/>/g, "&gt;")
3827+ .replace(/"/g, "&quot;")
3828+ .replace(/'/g, "&apos;");
3829+};
3830+
3831+/**
3832+ * Unescape the given string. This turns the occurences of the predefined XML
3833+ * entities to become the characters they represent correspond to the five predefined XML entities
3834+ * @param sXml the string to unescape
3835+ */
3836+Sarissa.unescape = function(sXml){
3837+ return sXml.replace(/&apos;/g,"'")
3838+ .replace(/&quot;/g,"\"")
3839+ .replace(/&gt;/g,">")
3840+ .replace(/&lt;/g,"<")
3841+ .replace(/&amp;/g,"&");
3842+};
3843+// EOF
3844
3845=== added file 'tools/ajaxterm/sarissa_dhtml.js'
3846--- tools/ajaxterm/sarissa_dhtml.js 1970-01-01 00:00:00 +0000
3847+++ tools/ajaxterm/sarissa_dhtml.js 2011-01-12 03:17:16 +0000
3848@@ -0,0 +1,105 @@
3849+/**
3850+ * ====================================================================
3851+ * About
3852+ * ====================================================================
3853+ * Sarissa cross browser XML library - AJAX module
3854+ * @version 0.9.6.1
3855+ * @author: Copyright Manos Batsis, mailto: mbatsis at users full stop sourceforge full stop net
3856+ *
3857+ * This module contains some convinient AJAX tricks based on Sarissa
3858+ *
3859+ * ====================================================================
3860+ * Licence
3861+ * ====================================================================
3862+ * This program is free software; you can redistribute it and/or modify
3863+ * it under the terms of the GNU General Public License version 2 or
3864+ * the GNU Lesser General Public License version 2.1 as published by
3865+ * the Free Software Foundation (your choice between the two).
3866+ *
3867+ * This program is distributed in the hope that it will be useful,
3868+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3869+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3870+ * GNU General Public License or GNU Lesser General Public License for more details.
3871+ *
3872+ * You should have received a copy of the GNU General Public License
3873+ * or GNU Lesser General Public License along with this program; if not,
3874+ * write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
3875+ * or visit http://www.gnu.org
3876+ *
3877+ */
3878+/**
3879+ * Update an element with response of a GET request on the given URL.
3880+ * @addon
3881+ * @param sFromUrl the URL to make the request to
3882+ * @param oTargetElement the element to update
3883+ * @param xsltproc (optional) the transformer to use on the returned
3884+ * content before updating the target element with it
3885+ */
3886+Sarissa.updateContentFromURI = function(sFromUrl, oTargetElement, xsltproc) {
3887+ try{
3888+ oTargetElement.style.cursor = "wait";
3889+ var xmlhttp = new XMLHttpRequest();
3890+ xmlhttp.open("GET", sFromUrl);
3891+ function sarissa_dhtml_loadHandler() {
3892+ if (xmlhttp.readyState == 4) {
3893+ oTargetElement.style.cursor = "auto";
3894+ Sarissa.updateContentFromNode(xmlhttp.responseXML, oTargetElement, xsltproc);
3895+ };
3896+ };
3897+ xmlhttp.onreadystatechange = sarissa_dhtml_loadHandler;
3898+ xmlhttp.send(null);
3899+ oTargetElement.style.cursor = "auto";
3900+ }
3901+ catch(e){
3902+ oTargetElement.style.cursor = "auto";
3903+ throw e;
3904+ };
3905+};
3906+
3907+/**
3908+ * Update an element's content with the given DOM node.
3909+ * @addon
3910+ * @param sFromUrl the URL to make the request to
3911+ * @param oTargetElement the element to update
3912+ * @param xsltproc (optional) the transformer to use on the given
3913+ * DOM node before updating the target element with it
3914+ */
3915+Sarissa.updateContentFromNode = function(oNode, oTargetElement, xsltproc) {
3916+ try {
3917+ oTargetElement.style.cursor = "wait";
3918+ Sarissa.clearChildNodes(oTargetElement);
3919+ // check for parsing errors
3920+ var ownerDoc = oNode.nodeType == Node.DOCUMENT_NODE?oNode:oNode.ownerDocument;
3921+ if(ownerDoc.parseError && ownerDoc.parseError != 0) {
3922+ var pre = document.createElement("pre");
3923+ pre.appendChild(document.createTextNode(Sarissa.getParseErrorText(ownerDoc)));
3924+ oTargetElement.appendChild(pre);
3925+ }
3926+ else {
3927+ // transform if appropriate
3928+ if(xsltproc) {
3929+ oNode = xsltproc.transformToDocument(oNode);
3930+ };
3931+ // be smart, maybe the user wants to display the source instead
3932+ if(oTargetElement.tagName.toLowerCase == "textarea" || oTargetElement.tagName.toLowerCase == "input") {
3933+ oTargetElement.value = Sarissa.serialize(oNode);
3934+ }
3935+ else {
3936+ // ok that was not smart; it was paranoid. Keep up the good work by trying to use DOM instead of innerHTML
3937+ if(oNode.nodeType == Node.DOCUMENT_NODE || oNode.ownerDocument.documentElement == oNode) {
3938+ oTargetElement.innerHTML = Sarissa.serialize(oNode);
3939+ }
3940+ else{
3941+ oTargetElement.appendChild(oTargetElement.ownerDocument.importNode(oNode, true));
3942+ };
3943+ };
3944+ };
3945+ }
3946+ catch(e) {
3947+ throw e;
3948+ }
3949+ finally{
3950+ oTargetElement.style.cursor = "auto";
3951+ };
3952+};
3953+
3954
3955=== added file 'tools/euca-get-ajax-console'
3956--- tools/euca-get-ajax-console 1970-01-01 00:00:00 +0000
3957+++ tools/euca-get-ajax-console 2011-01-12 03:17:16 +0000
3958@@ -0,0 +1,164 @@
3959+#!/usr/bin/env python
3960+# pylint: disable-msg=C0103
3961+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3962+
3963+# Copyright 2010 United States Government as represented by the
3964+# Administrator of the National Aeronautics and Space Administration.
3965+# All Rights Reserved.
3966+#
3967+# Licensed under the Apache License, Version 2.0 (the "License");
3968+# you may not use this file except in compliance with the License.
3969+# You may obtain a copy of the License at
3970+#
3971+# http://www.apache.org/licenses/LICENSE-2.0
3972+#
3973+# Unless required by applicable law or agreed to in writing, software
3974+# distributed under the License is distributed on an "AS IS" BASIS,
3975+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3976+# See the License for the specific language governing permissions and
3977+# limitations under the License.
3978+
3979+"""Euca add-on to use ajax console"""
3980+
3981+import getopt
3982+import os
3983+import sys
3984+
3985+# If ../nova/__init__.py exists, add ../ to Python search path, so that
3986+# it will override what happens to be installed in /usr/(local/)lib/python...
3987+possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
3988+ os.pardir,
3989+ os.pardir))
3990+if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
3991+ sys.path.insert(0, possible_topdir)
3992+
3993+import boto
3994+import nova
3995+from boto.ec2.connection import EC2Connection
3996+from euca2ools import Euca2ool, InstanceValidationError, Util, ConnectionFailed
3997+
3998+usage_string = """
3999+Retrieves a url to an ajax console terminal
4000+
4001+euca-get-ajax-console [-h, --help] [--version] [--debug] instance_id
4002+
4003+REQUIRED PARAMETERS
4004+
4005+instance_id: unique identifier for the instance show the console output for.
4006+
4007+OPTIONAL PARAMETERS
4008+
4009+"""
4010+
4011+
4012+# This class extends boto to add AjaxConsole functionality
4013+class NovaEC2Connection(EC2Connection):
4014+
4015+ def get_ajax_console(self, instance_id):
4016+ """
4017+ Retrieves a console connection for the specified instance.
4018+
4019+ :type instance_id: string
4020+ :param instance_id: The instance ID of a running instance on the cloud.
4021+
4022+ :rtype: :class:`AjaxConsole`
4023+ """
4024+
4025+ class AjaxConsole:
4026+ def __init__(self, parent=None):
4027+ self.parent = parent
4028+ self.instance_id = None
4029+ self.url = None
4030+
4031+ def startElement(self, name, attrs, connection):
4032+ return None
4033+
4034+ def endElement(self, name, value, connection):
4035+ if name == 'instanceId':
4036+ self.instance_id = value
4037+ elif name == 'url':
4038+ self.url = value
4039+ else:
4040+ setattr(self, name, value)
4041+
4042+ params = {}
4043+ self.build_list_params(params, [instance_id], 'InstanceId')
4044+ return self.get_object('GetAjaxConsole', params, AjaxConsole)
4045+ pass
4046+
4047+
4048+def override_connect_ec2(aws_access_key_id=None,
4049+ aws_secret_access_key=None, **kwargs):
4050+ return NovaEC2Connection(aws_access_key_id,
4051+ aws_secret_access_key, **kwargs)
4052+
4053+# override boto's connect_ec2 method, so that we can use NovaEC2Connection
4054+boto.connect_ec2 = override_connect_ec2
4055+
4056+
4057+def usage(status=1):
4058+ print usage_string
4059+ Util().usage()
4060+ sys.exit(status)
4061+
4062+
4063+def version():
4064+ print Util().version()
4065+ sys.exit()
4066+
4067+
4068+def display_console_output(console_output):
4069+ print console_output.instance_id
4070+ print console_output.timestamp
4071+ print console_output.output
4072+
4073+
4074+def display_ajax_console_output(console_output):
4075+ print console_output.url
4076+
4077+
4078+def main():
4079+ try:
4080+ euca = Euca2ool()
4081+ except Exception, e:
4082+ print e
4083+ usage()
4084+
4085+ instance_id = None
4086+
4087+ for name, value in euca.opts:
4088+ if name in ('-h', '--help'):
4089+ usage(0)
4090+ elif name == '--version':
4091+ version()
4092+ elif name == '--debug':
4093+ debug = True
4094+
4095+ for arg in euca.args:
4096+ instance_id = arg
4097+ break
4098+
4099+ if instance_id:
4100+ try:
4101+ euca.validate_instance_id(instance_id)
4102+ except InstanceValidationError:
4103+ print 'Invalid instance id'
4104+ sys.exit(1)
4105+
4106+ try:
4107+ euca_conn = euca.make_connection()
4108+ except ConnectionFailed, e:
4109+ print e.message
4110+ sys.exit(1)
4111+ try:
4112+ console_output = euca_conn.get_ajax_console(instance_id)
4113+ except Exception, ex:
4114+ euca.display_error_and_exit('%s' % ex)
4115+
4116+ display_ajax_console_output(console_output)
4117+ else:
4118+ print 'instance_id must be specified'
4119+ usage()
4120+
4121+if __name__ == "__main__":
4122+ main()