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, secure_dumps, secure_loads
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 FMT = '%a, %d-%b-%Y %H:%M:%S PST'
41 PAST = 'Sat, 1-Jan-1971 00:00:00'
42 FUTURE = 'Tue, 1-Dec-2999 23:59:59'
43
44 try:
45 from gluon.contrib.minify import minify
46 have_minify = True
47 except ImportError:
48 have_minify = False
49
50 regex_session_id = re.compile('^([\w\-]+/)?[\w\-\.]+$')
51
52 __all__ = ['Request', 'Response', 'Session']
53
54 current = threading.local()
55
56 css_template = '<link href="%s" rel="stylesheet" type="text/css" />'
57 js_template = '<script src="%s" type="text/javascript"></script>'
58 coffee_template = '<script src="%s" type="text/coffee"></script>'
59 typescript_template = '<script src="%s" type="text/typescript"></script>'
60 less_template = '<link href="%s" rel="stylesheet/less" type="text/css" />'
61 css_inline = '<style type="text/css">\n%s\n</style>'
62 js_inline = '<script type="text/javascript">\n%s\n</script>'
63
64
66
67 """
68 defines the request object and the default values of its members
69
70 - env: environment variables, by gluon.main.wsgibase()
71 - cookies
72 - get_vars
73 - post_vars
74 - vars
75 - folder
76 - application
77 - function
78 - args
79 - extension
80 - now: datetime.datetime.today()
81 - restful()
82 """
83
85 Storage.__init__(self)
86 self.wsgi = Storage()
87 self.env = Storage()
88 self.cookies = Cookie.SimpleCookie()
89 self.get_vars = Storage()
90 self.post_vars = Storage()
91 self.vars = Storage()
92 self.folder = None
93 self.application = None
94 self.function = None
95 self.args = List()
96 self.extension = 'html'
97 self.now = datetime.datetime.now()
98 self.utcnow = datetime.datetime.utcnow()
99 self.is_restful = False
100 self.is_https = False
101 self.is_local = False
102 self.global_settings = settings.global_settings
103
105 self.uuid = '%s/%s.%s.%s' % (
106 self.application,
107 self.client.replace(':', '_'),
108 self.now.strftime('%Y-%m-%d.%H-%M-%S'),
109 web2py_uuid())
110 return self.uuid
111
124
139
140
141
143 def wrapper(action, self=self):
144 def f(_action=action, _self=self, *a, **b):
145 self.is_restful = True
146 method = _self.env.request_method
147 if len(_self.args) and '.' in _self.args[-1]:
148 _self.args[-
149 1], _self.extension = _self.args[-1].rsplit('.', 1)
150 current.response.headers['Content-Type'] = \
151 contenttype(_self.extension.lower())
152 if not method in ['GET', 'POST', 'DELETE', 'PUT']:
153 raise HTTP(400, "invalid method")
154 rest_action = _action().get(method, None)
155 if not rest_action:
156 raise HTTP(400, "method not supported")
157 try:
158 return rest_action(*_self.args, **_self.vars)
159 except TypeError, e:
160 exc_type, exc_value, exc_traceback = sys.exc_info()
161 if len(traceback.extract_tb(exc_traceback)) == 1:
162 raise HTTP(400, "invalid arguments")
163 else:
164 raise e
165 f.__doc__ = action.__doc__
166 f.__name__ = action.__name__
167 return f
168 return wrapper
169
170
172
173 """
174 defines the response object and the default values of its members
175 response.write( ) can be used to write in the output html
176 """
177
179 Storage.__init__(self)
180 self.status = 200
181 self.headers = dict()
182 self.headers['X-Powered-By'] = 'web2py'
183 self.body = cStringIO.StringIO()
184 self.session_id = None
185 self.cookies = Cookie.SimpleCookie()
186 self.postprocessing = []
187 self.flash = ''
188 self.meta = Storage()
189 self.menu = []
190 self.files = []
191 self.generic_patterns = []
192 self.delimiters = ('{{', '}}')
193 self._vars = None
194 self._caller = lambda f: f()
195 self._view_environment = None
196 self._custom_commit = None
197 self._custom_rollback = None
198
199 - def write(self, data, escape=True):
204
206 from compileapp import run_view_in
207 if len(a) > 2:
208 raise SyntaxError(
209 'Response.render can be called with two arguments, at most')
210 elif len(a) == 2:
211 (view, self._vars) = (a[0], a[1])
212 elif len(a) == 1 and isinstance(a[0], str):
213 (view, self._vars) = (a[0], {})
214 elif len(a) == 1 and hasattr(a[0], 'read') and callable(a[0].read):
215 (view, self._vars) = (a[0], {})
216 elif len(a) == 1 and isinstance(a[0], dict):
217 (view, self._vars) = (None, a[0])
218 else:
219 (view, self._vars) = (None, {})
220 self._vars.update(b)
221 self._view_environment.update(self._vars)
222 if view:
223 import cStringIO
224 (obody, oview) = (self.body, self.view)
225 (self.body, self.view) = (cStringIO.StringIO(), view)
226 run_view_in(self._view_environment)
227 page = self.body.getvalue()
228 self.body.close()
229 (self.body, self.view) = (obody, oview)
230 else:
231 run_view_in(self._view_environment)
232 page = self.body.getvalue()
233 return page
234
240
242
243 """
244 Caching method for writing out files.
245 By default, caches in ram for 5 minutes. To change,
246 response.cache_includes = (cache_method, time_expire).
247 Example: (cache.disk, 60) # caches to disk for 1 minute.
248 """
249 from gluon import URL
250
251 files = []
252 has_js = has_css = False
253 for item in self.files:
254 if extensions and not item.split('.')[-1] in extensions:
255 continue
256 if item in files:
257 continue
258 if item.endswith('.js'):
259 has_js = True
260 if item.endswith('.css'):
261 has_css = True
262 files.append(item)
263
264 if have_minify and ((self.optimize_css and has_css) or (self.optimize_js and has_js)):
265
266 key = hashlib.md5(repr(files)).hexdigest()
267
268 cache = self.cache_includes or (current.cache.ram, 60 * 5)
269
270 def call_minify(files=files):
271 return minify.minify(files,
272 URL('static', 'temp'),
273 current.request.folder,
274 self.optimize_css,
275 self.optimize_js)
276 if cache:
277 cache_model, time_expire = cache
278 files = cache_model('response.files.minified/' + key,
279 call_minify,
280 time_expire)
281 else:
282 files = call_minify()
283 s = ''
284 for item in files:
285 if isinstance(item, str):
286 f = item.lower().split('?')[0]
287 if self.static_version:
288 item = item.replace(
289 '/static/', '/static/_%s/' % self.static_version, 1)
290 if f.endswith('.css'):
291 s += css_template % item
292 elif f.endswith('.js'):
293 s += js_template % item
294 elif f.endswith('.coffee'):
295 s += coffee_template % item
296 elif f.endswith('.ts'):
297
298 s += typescript_template % item
299 elif f.endswith('.less'):
300 s += less_template % item
301 elif isinstance(item, (list, tuple)):
302 f = item[0]
303 if f == 'css:inline':
304 s += css_inline % item[1]
305 elif f == 'js:inline':
306 s += js_inline % item[1]
307 self.write(s, escape=False)
308
309 - def stream(
310 self,
311 stream,
312 chunk_size=DEFAULT_CHUNK_SIZE,
313 request=None,
314 attachment=False,
315 filename=None
316 ):
317 """
318 if a controller function::
319
320 return response.stream(file, 100)
321
322 the file content will be streamed at 100 bytes at the time
323
324 Optional kwargs:
325 (for custom stream calls)
326 attachment=True # Send as attachment. Usually creates a
327 # pop-up download window on browsers
328 filename=None # The name for the attachment
329
330 Note: for using the stream name (filename) with attachments
331 the option must be explicitly set as function parameter(will
332 default to the last request argument otherwise)
333 """
334
335 headers = self.headers
336
337 keys = [item.lower() for item in headers]
338 if attachment:
339 if filename is None:
340 attname = ""
341 else:
342 attname = filename
343 headers["Content-Disposition"] = \
344 "attachment;filename=%s" % attname
345
346 if not request:
347 request = current.request
348 if isinstance(stream, (str, unicode)):
349 stream_file_or_304_or_206(stream,
350 chunk_size=chunk_size,
351 request=request,
352 headers=headers,
353 status=self.status)
354
355
356 if hasattr(stream, 'name'):
357 filename = stream.name
358
359 if filename and not 'content-type' in keys:
360 headers['Content-Type'] = contenttype(filename)
361 if filename and not 'content-length' in keys:
362 try:
363 headers['Content-Length'] = \
364 os.path.getsize(filename)
365 except OSError:
366 pass
367
368 env = request.env
369
370 if request.is_https and isinstance(env.http_user_agent, str) and \
371 not re.search(r'Opera', env.http_user_agent) and \
372 re.search(r'MSIE [5-8][^0-9]', env.http_user_agent):
373 headers['Pragma'] = 'cache'
374 headers['Cache-Control'] = 'private'
375
376 if request and env.web2py_use_wsgi_file_wrapper:
377 wrapped = env.wsgi_file_wrapper(stream, chunk_size)
378 else:
379 wrapped = streamer(stream, chunk_size=chunk_size)
380 return wrapped
381
383 """
384 example of usage in controller::
385
386 def download():
387 return response.download(request, db)
388
389 downloads from http://..../download/filename
390 """
391
392 current.session.forget(current.response)
393
394 if not request.args:
395 raise HTTP(404)
396 name = request.args[-1]
397 items = re.compile('(?P<table>.*?)\.(?P<field>.*?)\..*')\
398 .match(name)
399 if not items:
400 raise HTTP(404)
401 (t, f) = (items.group('table'), items.group('field'))
402 try:
403 field = db[t][f]
404 except AttributeError:
405 raise HTTP(404)
406 try:
407 (filename, stream) = field.retrieve(name,nameonly=True)
408 except IOError:
409 raise HTTP(404)
410 headers = self.headers
411 headers['Content-Type'] = contenttype(name)
412 if download_filename == None:
413 download_filename = filename
414 if attachment:
415 headers['Content-Disposition'] = \
416 'attachment; filename="%s"' % download_filename.replace('"','\"')
417 return self.stream(stream, chunk_size=chunk_size, request=request)
418
419 - def json(self, data, default=None):
421
422 - def xmlrpc(self, request, methods):
423 """
424 assuming::
425
426 def add(a, b):
427 return a+b
428
429 if a controller function \"func\"::
430
431 return response.xmlrpc(request, [add])
432
433 the controller will be able to handle xmlrpc requests for
434 the add function. Example::
435
436 import xmlrpclib
437 connection = xmlrpclib.ServerProxy(
438 'http://hostname/app/contr/func')
439 print connection.add(3, 4)
440
441 """
442
443 return handler(request, self, methods)
444
486
487
489
490 """
491 defines the session object and the default values of its members (None)
492 """
493
494 - def connect(
495 self,
496 request=None,
497 response=None,
498 db=None,
499 tablename='web2py_session',
500 masterapp=None,
501 migrate=True,
502 separate=None,
503 check_client=False,
504 cookie_key=None,
505 cookie_expires=None,
506 compression_level=None
507 ):
508 """
509 separate can be separate=lambda(session_name): session_name[-2:]
510 and it is used to determine a session prefix.
511 separate can be True and it is set to session_name[-2:]
512 """
513 if request is None:
514 request = current.request
515 if response is None:
516 response = current.response
517 if separate is True:
518 separate = lambda session_name: session_name[-2:]
519 self._unlock(response)
520 if not masterapp:
521 masterapp = request.application
522 response.session_id_name = 'session_id_%s' % masterapp.lower()
523 response.session_data_name = 'session_data_%s' % masterapp.lower()
524 response.session_cookie_expires = cookie_expires
525
526
527 cookies = request.cookies
528
529
530 if response.session_id_name in cookies:
531 response.session_id = \
532 cookies[response.session_id_name].value
533 else:
534 response.session_id = None
535
536
537 if response.session_data_name in cookies:
538 session_cookie_data = cookies[response.session_data_name].value
539 else:
540 session_cookie_data = None
541
542
543 if cookie_key:
544 response.session_storage_type = 'cookie'
545 response.session_cookie_key = cookie_key
546 response.session_cookie_compression_level = compression_level
547 if session_cookie_data:
548 data = secure_loads(session_cookie_data, cookie_key,
549 compression_level=compression_level)
550 if data:
551 self.update(data)
552
553 elif not db:
554 response.session_storage_type = 'file'
555 if global_settings.db_sessions is True \
556 or masterapp in global_settings.db_sessions:
557 return
558 response.session_new = False
559 client = request.client and request.client.replace(':', '.')
560 if response.session_id:
561 if regex_session_id.match(response.session_id):
562 response.session_filename = \
563 os.path.join(up(request.folder), masterapp,
564 'sessions', response.session_id)
565 else:
566 response.session_id = None
567
568 if response.session_id and not session_cookie_data:
569
570 try:
571 response.session_file = \
572 open(response.session_filename, 'rb+')
573 try:
574 portalocker.lock(response.session_file,
575 portalocker.LOCK_EX)
576 response.session_locked = True
577 self.update(cPickle.load(response.session_file))
578 response.session_file.seek(0)
579 oc = response.session_filename.split('/')[-1]\
580 .split('-')[0]
581 if check_client and client != oc:
582 raise Exception("cookie attack")
583 except:
584 response.session_id = None
585 finally:
586 pass
587
588
589 except:
590 response.session_file = None
591 if not response.session_id:
592 uuid = web2py_uuid()
593 response.session_id = '%s-%s' % (client, uuid)
594 if separate:
595 prefix = separate(response.session_id)
596 response.session_id = '%s/%s' % \
597 (prefix, response.session_id)
598 response.session_filename = \
599 os.path.join(up(request.folder), masterapp,
600 'sessions', response.session_id)
601 response.session_new = True
602
603 else:
604 response.session_storage_type = 'db'
605 if global_settings.db_sessions is not True:
606 global_settings.db_sessions.add(masterapp)
607 if response.session_file:
608 self._close(response)
609 if settings.global_settings.web2py_runtime_gae:
610
611 request.tickets_db = db
612 if masterapp == request.application:
613 table_migrate = migrate
614 else:
615 table_migrate = False
616 tname = tablename + '_' + masterapp
617 table = db.get(tname, None)
618 Field = db.Field
619 if table is None:
620 db.define_table(
621 tname,
622 Field('locked', 'boolean', default=False),
623 Field('client_ip', length=64),
624 Field('created_datetime', 'datetime',
625 default=request.now),
626 Field('modified_datetime', 'datetime'),
627 Field('unique_key', length=64),
628 Field('session_data', 'blob'),
629 migrate=table_migrate,
630 )
631 table = db[tname]
632 try:
633
634
635 (record_id, unique_key) = response.session_id.split(':')
636 if record_id == '0':
637 raise Exception('record_id == 0')
638
639 if not session_cookie_data:
640 rows = db(table.id == record_id).select()
641
642 if len(rows) == 0 or rows[0].unique_key != unique_key:
643 raise Exception('No record')
644
645
646 session_data = cPickle.loads(rows[0].session_data)
647 self.update(session_data)
648 except Exception:
649 record_id = None
650 unique_key = web2py_uuid()
651 session_data = {}
652 response.session_id = '%s:%s' % (record_id, unique_key)
653 response.session_db_table = table
654 response.session_db_record_id = record_id
655 response.session_db_unique_key = unique_key
656 rcookies = response.cookies
657 rcookies[response.session_id_name] = response.session_id
658 rcookies[response.session_id_name]['path'] = '/'
659 if cookie_expires:
660 rcookies[response.session_id_name][
661 'expires'] = cookie_expires.strftime(FMT)
662
663
664 if session_cookie_data:
665 rcookies[response.session_data_name] = 'expired'
666 rcookies[response.session_data_name]['path'] = '/'
667 rcookies[response.session_data_name]['expires'] = PAST
668 if self.flash:
669 (response.flash, self.flash) = (self.flash, None)
670
672 previous_session_hash = self.pop('_session_hash', None)
673 Storage.clear(self)
674 if previous_session_hash:
675 self._session_hash = previous_session_hash
676
678 if self._start_timestamp:
679 return False
680 else:
681 self._start_timestamp = datetime.datetime.today()
682 return True
683
685 now = datetime.datetime.today()
686 if not self._last_timestamp or \
687 self._last_timestamp + datetime.timedelta(seconds=seconds) > now:
688 self._last_timestamp = now
689 return False
690 else:
691 return True
692
695
696 - def forget(self, response=None):
699
701 if response.session_storage_type != 'cookie':
702 return False
703 name = response.session_data_name
704 value = secure_dumps(dict(self), response.session_cookie_key, compression_level=response.session_cookie_compression_level)
705 expires = response.session_cookie_expires
706 rcookies = response.cookies
707 rcookies.pop(name, None)
708 rcookies[name] = value
709 rcookies[name]['path'] = '/'
710 if expires:
711 rcookies[name]['expires'] = expires.strftime(FMT)
712 return True
713
715 previous_session_hash = self.pop('_session_hash', None)
716 if not previous_session_hash and not \
717 any(value is not None for value in self.itervalues()):
718 return True
719 session_pickled = cPickle.dumps(dict(self))
720 session_hash = hashlib.md5(session_pickled).hexdigest()
721 if previous_session_hash == session_hash:
722 return True
723 else:
724 self._session_hash = session_hash
725 return False
726
728
729
730
731 if response.session_storage_type != 'db' or not response.session_id \
732 or self._forget or self._unchanged():
733 return False
734
735 table = response.session_db_table
736 record_id = response.session_db_record_id
737 unique_key = response.session_db_unique_key
738
739 dd = dict(locked=False,
740 client_ip=request.client.replace(':', '.'),
741 modified_datetime=request.now,
742 session_data=cPickle.dumps(dict(self)),
743 unique_key=unique_key)
744 if record_id:
745 table._db(table.id == record_id).update(**dd)
746 else:
747 record_id = table.insert(**dd)
748
749 cookies, session_id_name = response.cookies, response.session_id_name
750 cookies[session_id_name] = '%s:%s' % (record_id, unique_key)
751 cookies[session_id_name]['path'] = '/'
752 return True
753
758
780
788
797