1
2
3
4 """
5 This file is part of the web2py Web Framework
6 Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
7 License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
8
9 Contains the classes for the global used variables:
10
11 - Request
12 - Response
13 - Session
14
15 """
16
17 from storage import Storage, List
18 from streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE
19 from xmlrpc import handler
20 from contenttype import contenttype
21 from html import xmlescape, TABLE, TR, PRE, URL
22 from http import HTTP, redirect
23 from fileutils import up
24 from serializers import json, custom_json
25 import settings
26 from utils import web2py_uuid
27 from settings import global_settings
28 import hashlib
29 import portalocker
30 import cPickle
31 import cStringIO
32 import datetime
33 import re
34 import Cookie
35 import os
36 import sys
37 import traceback
38 import threading
39
40 try:
41 from gluon.contrib.minify import minify
42 have_minify = True
43 except ImportError:
44 have_minify = False
45
46 regex_session_id = re.compile('^([\w\-]+/)?[\w\-\.]+$')
47
48 __all__ = ['Request', 'Response', 'Session']
49
50 current = threading.local()
51
52 css_template = '<link href="%s" rel="stylesheet" type="text/css" />'
53 js_template = '<script src="%s" type="text/javascript"></script>'
54 css_inline = '<style type="text/css">\n%s\n</style>'
55 js_inline = '<script type="text/javascript">\n%s\n</script>'
56
58
59 """
60 defines the request object and the default values of its members
61
62 - env: environment variables, by gluon.main.wsgibase()
63 - cookies
64 - get_vars
65 - post_vars
66 - vars
67 - folder
68 - application
69 - function
70 - args
71 - extension
72 - now: datetime.datetime.today()
73 - restful()
74 """
75
94
96 self.uuid = '%s/%s.%s.%s' % (
97 self.application,
98 self.client.replace(':', '_'),
99 self.now.strftime('%Y-%m-%d.%H-%M-%S'),
100 web2py_uuid())
101 return self.uuid
102
112
114 """
115 If request comes in over HTTP, redirect it to HTTPS
116 and secure the session.
117 """
118 if not global_settings.cronjob and not self.is_https:
119 redirect(URL(scheme='https', args=self.args, vars=self.vars))
120
121 current.session.secure()
122
124 def wrapper(action,self=self):
125 def f(_action=action,_self=self,*a,**b):
126 self.is_restful = True
127 method = _self.env.request_method
128 if len(_self.args) and '.' in _self.args[-1]:
129 _self.args[-1],_self.extension = _self.args[-1].rsplit('.',1)
130 current.response.headers['Content-Type'] = \
131 contenttype(_self.extension.lower())
132 if not method in ['GET','POST','DELETE','PUT']:
133 raise HTTP(400,"invalid method")
134 rest_action = _action().get(method,None)
135 if not rest_action:
136 raise HTTP(400,"method not supported")
137 try:
138 return rest_action(*_self.args,**_self.vars)
139 except TypeError, e:
140 exc_type, exc_value, exc_traceback = sys.exc_info()
141 if len(traceback.extract_tb(exc_traceback))==1:
142 raise HTTP(400,"invalid arguments")
143 else:
144 raise e
145 f.__doc__ = action.__doc__
146 f.__name__ = action.__name__
147 return f
148 return wrapper
149
150
152
153 """
154 defines the response object and the default values of its members
155 response.write( ) can be used to write in the output html
156 """
157
159 self.status = 200
160 self.headers = Storage()
161 self.headers['X-Powered-By'] = 'web2py'
162 self.body = cStringIO.StringIO()
163 self.session_id = None
164 self.cookies = Cookie.SimpleCookie()
165 self.postprocessing = []
166 self.flash = ''
167 self.meta = Storage()
168 self.menu = []
169 self.files = []
170 self.generic_patterns = []
171 self._vars = None
172 self._caller = lambda f: f()
173 self._view_environment = None
174 self._custom_commit = None
175 self._custom_rollback = None
176
177 - def write(self, data, escape=True):
182
184 from compileapp import run_view_in
185 if len(a) > 2:
186 raise SyntaxError, 'Response.render can be called with two arguments, at most'
187 elif len(a) == 2:
188 (view, self._vars) = (a[0], a[1])
189 elif len(a) == 1 and isinstance(a[0], str):
190 (view, self._vars) = (a[0], {})
191 elif len(a) == 1 and hasattr(a[0], 'read') and callable(a[0].read):
192 (view, self._vars) = (a[0], {})
193 elif len(a) == 1 and isinstance(a[0], dict):
194 (view, self._vars) = (None, a[0])
195 else:
196 (view, self._vars) = (None, {})
197 self._vars.update(b)
198 self._view_environment.update(self._vars)
199 if view:
200 import cStringIO
201 (obody, oview) = (self.body, self.view)
202 (self.body, self.view) = (cStringIO.StringIO(), view)
203 run_view_in(self._view_environment)
204 page = self.body.getvalue()
205 self.body.close()
206 (self.body, self.view) = (obody, oview)
207 else:
208 run_view_in(self._view_environment)
209 page = self.body.getvalue()
210 return page
211
217
219
220
221 """
222 Caching method for writing out files.
223 By default, caches in ram for 5 minutes. To change,
224 response.cache_includes = (cache_method, time_expire).
225 Example: (cache.disk, 60) # caches to disk for 1 minute.
226 """
227 from gluon import URL
228
229 files = []
230 for item in self.files:
231 if not item in files: files.append(item)
232 if have_minify and (self.optimize_css or self.optimize_js):
233
234 cache = self.cache_includes or (current.cache.ram, 60*5)
235 def call_minify():
236 return minify.minify(files,URL('static','temp'),
237 current.request.folder,
238 self.optimize_css,self.optimize_js)
239 if cache:
240 cache_model, time_expire = cache
241 files = cache_model('response.files.minified',call_minify,
242 time_expire)
243 else:
244 files = call_minify()
245 s = ''
246 for item in files:
247 if isinstance(item,str):
248 f = item.lower()
249 if f.endswith('.css'): s += css_template % item
250 elif f.endswith('.js'): s += js_template % item
251 elif isinstance(item,(list,tuple)):
252 f = item[0]
253 if f=='css:inline': s += css_inline % item[1]
254 elif f=='js:inline': s += js_inline % item[1]
255 self.write(s, escape=False)
256
263 """
264 if a controller function::
265
266 return response.stream(file, 100)
267
268 the file content will be streamed at 100 bytes at the time
269 """
270 if not request:
271 request = current.request
272 if isinstance(stream, (str, unicode)):
273 stream_file_or_304_or_206(stream,
274 chunk_size=chunk_size,
275 request=request,
276 headers=self.headers)
277
278
279
280 if hasattr(stream, 'name'):
281 filename = stream.name
282 else:
283 filename = None
284 keys = [item.lower() for item in self.headers]
285 if filename and not 'content-type' in keys:
286 self.headers['Content-Type'] = contenttype(filename)
287 if filename and not 'content-length' in keys:
288 try:
289 self.headers['Content-Length'] = \
290 os.path.getsize(filename)
291 except OSError:
292 pass
293
294
295 if request.is_https and isinstance(request.env.http_user_agent,str) and \
296 not re.search(r'Opera', request.env.http_user_agent) and \
297 re.search(r'MSIE [5-8][^0-9]', request.env.http_user_agent):
298 self.headers['Pragma'] = 'cache'
299 self.headers['Cache-Control'] = 'private'
300
301 if request and request.env.web2py_use_wsgi_file_wrapper:
302 wrapped = request.env.wsgi_file_wrapper(stream, chunk_size)
303 else:
304 wrapped = streamer(stream, chunk_size=chunk_size)
305 return wrapped
306
308 """
309 example of usage in controller::
310
311 def download():
312 return response.download(request, db)
313
314 downloads from http://..../download/filename
315 """
316
317 import contenttype as c
318 if not request.args:
319 raise HTTP(404)
320 name = request.args[-1]
321 items = re.compile('(?P<table>.*?)\.(?P<field>.*?)\..*')\
322 .match(name)
323 if not items:
324 raise HTTP(404)
325 (t, f) = (items.group('table'), items.group('field'))
326 field = db[t][f]
327 try:
328 (filename, stream) = field.retrieve(name)
329 except IOError:
330 raise HTTP(404)
331 self.headers['Content-Type'] = c.contenttype(name)
332 if attachment:
333 self.headers['Content-Disposition'] = \
334 "attachment; filename=%s" % filename
335 return self.stream(stream, chunk_size = chunk_size, request=request)
336
337 - def json(self, data, default=None):
339
340 - def xmlrpc(self, request, methods):
341 """
342 assuming::
343
344 def add(a, b):
345 return a+b
346
347 if a controller function \"func\"::
348
349 return response.xmlrpc(request, [add])
350
351 the controller will be able to handle xmlrpc requests for
352 the add function. Example::
353
354 import xmlrpclib
355 connection = xmlrpclib.ServerProxy('http://hostname/app/contr/func')
356 print connection.add(3, 4)
357
358 """
359
360 return handler(request, self, methods)
361
387
389
390 """
391 defines the session object and the default values of its members (None)
392 """
393
394 - def connect(
395 self,
396 request,
397 response,
398 db=None,
399 tablename='web2py_session',
400 masterapp=None,
401 migrate=True,
402 separate = None,
403 check_client=False,
404 ):
405 """
406 separate can be separate=lambda(session_name): session_name[-2:]
407 and it is used to determine a session prefix.
408 separate can be True and it is set to session_name[-2:]
409 """
410 if separate == True:
411 separate = lambda session_name: session_name[-2:]
412 self._unlock(response)
413 if not masterapp:
414 masterapp = request.application
415 response.session_id_name = 'session_id_%s' % masterapp.lower()
416
417 if not db:
418 if global_settings.db_sessions is True or masterapp in global_settings.db_sessions:
419 return
420 response.session_new = False
421 client = request.client and request.client.replace(':', '.')
422 if response.session_id_name in request.cookies:
423 response.session_id = \
424 request.cookies[response.session_id_name].value
425 if regex_session_id.match(response.session_id):
426 response.session_filename = \
427 os.path.join(up(request.folder), masterapp,
428 'sessions', response.session_id)
429 else:
430 response.session_id = None
431 if response.session_id:
432 try:
433 response.session_file = \
434 open(response.session_filename, 'rb+')
435 try:
436 portalocker.lock(response.session_file,
437 portalocker.LOCK_EX)
438 response.session_locked = True
439 self.update(cPickle.load(response.session_file))
440 response.session_file.seek(0)
441 oc = response.session_filename.split('/')[-1].split('-')[0]
442 if check_client and client!=oc:
443 raise Exception, "cookie attack"
444 finally:
445 pass
446
447
448 except:
449 response.session_id = None
450 if not response.session_id:
451 uuid = web2py_uuid()
452 response.session_id = '%s-%s' % (client, uuid)
453 if separate:
454 prefix = separate(response.session_id)
455 response.session_id = '%s/%s' % (prefix,response.session_id)
456 response.session_filename = \
457 os.path.join(up(request.folder), masterapp,
458 'sessions', response.session_id)
459 response.session_new = True
460 else:
461 if global_settings.db_sessions is not True:
462 global_settings.db_sessions.add(masterapp)
463 response.session_db = True
464 if response.session_file:
465 self._close(response)
466 if settings.global_settings.web2py_runtime_gae:
467
468 request.tickets_db = db
469 if masterapp == request.application:
470 table_migrate = migrate
471 else:
472 table_migrate = False
473 tname = tablename + '_' + masterapp
474 table = db.get(tname, None)
475 if table is None:
476 table = db.define_table(
477 tname,
478 db.Field('locked', 'boolean', default=False),
479 db.Field('client_ip', length=64),
480 db.Field('created_datetime', 'datetime',
481 default=request.now),
482 db.Field('modified_datetime', 'datetime'),
483 db.Field('unique_key', length=64),
484 db.Field('session_data', 'blob'),
485 migrate=table_migrate,
486 )
487 try:
488 key = request.cookies[response.session_id_name].value
489 (record_id, unique_key) = key.split(':')
490 if record_id == '0':
491 raise Exception, 'record_id == 0'
492 rows = db(table.id == record_id).select()
493 if len(rows) == 0 or rows[0].unique_key != unique_key:
494 raise Exception, 'No record'
495
496
497
498 session_data = cPickle.loads(rows[0].session_data)
499 self.update(session_data)
500 except Exception:
501 record_id = None
502 unique_key = web2py_uuid()
503 session_data = {}
504 response._dbtable_and_field = \
505 (response.session_id_name, table, record_id, unique_key)
506 response.session_id = '%s:%s' % (record_id, unique_key)
507 response.cookies[response.session_id_name] = response.session_id
508 response.cookies[response.session_id_name]['path'] = '/'
509 self.__hash = hashlib.md5(str(self)).digest()
510 if self.flash:
511 (response.flash, self.flash) = (self.flash, None)
512
514 if self._start_timestamp:
515 return False
516 else:
517 self._start_timestamp = datetime.datetime.today()
518 return True
519
521 now = datetime.datetime.today()
522 if not self._last_timestamp or \
523 self._last_timestamp + datetime.timedelta(seconds = seconds) > now:
524 self._last_timestamp = now
525 return False
526 else:
527 return True
528
531
532 - def forget(self, response=None):
533 self._close(response)
534 self._forget = True
535
537
538
539 if not response.session_db or not response.session_id or self._forget:
540 return
541
542
543 __hash = self.__hash
544 if __hash is not None:
545 del self.__hash
546 if __hash == hashlib.md5(str(self)).digest():
547 return
548
549 (record_id_name, table, record_id, unique_key) = \
550 response._dbtable_and_field
551 dd = dict(locked=False, client_ip=request.env.remote_addr,
552 modified_datetime=request.now,
553 session_data=cPickle.dumps(dict(self)),
554 unique_key=unique_key)
555 if record_id:
556 table._db(table.id == record_id).update(**dd)
557 else:
558 record_id = table.insert(**dd)
559 response.cookies[response.session_id_name] = '%s:%s'\
560 % (record_id, unique_key)
561 response.cookies[response.session_id_name]['path'] = '/'
562
564
565
566 if response.session_db:
567 return
568
569
570 __hash = self.__hash
571 if __hash is not None:
572 del self.__hash
573 if __hash == hashlib.md5(str(self)).digest():
574 self._close(response)
575 return
576
577 if not response.session_id or self._forget:
578 self._close(response)
579 return
580
581 if response.session_new:
582
583 session_folder = os.path.dirname(response.session_filename)
584 if not os.path.exists(session_folder):
585 os.mkdir(session_folder)
586 response.session_file = open(response.session_filename, 'wb')
587 portalocker.lock(response.session_file, portalocker.LOCK_EX)
588 response.session_locked = True
589
590 if response.session_file:
591 cPickle.dump(dict(self), response.session_file)
592 response.session_file.truncate()
593 self._close(response)
594
596 if response and response.session_file and response.session_locked:
597 try:
598 portalocker.unlock(response.session_file)
599 response.session_locked = False
600 except:
601 pass
602
604 if response and response.session_file:
605 self._unlock(response)
606 try:
607 response.session_file.close()
608 del response.session_file
609 except:
610 pass
611