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.now()
159 - utcnow : datetime.datetime.utcnow()
160 - is_local
161 - is_https
162 - restful()
163 """
164
186
187
189 """Takes the QUERY_STRING and unpacks it to get_vars
190 """
191 query_string = self.env.get('QUERY_STRING','')
192 dget = cgi.parse_qs(query_string, keep_blank_values=1)
193 get_vars = self._get_vars = Storage(dget)
194 for (key, value) in get_vars.iteritems():
195 if isinstance(value,list) and len(value)==1:
196 get_vars[key] = value[0]
197
198 - def parse_post_vars(self):
199 """Takes the body of the request and unpacks it into
200 post_vars. application/json is also automatically parsed
201 """
202 env = self.env
203 post_vars = self._post_vars = Storage()
204 body = self.body
205
206 is_json = env.get('content_type', '')[:16] == 'application/json'
207
208 if is_json:
209 try:
210 json_vars = sj.load(body)
211 except:
212
213 json_vars = {}
214 pass
215
216 if isinstance(json_vars, dict):
217 post_vars.update(json_vars)
218
219 body.seek(0)
220
221
222 if (body and not is_json
223 and env.request_method in ('POST', 'PUT', 'DELETE', 'BOTH')):
224 query_string = env.pop('QUERY_STRING',None)
225 dpost = cgi.FieldStorage(fp=body, environ=env, keep_blank_values=1)
226 try:
227 post_vars.update(dpost)
228 except: pass
229 if query_string is not None:
230 env['QUERY_STRING'] = query_string
231
232 body.seek(0)
233
234 def listify(a):
235 return (not isinstance(a, list) and [a]) or a
236 try:
237 keys = sorted(dpost)
238 except TypeError:
239 keys = []
240 for key in keys:
241 if key is None:
242 continue
243 dpk = dpost[key]
244
245
246
247 pvalue = listify([(_dpk if _dpk.filename else _dpk.value)
248 for _dpk in dpk]
249 if isinstance(dpk, list) else
250 (dpk if dpk.filename else dpk.value))
251 if len(pvalue):
252 post_vars[key] = (len(pvalue) > 1 and pvalue) or pvalue[0]
253
254 @property
256 if self._body is None:
257 try:
258 self._body = copystream_progress(self)
259 except IOError:
260 raise HTTP(400, "Bad Request - HTTP body is incomplete")
261 return self._body
262
264 """Merges get_vars and post_vars to vars
265 """
266 self._vars = copy.copy(self.get_vars)
267 for key,value in self.post_vars.iteritems():
268 if not key in self._vars:
269 self._vars[key] = value
270 else:
271 if not isinstance(self._vars[key],list):
272 self._vars[key] = [self._vars[key]]
273 self._vars[key] += value if isinstance(value,list) else [value]
274
275 @property
277 """Lazily parses the query string into get_vars
278 """
279 if self._get_vars is None:
280 self.parse_get_vars()
281 return self._get_vars
282
283 @property
284 - def post_vars(self):
285 """Lazily parse the body into post_vars
286 """
287 if self._post_vars is None:
288 self.parse_post_vars()
289 return self._post_vars
290
291 @property
293 """Lazily parses all get_vars and post_vars to fill vars
294 """
295 if self._vars is None:
296 self.parse_all_vars()
297 return self._vars
298
300 self.uuid = '%s/%s.%s.%s' % (
301 self.application,
302 self.client.replace(':', '_'),
303 self.now.strftime('%Y-%m-%d.%H-%M-%S'),
304 web2py_uuid())
305 return self.uuid
306
319
334
336 def wrapper(action, self=self):
337 def f(_action=action, _self=self, *a, **b):
338 self.is_restful = True
339 method = _self.env.request_method
340 if len(_self.args) and '.' in _self.args[-1]:
341 _self.args[-1], _, self.extension = self.args[-1].rpartition('.')
342 current.response.headers['Content-Type'] = \
343 contenttype('.' + _self.extension.lower())
344 rest_action = _action().get(method, None)
345 if not (rest_action and method==method.upper()
346 and callable(rest_action)):
347 raise HTTP(400, "method not supported")
348 try:
349 return rest_action(*_self.args, **getattr(_self,'vars',{}))
350 except TypeError, e:
351 exc_type, exc_value, exc_traceback = sys.exc_info()
352 if len(traceback.extract_tb(exc_traceback)) == 1:
353 raise HTTP(400, "invalid arguments")
354 else:
355 raise e
356 f.__doc__ = action.__doc__
357 f.__name__ = action.__name__
358 return f
359 return wrapper
360
363
364 """
365 Defines the response object and the default values of its members
366 response.write( ) can be used to write in the output html
367 """
368
370 Storage.__init__(self)
371 self.status = 200
372 self.headers = dict()
373 self.headers['X-Powered-By'] = 'web2py'
374 self.body = cStringIO.StringIO()
375 self.session_id = None
376 self.cookies = Cookie.SimpleCookie()
377 self.postprocessing = []
378 self.flash = ''
379 self.meta = Storage()
380 self.menu = []
381 self.files = []
382 self.generic_patterns = []
383 self.delimiters = ('{{', '}}')
384 self._vars = None
385 self._caller = lambda f: f()
386 self._view_environment = None
387 self._custom_commit = None
388 self._custom_rollback = None
389
390 - def write(self, data, escape=True):
395
397 from compileapp import run_view_in
398 if len(a) > 2:
399 raise SyntaxError(
400 'Response.render can be called with two arguments, at most')
401 elif len(a) == 2:
402 (view, self._vars) = (a[0], a[1])
403 elif len(a) == 1 and isinstance(a[0], str):
404 (view, self._vars) = (a[0], {})
405 elif len(a) == 1 and hasattr(a[0], 'read') and callable(a[0].read):
406 (view, self._vars) = (a[0], {})
407 elif len(a) == 1 and isinstance(a[0], dict):
408 (view, self._vars) = (None, a[0])
409 else:
410 (view, self._vars) = (None, {})
411 self._vars.update(b)
412 self._view_environment.update(self._vars)
413 if view:
414 import cStringIO
415 (obody, oview) = (self.body, self.view)
416 (self.body, self.view) = (cStringIO.StringIO(), view)
417 run_view_in(self._view_environment)
418 page = self.body.getvalue()
419 self.body.close()
420 (self.body, self.view) = (obody, oview)
421 else:
422 run_view_in(self._view_environment)
423 page = self.body.getvalue()
424 return page
425
431
433
434 """
435 Caching method for writing out files.
436 By default, caches in ram for 5 minutes. To change,
437 response.cache_includes = (cache_method, time_expire).
438 Example: (cache.disk, 60) # caches to disk for 1 minute.
439 """
440 from gluon import URL
441
442 files = []
443 has_js = has_css = False
444 for item in self.files:
445 if extensions and not item.split('.')[-1] in extensions:
446 continue
447 if item in files:
448 continue
449 if item.endswith('.js'):
450 has_js = True
451 if item.endswith('.css'):
452 has_css = True
453 files.append(item)
454
455 if have_minify and ((self.optimize_css and has_css) or (self.optimize_js and has_js)):
456
457 key = hashlib.md5(repr(files)).hexdigest()
458
459 cache = self.cache_includes or (current.cache.ram, 60 * 5)
460
461 def call_minify(files=files):
462 return minify.minify(files,
463 URL('static', 'temp'),
464 current.request.folder,
465 self.optimize_css,
466 self.optimize_js)
467 if cache:
468 cache_model, time_expire = cache
469 files = cache_model('response.files.minified/' + key,
470 call_minify,
471 time_expire)
472 else:
473 files = call_minify()
474 s = ''
475 for item in files:
476 if isinstance(item, str):
477 f = item.lower().split('?')[0]
478 if self.static_version:
479 item = item.replace(
480 '/static/', '/static/_%s/' % self.static_version, 1)
481 if f.endswith('.css'):
482 s += css_template % item
483 elif f.endswith('.js'):
484 s += js_template % item
485 elif f.endswith('.coffee'):
486 s += coffee_template % item
487 elif f.endswith('.ts'):
488
489 s += typescript_template % item
490 elif f.endswith('.less'):
491 s += less_template % item
492 elif isinstance(item, (list, tuple)):
493 f = item[0]
494 if f == 'css:inline':
495 s += css_inline % item[1]
496 elif f == 'js:inline':
497 s += js_inline % item[1]
498 self.write(s, escape=False)
499
500 - def stream(
501 self,
502 stream,
503 chunk_size=DEFAULT_CHUNK_SIZE,
504 request=None,
505 attachment=False,
506 filename=None
507 ):
508 """
509 If in a controller function::
510
511 return response.stream(file, 100)
512
513 the file content will be streamed at 100 bytes at the time
514
515 Args:
516 stream: filename or read()able content
517 chunk_size(int): Buffer size
518 request: the request object
519 attachment(bool): prepares the correct headers to download the file
520 as an attachment. Usually creates a pop-up download window
521 on browsers
522 filename(str): the name for the attachment
523
524 Note:
525 for using the stream name (filename) with attachments
526 the option must be explicitly set as function parameter (will
527 default to the last request argument otherwise)
528 """
529
530 headers = self.headers
531
532 keys = [item.lower() for item in headers]
533 if attachment:
534 if filename is None:
535 attname = ""
536 else:
537 attname = filename
538 headers["Content-Disposition"] = \
539 "attachment;filename=%s" % attname
540
541 if not request:
542 request = current.request
543 if isinstance(stream, (str, unicode)):
544 stream_file_or_304_or_206(stream,
545 chunk_size=chunk_size,
546 request=request,
547 headers=headers,
548 status=self.status)
549
550
551 if hasattr(stream, 'name'):
552 filename = stream.name
553
554 if filename and not 'content-type' in keys:
555 headers['Content-Type'] = contenttype(filename)
556 if filename and not 'content-length' in keys:
557 try:
558 headers['Content-Length'] = \
559 os.path.getsize(filename)
560 except OSError:
561 pass
562
563 env = request.env
564
565 if request.is_https and isinstance(env.http_user_agent, str) and \
566 not re.search(r'Opera', env.http_user_agent) and \
567 re.search(r'MSIE [5-8][^0-9]', env.http_user_agent):
568 headers['Pragma'] = 'cache'
569 headers['Cache-Control'] = 'private'
570
571 if request and env.web2py_use_wsgi_file_wrapper:
572 wrapped = env.wsgi_file_wrapper(stream, chunk_size)
573 else:
574 wrapped = streamer(stream, chunk_size=chunk_size)
575 return wrapped
576
578 """
579 Example of usage in controller::
580
581 def download():
582 return response.download(request, db)
583
584 Downloads from http://..../download/filename
585 """
586
587 current.session.forget(current.response)
588
589 if not request.args:
590 raise HTTP(404)
591 name = request.args[-1]
592 items = re.compile('(?P<table>.*?)\.(?P<field>.*?)\..*').match(name)
593 if not items:
594 raise HTTP(404)
595 (t, f) = (items.group('table'), items.group('field'))
596 try:
597 field = db[t][f]
598 except AttributeError:
599 raise HTTP(404)
600 try:
601 (filename, stream) = field.retrieve(name,nameonly=True)
602 except IOError:
603 raise HTTP(404)
604 headers = self.headers
605 headers['Content-Type'] = contenttype(name)
606 if download_filename == None:
607 download_filename = filename
608 if attachment:
609 headers['Content-Disposition'] = \
610 'attachment; filename="%s"' % download_filename.replace('"','\"')
611 return self.stream(stream, chunk_size=chunk_size, request=request)
612
613 - def json(self, data, default=None):
615
616 - def xmlrpc(self, request, methods):
617 """
618 assuming::
619
620 def add(a, b):
621 return a+b
622
623 if a controller function \"func\"::
624
625 return response.xmlrpc(request, [add])
626
627 the controller will be able to handle xmlrpc requests for
628 the add function. Example::
629
630 import xmlrpclib
631 connection = xmlrpclib.ServerProxy(
632 'http://hostname/app/contr/func')
633 print connection.add(3, 4)
634
635 """
636
637 return handler(request, self, methods)
638
686
689 """
690 Defines the session object and the default values of its members (None)
691
692 - session_storage_type : 'file', 'db', or 'cookie'
693 - session_cookie_compression_level :
694 - session_cookie_expires : cookie expiration
695 - session_cookie_key : for encrypted sessions in cookies
696 - session_id : a number or None if no session
697 - session_id_name :
698 - session_locked :
699 - session_masterapp :
700 - session_new : a new session obj is being created
701 - session_hash : hash of the pickled loaded session
702 - session_pickled : picked session
703
704 if session in cookie:
705
706 - session_data_name : name of the cookie for session data
707
708 if session in db:
709
710 - session_db_record_id
711 - session_db_table
712 - session_db_unique_key
713
714 if session in file:
715
716 - session_file
717 - session_filename
718 """
719
720 - def connect(
721 self,
722 request=None,
723 response=None,
724 db=None,
725 tablename='web2py_session',
726 masterapp=None,
727 migrate=True,
728 separate=None,
729 check_client=False,
730 cookie_key=None,
731 cookie_expires=None,
732 compression_level=None
733 ):
734 """
735 Used in models, allows to customize Session handling
736
737 Args:
738 request: the request object
739 response: the response object
740 db: to store/retrieve sessions in db (a table is created)
741 tablename(str): table name
742 masterapp(str): points to another's app sessions. This enables a
743 "SSO" environment among apps
744 migrate: passed to the underlying db
745 separate: with True, creates a folder with the 2 initials of the
746 session id. Can also be a function, e.g. ::
747
748 separate=lambda(session_name): session_name[-2:]
749
750 check_client: if True, sessions can only come from the same ip
751 cookie_key(str): secret for cookie encryption
752 cookie_expires: sets the expiration of the cookie
753 compression_level(int): 0-9, sets zlib compression on the data
754 before the encryption
755 """
756 request = request or current.request
757 response = response or current.response
758 masterapp = masterapp or request.application
759 cookies = request.cookies
760
761 self._unlock(response)
762
763 response.session_masterapp = masterapp
764 response.session_id_name = 'session_id_%s' % masterapp.lower()
765 response.session_data_name = 'session_data_%s' % masterapp.lower()
766 response.session_cookie_expires = cookie_expires
767 response.session_client = str(request.client).replace(':', '.')
768 response.session_cookie_key = cookie_key
769 response.session_cookie_compression_level = compression_level
770
771
772 try:
773 old_session_id = cookies[response.session_id_name].value
774 except KeyError:
775 old_session_id = None
776 response.session_id = old_session_id
777
778
779 if cookie_key:
780 response.session_storage_type = 'cookie'
781 elif db:
782 response.session_storage_type = 'db'
783 else:
784 response.session_storage_type = 'file'
785
786
787
788 if (global_settings.db_sessions is True or
789 masterapp in global_settings.db_sessions):
790 return
791
792 if response.session_storage_type == 'cookie':
793
794 if response.session_data_name in cookies:
795 session_cookie_data = cookies[response.session_data_name].value
796 else:
797 session_cookie_data = None
798 if session_cookie_data:
799 data = secure_loads(session_cookie_data, cookie_key,
800 compression_level=compression_level)
801 if data:
802 self.update(data)
803 response.session_id = True
804
805
806 elif response.session_storage_type == 'file':
807 response.session_new = False
808 response.session_file = None
809
810 if response.session_id:
811 if not regex_session_id.match(response.session_id):
812 response.session_id = None
813 else:
814 response.session_filename = \
815 os.path.join(up(request.folder), masterapp,
816 'sessions', response.session_id)
817 try:
818 response.session_file = \
819 open(response.session_filename, 'rb+')
820 portalocker.lock(response.session_file,
821 portalocker.LOCK_EX)
822 response.session_locked = True
823 self.update(cPickle.load(response.session_file))
824 response.session_file.seek(0)
825 oc = response.session_filename.split('/')[-1].split('-')[0]
826 if check_client and response.session_client != oc:
827 raise Exception("cookie attack")
828 except:
829 response.session_id = None
830 if not response.session_id:
831 uuid = web2py_uuid()
832 response.session_id = '%s-%s' % (response.session_client, uuid)
833 separate = separate and (lambda session_name: session_name[-2:])
834 if separate:
835 prefix = separate(response.session_id)
836 response.session_id = '%s/%s' % (prefix, response.session_id)
837 response.session_filename = \
838 os.path.join(up(request.folder), masterapp,
839 'sessions', response.session_id)
840 response.session_new = True
841
842
843 elif response.session_storage_type == 'db':
844 if global_settings.db_sessions is not True:
845 global_settings.db_sessions.add(masterapp)
846
847 if response.session_file:
848 self._close(response)
849
850 if settings.global_settings.web2py_runtime_gae:
851 request.tickets_db = db
852 if masterapp == request.application:
853 table_migrate = migrate
854 else:
855 table_migrate = False
856 tname = tablename + '_' + masterapp
857 table = db.get(tname, None)
858 Field = db.Field
859 if table is None:
860 db.define_table(
861 tname,
862 Field('locked', 'boolean', default=False),
863 Field('client_ip', length=64),
864 Field('created_datetime', 'datetime',
865 default=request.now),
866 Field('modified_datetime', 'datetime'),
867 Field('unique_key', length=64),
868 Field('session_data', 'blob'),
869 migrate=table_migrate,
870 )
871 table = db[tname]
872 response.session_db_table = table
873 if response.session_id:
874
875 try:
876 (record_id, unique_key) = response.session_id.split(':')
877 record_id = long(record_id)
878 except (TypeError,ValueError):
879 record_id = None
880
881
882 if record_id:
883 row = table(record_id, unique_key=unique_key)
884
885 if row:
886
887
888 session_data = cPickle.loads(row.session_data)
889 self.update(session_data)
890 response.session_new = False
891 else:
892 record_id = None
893 if record_id:
894 response.session_id = '%s:%s' % (record_id, unique_key)
895 response.session_db_unique_key = unique_key
896 response.session_db_record_id = record_id
897 else:
898 response.session_id = None
899 response.session_new = True
900
901
902 else:
903 response.session_new = True
904
905
906
907
908
909
910
911 if isinstance(response.session_id,str):
912 response.cookies[response.session_id_name] = response.session_id
913 response.cookies[response.session_id_name]['path'] = '/'
914 if cookie_expires:
915 response.cookies[response.session_id_name]['expires'] = \
916 cookie_expires.strftime(FMT)
917
918 session_pickled = cPickle.dumps(self)
919 response.session_hash = hashlib.md5(session_pickled).hexdigest()
920
921 if self.flash:
922 (response.flash, self.flash) = (self.flash, None)
923
924
925 - def renew(self, clear_session=False):
981
989
1005
1029
1032
1034 if self._start_timestamp:
1035 return False
1036 else:
1037 self._start_timestamp = datetime.datetime.today()
1038 return True
1039
1041 now = datetime.datetime.today()
1042 if not self._last_timestamp or \
1043 self._last_timestamp + datetime.timedelta(seconds=seconds) > now:
1044 self._last_timestamp = now
1045 return False
1046 else:
1047 return True
1048
1051
1052 - def forget(self, response=None):
1055
1057 if self._forget or self._unchanged(response):
1058
1059 self.save_session_id_cookie()
1060 return False
1061 name = response.session_data_name
1062 compression_level = response.session_cookie_compression_level
1063 value = secure_dumps(dict(self),
1064 response.session_cookie_key,
1065 compression_level=compression_level)
1066 rcookies = response.cookies
1067 rcookies.pop(name, None)
1068 rcookies[name] = value
1069 rcookies[name]['path'] = '/'
1070 expires = response.session_cookie_expires
1071 if isinstance(expires,datetime.datetime):
1072 expires = expires.strftime(FMT)
1073 if expires:
1074 rcookies[name]['expires'] = expires
1075 return True
1076
1078 session_pickled = cPickle.dumps(self)
1079 response.session_pickled = session_pickled
1080 session_hash = hashlib.md5(session_pickled).hexdigest()
1081 return response.session_hash == session_hash
1082
1123
1129
1153
1161
1170