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 gluon.storage import Storage, List
18 from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE
19 from gluon.xmlrpc import handler
20 from gluon.contenttype import contenttype
21 from gluon.html import xmlescape, TABLE, TR, PRE, URL
22 from gluon.http import HTTP, redirect
23 from gluon.fileutils import up
24 from gluon.serializers import json, custom_json
25 import gluon.settings as settings
26 from gluon.utils import web2py_uuid, secure_dumps, secure_loads
27 from gluon.settings import global_settings
28 import hashlib
29 import portalocker
30 import cPickle
31 from pickle import Pickler, MARK, DICT, EMPTY_DICT
32 from types import DictionaryType
33 import cStringIO
34 import datetime
35 import re
36 import Cookie
37 import os
38 import sys
39 import traceback
40 import threading
41 import cgi
42 import copy
43 import tempfile
44 from gluon.cache import CacheInRam
45 from gluon.fileutils import copystream
46
47 FMT = '%a, %d-%b-%Y %H:%M:%S PST'
48 PAST = 'Sat, 1-Jan-1971 00:00:00'
49 FUTURE = 'Tue, 1-Dec-2999 23:59:59'
50
51 try:
52 from gluon.contrib.minify import minify
53 have_minify = True
54 except ImportError:
55 have_minify = False
56
57 try:
58 import simplejson as sj
59 except:
60 try:
61 import json as sj
62 except:
63 import gluon.contrib.simplejson as sj
64
65 regex_session_id = re.compile('^([\w\-]+/)?[\w\-\.]+$')
66
67 __all__ = ['Request', 'Response', 'Session']
68
69 current = threading.local()
70
71 css_template = '<link href="%s" rel="stylesheet" type="text/css" />'
72 js_template = '<script src="%s" type="text/javascript"></script>'
73 coffee_template = '<script src="%s" type="text/coffee"></script>'
74 typescript_template = '<script src="%s" type="text/typescript"></script>'
75 less_template = '<link href="%s" rel="stylesheet/less" type="text/css" />'
76 css_inline = '<style type="text/css">\n%s\n</style>'
77 js_inline = '<script type="text/javascript">\n%s\n</script>'
84 self.write(EMPTY_DICT if self.bin else MARK+DICT)
85 self.memoize(obj)
86 self._batch_setitems([(key,obj[key]) for key in sorted(obj)])
87
88 SortingPickler.dispatch = copy.copy(Pickler.dispatch)
89 SortingPickler.dispatch[DictionaryType] = SortingPickler.save_dict
92 file = cStringIO.StringIO()
93 SortingPickler(file, protocol).dump(obj)
94 return file.getvalue()
95
98 """
99 copies request.env.wsgi_input into request.body
100 and stores progress upload status in cache_ram
101 X-Progress-ID:length and X-Progress-ID:uploaded
102 """
103 env = request.env
104 if not env.get('CONTENT_LENGTH', None):
105 return cStringIO.StringIO()
106 source = env['wsgi.input']
107 try:
108 size = int(env['CONTENT_LENGTH'])
109 except ValueError:
110 raise HTTP(400, "Invalid Content-Length header")
111 try:
112 dest = tempfile.NamedTemporaryFile()
113 except NotImplementedError:
114 dest = tempfile.TemporaryFile()
115 if not 'X-Progress-ID' in request.get_vars:
116 copystream(source, dest, size, chunk_size)
117 return dest
118 cache_key = 'X-Progress-ID:' + request.get_vars['X-Progress-ID']
119 cache_ram = CacheInRam(request)
120 cache_ram(cache_key + ':length', lambda: size, 0)
121 cache_ram(cache_key + ':uploaded', lambda: 0, 0)
122 while size > 0:
123 if size < chunk_size:
124 data = source.read(size)
125 cache_ram.increment(cache_key + ':uploaded', size)
126 else:
127 data = source.read(chunk_size)
128 cache_ram.increment(cache_key + ':uploaded', chunk_size)
129 length = len(data)
130 if length > size:
131 (data, length) = (data[:size], size)
132 size -= length
133 if length == 0:
134 break
135 dest.write(data)
136 if length < chunk_size:
137 break
138 dest.seek(0)
139 cache_ram(cache_key + ':length', None)
140 cache_ram(cache_key + ':uploaded', None)
141 return dest
142
144
145 """
146 defines the request object and the default values of its members
147
148 - env: environment variables, by gluon.main.wsgibase()
149 - cookies
150 - get_vars
151 - post_vars
152 - vars
153 - folder
154 - application
155 - function
156 - args
157 - extension
158 - now: datetime.datetime.today()
159 - restful()
160 """
161
183
184
186 query_string = self.env.get('QUERY_STRING','')
187 dget = cgi.parse_qs(query_string, keep_blank_values=1)
188 get_vars = self._get_vars = Storage(dget)
189 for (key, value) in get_vars.iteritems():
190 if isinstance(value,list) and len(value)==1:
191 get_vars[key] = value[0]
192
193 - def parse_post_vars(self):
194 env = self.env
195 post_vars = self._post_vars = Storage()
196 body = self.body
197
198 is_json = env.get('content_type', '')[:16] == 'application/json'
199
200 if is_json:
201 try:
202 json_vars = sj.load(body)
203 except:
204
205 json_vars = {}
206 pass
207
208 if isinstance(json_vars, dict):
209 post_vars.update(json_vars)
210
211 body.seek(0)
212
213
214 if (body and not is_json
215 and env.request_method in ('POST', 'PUT', 'DELETE', 'BOTH')):
216 query_string = env.pop('QUERY_STRING',None)
217 dpost = cgi.FieldStorage(fp=body, environ=env, keep_blank_values=1)
218 try:
219 post_vars.update(dpost)
220 except: pass
221 if query_string is not None:
222 env['QUERY_STRING'] = query_string
223
224 body.seek(0)
225
226 def listify(a):
227 return (not isinstance(a, list) and [a]) or a
228 try:
229 keys = sorted(dpost)
230 except TypeError:
231 keys = []
232 for key in keys:
233 if key is None:
234 continue
235 dpk = dpost[key]
236
237
238
239 pvalue = listify([(_dpk if _dpk.filename else _dpk.value)
240 for _dpk in dpk]
241 if isinstance(dpk, list) else
242 (dpk if dpk.filename else dpk.value))
243 if len(pvalue):
244 post_vars[key] = (len(pvalue) > 1 and pvalue) or pvalue[0]
245
246 @property
248 if self._body is None:
249 try:
250 self._body = copystream_progress(self)
251 except IOError:
252 raise HTTP(400, "Bad Request - HTTP body is incomplete")
253 return self._body
254
256 self._vars = copy.copy(self.get_vars)
257 for key,value in self.post_vars.iteritems():
258 if not key in self._vars:
259 self._vars[key] = value
260 else:
261 if not isinstance(self._vars[key],list):
262 self._vars[key] = [self._vars[key]]
263 self._vars[key] += value if isinstance(value,list) else [value]
264
265 @property
267 "lazily parse the query string into get_vars"
268 if self._get_vars is None:
269 self.parse_get_vars()
270 return self._get_vars
271
272 @property
273 - def post_vars(self):
274 "lazily parse the body into post_vars"
275 if self._post_vars is None:
276 self.parse_post_vars()
277 return self._post_vars
278
279 @property
281 "lazily parse all get_vars and post_vars to fill vars"
282 if self._vars is None:
283 self.parse_all_vars()
284 return self._vars
285
287 self.uuid = '%s/%s.%s.%s' % (
288 self.application,
289 self.client.replace(':', '_'),
290 self.now.strftime('%Y-%m-%d.%H-%M-%S'),
291 web2py_uuid())
292 return self.uuid
293
306
321
323 def wrapper(action, self=self):
324 def f(_action=action, _self=self, *a, **b):
325 self.is_restful = True
326 method = _self.env.request_method
327 if len(_self.args) and '.' in _self.args[-1]:
328 _self.args[-1], _, self.extension = self.args[-1].rpartition('.')
329 current.response.headers['Content-Type'] = \
330 contenttype('.' + _self.extension.lower())
331 rest_action = _action().get(method, None)
332 if not (rest_action and method==method.upper()
333 and callable(rest_action)):
334 raise HTTP(400, "method not supported")
335 try:
336 return rest_action(*_self.args, **getattr(_self,'vars',{}))
337 except TypeError, e:
338 exc_type, exc_value, exc_traceback = sys.exc_info()
339 if len(traceback.extract_tb(exc_traceback)) == 1:
340 raise HTTP(400, "invalid arguments")
341 else:
342 raise e
343 f.__doc__ = action.__doc__
344 f.__name__ = action.__name__
345 return f
346 return wrapper
347
350
351 """
352 defines the response object and the default values of its members
353 response.write( ) can be used to write in the output html
354 """
355
357 Storage.__init__(self)
358 self.status = 200
359 self.headers = dict()
360 self.headers['X-Powered-By'] = 'web2py'
361 self.body = cStringIO.StringIO()
362 self.session_id = None
363 self.cookies = Cookie.SimpleCookie()
364 self.postprocessing = []
365 self.flash = ''
366 self.meta = Storage()
367 self.menu = []
368 self.files = []
369 self.generic_patterns = []
370 self.delimiters = ('{{', '}}')
371 self._vars = None
372 self._caller = lambda f: f()
373 self._view_environment = None
374 self._custom_commit = None
375 self._custom_rollback = None
376
377 - def write(self, data, escape=True):
382
384 from compileapp import run_view_in
385 if len(a) > 2:
386 raise SyntaxError(
387 'Response.render can be called with two arguments, at most')
388 elif len(a) == 2:
389 (view, self._vars) = (a[0], a[1])
390 elif len(a) == 1 and isinstance(a[0], str):
391 (view, self._vars) = (a[0], {})
392 elif len(a) == 1 and hasattr(a[0], 'read') and callable(a[0].read):
393 (view, self._vars) = (a[0], {})
394 elif len(a) == 1 and isinstance(a[0], dict):
395 (view, self._vars) = (None, a[0])
396 else:
397 (view, self._vars) = (None, {})
398 self._vars.update(b)
399 self._view_environment.update(self._vars)
400 if view:
401 import cStringIO
402 (obody, oview) = (self.body, self.view)
403 (self.body, self.view) = (cStringIO.StringIO(), view)
404 run_view_in(self._view_environment)
405 page = self.body.getvalue()
406 self.body.close()
407 (self.body, self.view) = (obody, oview)
408 else:
409 run_view_in(self._view_environment)
410 page = self.body.getvalue()
411 return page
412
418
420
421 """
422 Caching method for writing out files.
423 By default, caches in ram for 5 minutes. To change,
424 response.cache_includes = (cache_method, time_expire).
425 Example: (cache.disk, 60) # caches to disk for 1 minute.
426 """
427 from gluon import URL
428
429 files = []
430 has_js = has_css = False
431 for item in self.files:
432 if extensions and not item.split('.')[-1] in extensions:
433 continue
434 if item in files:
435 continue
436 if item.endswith('.js'):
437 has_js = True
438 if item.endswith('.css'):
439 has_css = True
440 files.append(item)
441
442 if have_minify and ((self.optimize_css and has_css) or (self.optimize_js and has_js)):
443
444 key = hashlib.md5(repr(files)).hexdigest()
445
446 cache = self.cache_includes or (current.cache.ram, 60 * 5)
447
448 def call_minify(files=files):
449 return minify.minify(files,
450 URL('static', 'temp'),
451 current.request.folder,
452 self.optimize_css,
453 self.optimize_js)
454 if cache:
455 cache_model, time_expire = cache
456 files = cache_model('response.files.minified/' + key,
457 call_minify,
458 time_expire)
459 else:
460 files = call_minify()
461 s = ''
462 for item in files:
463 if isinstance(item, str):
464 f = item.lower().split('?')[0]
465 if self.static_version:
466 item = item.replace(
467 '/static/', '/static/_%s/' % self.static_version, 1)
468 if f.endswith('.css'):
469 s += css_template % item
470 elif f.endswith('.js'):
471 s += js_template % item
472 elif f.endswith('.coffee'):
473 s += coffee_template % item
474 elif f.endswith('.ts'):
475
476 s += typescript_template % item
477 elif f.endswith('.less'):
478 s += less_template % item
479 elif isinstance(item, (list, tuple)):
480 f = item[0]
481 if f == 'css:inline':
482 s += css_inline % item[1]
483 elif f == 'js:inline':
484 s += js_inline % item[1]
485 self.write(s, escape=False)
486
487 - def stream(
488 self,
489 stream,
490 chunk_size=DEFAULT_CHUNK_SIZE,
491 request=None,
492 attachment=False,
493 filename=None
494 ):
495 """
496 if a controller function::
497
498 return response.stream(file, 100)
499
500 the file content will be streamed at 100 bytes at the time
501
502 Optional kwargs:
503 (for custom stream calls)
504 attachment=True # Send as attachment. Usually creates a
505 # pop-up download window on browsers
506 filename=None # The name for the attachment
507
508 Note: for using the stream name (filename) with attachments
509 the option must be explicitly set as function parameter(will
510 default to the last request argument otherwise)
511 """
512
513 headers = self.headers
514
515 keys = [item.lower() for item in headers]
516 if attachment:
517 if filename is None:
518 attname = ""
519 else:
520 attname = filename
521 headers["Content-Disposition"] = \
522 "attachment;filename=%s" % attname
523
524 if not request:
525 request = current.request
526 if isinstance(stream, (str, unicode)):
527 stream_file_or_304_or_206(stream,
528 chunk_size=chunk_size,
529 request=request,
530 headers=headers,
531 status=self.status)
532
533
534 if hasattr(stream, 'name'):
535 filename = stream.name
536
537 if filename and not 'content-type' in keys:
538 headers['Content-Type'] = contenttype(filename)
539 if filename and not 'content-length' in keys:
540 try:
541 headers['Content-Length'] = \
542 os.path.getsize(filename)
543 except OSError:
544 pass
545
546 env = request.env
547
548 if request.is_https and isinstance(env.http_user_agent, str) and \
549 not re.search(r'Opera', env.http_user_agent) and \
550 re.search(r'MSIE [5-8][^0-9]', env.http_user_agent):
551 headers['Pragma'] = 'cache'
552 headers['Cache-Control'] = 'private'
553
554 if request and env.web2py_use_wsgi_file_wrapper:
555 wrapped = env.wsgi_file_wrapper(stream, chunk_size)
556 else:
557 wrapped = streamer(stream, chunk_size=chunk_size)
558 return wrapped
559
561 """
562 example of usage in controller::
563
564 def download():
565 return response.download(request, db)
566
567 downloads from http://..../download/filename
568 """
569
570 current.session.forget(current.response)
571
572 if not request.args:
573 raise HTTP(404)
574 name = request.args[-1]
575 items = re.compile('(?P<table>.*?)\.(?P<field>.*?)\..*')\
576 .match(name)
577 if not items:
578 raise HTTP(404)
579 (t, f) = (items.group('table'), items.group('field'))
580 try:
581 field = db[t][f]
582 except AttributeError:
583 raise HTTP(404)
584 try:
585 (filename, stream) = field.retrieve(name,nameonly=True)
586 except IOError:
587 raise HTTP(404)
588 headers = self.headers
589 headers['Content-Type'] = contenttype(name)
590 if download_filename == None:
591 download_filename = filename
592 if attachment:
593 headers['Content-Disposition'] = \
594 'attachment; filename="%s"' % download_filename.replace('"','\"')
595 return self.stream(stream, chunk_size=chunk_size, request=request)
596
597 - def json(self, data, default=None):
599
600 - def xmlrpc(self, request, methods):
601 """
602 assuming::
603
604 def add(a, b):
605 return a+b
606
607 if a controller function \"func\"::
608
609 return response.xmlrpc(request, [add])
610
611 the controller will be able to handle xmlrpc requests for
612 the add function. Example::
613
614 import xmlrpclib
615 connection = xmlrpclib.ServerProxy(
616 'http://hostname/app/contr/func')
617 print connection.add(3, 4)
618
619 """
620
621 return handler(request, self, methods)
622
670
673 """
674 defines the session object and the default values of its members (None)
675
676 response.session_storage_type : 'file', 'db', or 'cookie'
677 response.session_cookie_compression_level :
678 response.session_cookie_expires : cookie expiration
679 response.session_cookie_key : for encrypted sessions in cookies
680 response.session_id : a number or None if no session
681 response.session_id_name :
682 response.session_locked :
683 response.session_masterapp :
684 response.session_new : a new session obj is being created
685 response.session_hash : hash of the pickled loaded session
686 response.session_pickled : picked session
687
688 if session in cookie:
689
690 response.session_data_name : name of the cookie for session data
691
692 if session in db:
693
694 response.session_db_record_id :
695 response.session_db_table :
696 response.session_db_unique_key :
697
698 if session in file:
699
700 response.session_file :
701 response.session_filename :
702 """
703
704 - def connect(
705 self,
706 request=None,
707 response=None,
708 db=None,
709 tablename='web2py_session',
710 masterapp=None,
711 migrate=True,
712 separate=None,
713 check_client=False,
714 cookie_key=None,
715 cookie_expires=None,
716 compression_level=None
717 ):
718 """
719 separate can be separate=lambda(session_name): session_name[-2:]
720 and it is used to determine a session prefix.
721 separate can be True and it is set to session_name[-2:]
722 """
723 request = request or current.request
724 response = response or current.response
725 masterapp = masterapp or request.application
726 cookies = request.cookies
727
728 self._unlock(response)
729
730 response.session_masterapp = masterapp
731 response.session_id_name = 'session_id_%s' % masterapp.lower()
732 response.session_data_name = 'session_data_%s' % masterapp.lower()
733 response.session_cookie_expires = cookie_expires
734 response.session_client = str(request.client).replace(':', '.')
735 response.session_cookie_key = cookie_key
736 response.session_cookie_compression_level = compression_level
737
738
739 try:
740 old_session_id = cookies[response.session_id_name].value
741 except KeyError:
742 old_session_id = None
743 response.session_id = old_session_id
744
745
746 if cookie_key:
747 response.session_storage_type = 'cookie'
748 elif db:
749 response.session_storage_type = 'db'
750 else:
751 response.session_storage_type = 'file'
752
753
754
755 if (global_settings.db_sessions is True or
756 masterapp in global_settings.db_sessions):
757 return
758
759 if response.session_storage_type == 'cookie':
760
761 if response.session_data_name in cookies:
762 session_cookie_data = cookies[response.session_data_name].value
763 else:
764 session_cookie_data = None
765 if session_cookie_data:
766 data = secure_loads(session_cookie_data, cookie_key,
767 compression_level=compression_level)
768 if data:
769 self.update(data)
770 response.session_id = True
771
772
773 elif response.session_storage_type == 'file':
774 response.session_new = False
775 response.session_file = None
776
777 if response.session_id:
778 if not regex_session_id.match(response.session_id):
779 response.session_id = None
780 else:
781 response.session_filename = \
782 os.path.join(up(request.folder), masterapp,
783 'sessions', response.session_id)
784 try:
785 response.session_file = \
786 open(response.session_filename, 'rb+')
787 portalocker.lock(response.session_file,
788 portalocker.LOCK_EX)
789 response.session_locked = True
790 self.update(cPickle.load(response.session_file))
791 response.session_file.seek(0)
792 oc = response.session_filename.split('/')[-1].split('-')[0]
793 if check_client and response.session_client != oc:
794 raise Exception("cookie attack")
795 except:
796 response.session_id = None
797 if not response.session_id:
798 uuid = web2py_uuid()
799 response.session_id = '%s-%s' % (response.session_client, uuid)
800 separate = separate and (lambda session_name: session_name[-2:])
801 if separate:
802 prefix = separate(response.session_id)
803 response.session_id = '%s/%s' % (prefix, response.session_id)
804 response.session_filename = \
805 os.path.join(up(request.folder), masterapp,
806 'sessions', response.session_id)
807 response.session_new = True
808
809
810 elif response.session_storage_type == 'db':
811 if global_settings.db_sessions is not True:
812 global_settings.db_sessions.add(masterapp)
813
814 if response.session_file:
815 self._close(response)
816
817 if settings.global_settings.web2py_runtime_gae:
818 request.tickets_db = db
819 table_migrate = (masterapp == request.application)
820 tname = tablename + '_' + masterapp
821 table = db.get(tname, None)
822 Field = db.Field
823 if table is None:
824 db.define_table(
825 tname,
826 Field('locked', 'boolean', default=False),
827 Field('client_ip', length=64),
828 Field('created_datetime', 'datetime',
829 default=request.now),
830 Field('modified_datetime', 'datetime'),
831 Field('unique_key', length=64),
832 Field('session_data', 'blob'),
833 migrate=table_migrate,
834 )
835 table = db[tname]
836 response.session_db_table = table
837 if response.session_id:
838
839 try:
840 (record_id, unique_key) = response.session_id.split(':')
841 record_id = long(record_id)
842 except (TypeError,ValueError):
843 record_id = None
844
845
846 if record_id:
847 row = table(record_id)
848
849 if row:
850
851
852 session_data = cPickle.loads(row.session_data)
853 self.update(session_data)
854 else:
855 record_id = None
856 if record_id:
857 response.session_id = '%s:%s' % (record_id, unique_key)
858 response.session_db_unique_key = unique_key
859 response.session_db_record_id = record_id
860 else:
861 response.session_id = None
862 response.session_new = True
863
864
865 else:
866 response.session_new = True
867
868
869
870
871
872
873
874 if isinstance(response.session_id,str):
875 response.cookies[response.session_id_name] = response.session_id
876 response.cookies[response.session_id_name]['path'] = '/'
877 if cookie_expires:
878 response.cookies[response.session_id_name]['expires'] = \
879 cookie_expires.strftime(FMT)
880
881 session_pickled = cPickle.dumps(self)
882 response.session_hash = hashlib.md5(session_pickled).hexdigest()
883
884 if self.flash:
885 (response.flash, self.flash) = (self.flash, None)
886
887
888 - def renew(self, clear_session=False):
944
952
968
991
994
996 if self._start_timestamp:
997 return False
998 else:
999 self._start_timestamp = datetime.datetime.today()
1000 return True
1001
1003 now = datetime.datetime.today()
1004 if not self._last_timestamp or \
1005 self._last_timestamp + datetime.timedelta(seconds=seconds) > now:
1006 self._last_timestamp = now
1007 return False
1008 else:
1009 return True
1010
1013
1014 - def forget(self, response=None):
1017
1019 if self._forget or self._unchanged(response):
1020
1021 self.save_session_id_cookie()
1022 return False
1023 name = response.session_data_name
1024 compression_level = response.session_cookie_compression_level
1025 value = secure_dumps(dict(self),
1026 response.session_cookie_key,
1027 compression_level=compression_level)
1028 rcookies = response.cookies
1029 rcookies.pop(name, None)
1030 rcookies[name] = value
1031 rcookies[name]['path'] = '/'
1032 expires = response.session_cookie_expires
1033 if isinstance(expires,datetime.datetime):
1034 expires = expires.strftime(FMT)
1035 if expires:
1036 rcookies[name]['expires'] = expires
1037 return True
1038
1040 session_pickled = cPickle.dumps(self)
1041 response.session_pickled = session_pickled
1042 session_hash = hashlib.md5(session_pickled).hexdigest()
1043 return response.session_hash == session_hash
1044
1085
1091
1115
1123
1132