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