Package gluon :: Module globals
[hide private]
[frames] | no frames]

Source Code for Module gluon.globals

   1  #!/usr/bin/env python 
   2  # -*- coding: utf-8 -*- 
   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 #external installed library 
  59  except: 
  60      try: 
  61          import json as sj #standard installed library 
  62      except: 
  63          import gluon.contrib.simplejson as sj #pure python library 
  64   
  65  regex_session_id = re.compile('^([\w\-]+/)?[\w\-\.]+$') 
  66   
  67  __all__ = ['Request', 'Response', 'Session'] 
  68   
  69  current = threading.local()  # thread-local storage for request-scope globals 
  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>' 
78 79 # IMPORTANT: 80 # this is required so that pickled dict(s) and class.__dict__ 81 # are sorted and web2py can detect without ambiguity when a session changes 82 -class SortingPickler(Pickler):
83 - def save_dict(self, obj):
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
90 91 -def sorting_dumps(obj, protocol=None):
92 file = cStringIO.StringIO() 93 SortingPickler(file, protocol).dump(obj) 94 return file.getvalue()
95 # END #####################################################################
96 97 -def copystream_progress(request, chunk_size=10 ** 5):
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: # Android requires this 112 dest = tempfile.NamedTemporaryFile() 113 except NotImplementedError: # and GAE this 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) # same as cache.ram because meta_storage 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
143 -class Request(Storage):
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
162 - def __init__(self, env):
163 Storage.__init__(self) 164 self.env = Storage(env) 165 self.env.web2py_path = global_settings.applications_parent 166 self.env.update(global_settings) 167 self.cookies = Cookie.SimpleCookie() 168 self._get_vars = None 169 self._post_vars = None 170 self._vars = None 171 self._body = None 172 self.folder = None 173 self.application = None 174 self.function = None 175 self.args = List() 176 self.extension = 'html' 177 self.now = datetime.datetime.now() 178 self.utcnow = datetime.datetime.utcnow() 179 self.is_restful = False 180 self.is_https = False 181 self.is_local = False 182 self.global_settings = settings.global_settings
183 184
185 - def parse_get_vars(self):
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 #if content-type is application/json, we must read the body 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 # incoherent request bodies can still be parsed "ad-hoc" 205 json_vars = {} 206 pass 207 # update vars and get_vars with what was posted as json 208 if isinstance(json_vars, dict): 209 post_vars.update(json_vars) 210 211 body.seek(0) 212 213 # parse POST variables on POST, PUT, BOTH only in post_vars 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 # The same detection used by FieldStorage to detect multipart POSTs 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 # not sure why cgi.FieldStorage returns None key 235 dpk = dpost[key] 236 # if an element is not a file replace it with 237 # its value else leave it alone 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
247 - def body(self):
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
255 - def parse_all_vars(self):
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
266 - def get_vars(self):
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
280 - def vars(self):
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
286 - def compute_uuid(self):
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
294 - def user_agent(self):
295 from gluon.contrib import user_agent_parser 296 session = current.session 297 user_agent = session._user_agent or \ 298 user_agent_parser.detect(self.env.http_user_agent) 299 if session: 300 session._user_agent = user_agent 301 user_agent = Storage(user_agent) 302 for key, value in user_agent.items(): 303 if isinstance(value, dict): 304 user_agent[key] = Storage(value) 305 return user_agent
306
307 - def requires_https(self):
308 """ 309 If request comes in over HTTP, redirect it to HTTPS 310 and secure the session. 311 """ 312 cmd_opts = global_settings.cmd_options 313 #checking if this is called within the scheduler or within the shell 314 #in addition to checking if it's not a cronjob 315 if ((cmd_opts and (cmd_opts.shell or cmd_opts.scheduler)) 316 or global_settings.cronjob or self.is_https): 317 current.session.secure() 318 else: 319 current.session.forget() 320 redirect(URL(scheme='https', args=self.args, vars=self.vars))
321
322 - def restful(self):
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
348 349 -class Response(Storage):
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
356 - def __init__(self):
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 = '' # used by the default view layout 366 self.meta = Storage() # used by web2py_ajax.html 367 self.menu = [] # used by the default view layout 368 self.files = [] # used by web2py_ajax.html 369 self.generic_patterns = [] # patterns to allow generic views 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):
378 if not escape: 379 self.body.write(str(data)) 380 else: 381 self.body.write(xmlescape(data))
382
383 - def render(self, *a, **b):
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
413 - def include_meta(self):
414 s = '\n'.join( 415 '<meta name="%s" content="%s" />\n' % (k, xmlescape(v)) 416 for k, v in (self.meta or {}).iteritems()) 417 self.write(s, escape=False)
418
419 - def include_files(self, extensions=None):
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 # cache for 5 minutes by default 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 # http://www.typescriptlang.org/ 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 # for attachment settings and backward compatibility 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 # ## the following is for backward compatibility 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 # Internet Explorer < 9.0 will not allow downloads over SSL unless caching is enabled 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
560 - def download(self, request, db, chunk_size=DEFAULT_CHUNK_SIZE, attachment=True, download_filename=None):
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):
598 return json(data, default=default or custom_json)
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
623 - def toolbar(self):
624 from html import DIV, SCRIPT, BEAUTIFY, TAG, URL, A 625 BUTTON = TAG.button 626 admin = URL("admin", "default", "design", extension='html', 627 args=current.request.application) 628 from gluon.dal import DAL 629 dbstats = [] 630 dbtables = {} 631 infos = DAL.get_instances() 632 for k,v in infos.iteritems(): 633 dbstats.append(TABLE(*[TR(PRE(row[0]),'%.2fms' % 634 (row[1]*1000)) 635 for row in v['dbstats']])) 636 dbtables[k] = dict(defined=v['dbtables']['defined'] or '[no defined tables]', 637 lazy=v['dbtables']['lazy'] or '[no lazy tables]') 638 u = web2py_uuid() 639 backtotop = A('Back to top', _href="#totop-%s" % u) 640 # Convert lazy request.vars from property to Storage so they 641 # will be displayed in the toolbar. 642 request = copy.copy(current.request) 643 request.update(vars=current.request.vars, 644 get_vars=current.request.get_vars, 645 post_vars=current.request.post_vars) 646 return DIV( 647 BUTTON('design', _onclick="document.location='%s'" % admin), 648 BUTTON('request', 649 _onclick="jQuery('#request-%s').slideToggle()" % u), 650 BUTTON('response', 651 _onclick="jQuery('#response-%s').slideToggle()" % u), 652 BUTTON('session', 653 _onclick="jQuery('#session-%s').slideToggle()" % u), 654 BUTTON('db tables', 655 _onclick="jQuery('#db-tables-%s').slideToggle()" % u), 656 BUTTON('db stats', 657 _onclick="jQuery('#db-stats-%s').slideToggle()" % u), 658 DIV(BEAUTIFY(request), backtotop, 659 _class="hidden", _id="request-%s" % u), 660 DIV(BEAUTIFY(current.session), backtotop, 661 _class="hidden", _id="session-%s" % u), 662 DIV(BEAUTIFY(current.response), backtotop, 663 _class="hidden", _id="response-%s" % u), 664 DIV(BEAUTIFY(dbtables), backtotop, _class="hidden", 665 _id="db-tables-%s" % u), 666 DIV(BEAUTIFY( 667 dbstats), backtotop, _class="hidden", _id="db-stats-%s" % u), 668 SCRIPT("jQuery('.hidden').hide()"), _id="totop-%s" % u 669 )
670
671 672 -class Session(Storage):
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 # check if there is a session_id in cookies 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 # if we are supposed to use cookie based session data 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 # why do we do this? 753 # because connect may be called twice, by web2py and in models. 754 # the first time there is no db yet so it should do nothing 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 # check if there is session data in cookies 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 # else if we are supposed to use file based sessions 773 elif response.session_storage_type == 'file': 774 response.session_new = False 775 response.session_file = None 776 # check if the session_id points to a valid sesion filename 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 # else the session goes in db 810 elif response.session_storage_type == 'db': 811 if global_settings.db_sessions is not True: 812 global_settings.db_sessions.add(masterapp) 813 # if had a session on file alreday, close it (yes, can happen) 814 if response.session_file: 815 self._close(response) 816 # if on GAE tickets go also in DB 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] # to allow for lazy table 836 response.session_db_table = table 837 if response.session_id: 838 # Get session data out of the database 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 # Select from database 846 if record_id: 847 row = table(record_id) #,unique_key=unique_key) 848 # Make sure the session data exists in the database 849 if row: 850 # rows[0].update_record(locked=True) 851 # Unpickle the data 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 # if there is no session id yet, we'll need to create a 864 # new session 865 else: 866 response.session_new = True 867 868 # set the cookie now if you know the session_id so user can set 869 # cookie attributes in controllers/models 870 # cookie will be reset later 871 # yet cookie may be reset later 872 # Removed comparison between old and new session ids - should send 873 # the cookie all the time 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):
889 890 if clear_session: 891 self.clear() 892 893 request = current.request 894 response = current.response 895 session = response.session 896 masterapp = response.session_masterapp 897 cookies = request.cookies 898 899 if response.session_storage_type == 'cookie': 900 return 901 902 # if the session goes in file 903 if response.session_storage_type == 'file': 904 self._close(response) 905 uuid = web2py_uuid() 906 response.session_id = '%s-%s' % (response.session_client, uuid) 907 separate = (lambda s: s[-2:]) if session and response.session_id[2:3]=="/" else None 908 if separate: 909 prefix = separate(response.session_id) 910 response.session_id = '%s/%s' % \ 911 (prefix, response.session_id) 912 response.session_filename = \ 913 os.path.join(up(request.folder), masterapp, 914 'sessions', response.session_id) 915 response.session_new = True 916 917 # else the session goes in db 918 elif response.session_storage_type == 'db': 919 table = response.session_db_table 920 921 # verify that session_id exists 922 if response.session_file: 923 self._close(response) 924 if response.session_new: 925 return 926 # Get session data out of the database 927 if response.session_id is None: 928 return 929 (record_id, sep, unique_key) = response.session_id.partition(':') 930 931 if record_id.isdigit() and long(record_id)>0: 932 new_unique_key = web2py_uuid() 933 row = table(record_id) 934 if row and row.unique_key==unique_key: 935 table._db(table.id==record_id).update(unique_key=new_unique_key) 936 else: 937 record_id = None 938 if record_id: 939 response.session_id = '%s:%s' % (record_id, unique_key) 940 response.session_db_record_id = record_id 941 response.session_db_unique_key = new_unique_key 942 else: 943 response.session_new = True
944
945 - def _fixup_before_save(self):
946 response = current.response 947 rcookies = response.cookies 948 if self._forget and response.session_id_name in rcookies: 949 del rcookies[response.session_id_name] 950 elif self._secure and response.session_id_name in rcookies: 951 rcookies[response.session_id_name]['secure'] = True
952
953 - def clear_session_cookies(sefl):
954 request = current.request 955 response = current.response 956 session = response.session 957 masterapp = response.session_masterapp 958 cookies = request.cookies 959 rcookies = response.cookies 960 # if not cookie_key, but session_data_name in cookies 961 # expire session_data_name from cookies 962 if response.session_data_name in cookies: 963 rcookies[response.session_data_name] = 'expired' 964 rcookies[response.session_data_name]['path'] = '/' 965 rcookies[response.session_data_name]['expires'] = PAST 966 if response.session_id_name in rcookies: 967 del rcookies[response.session_id_name]
968 991
992 - def clear(self):
993 Storage.clear(self)
994
995 - def is_new(self):
996 if self._start_timestamp: 997 return False 998 else: 999 self._start_timestamp = datetime.datetime.today() 1000 return True
1001
1002 - def is_expired(self, seconds=3600):
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
1011 - def secure(self):
1012 self._secure = True
1013
1014 - def forget(self, response=None):
1015 self._close(response) 1016 self._forget = True
1017 1038
1039 - def _unchanged(self,response):
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
1045 - def _try_store_in_db(self, request, response):
1046 # don't save if file-based sessions, 1047 # no session id, or session being forgotten 1048 # or no changes to session (Unless the session is new) 1049 if (not response.session_db_table or 1050 self._forget or 1051 (self._unchanged(response) and not response.session_new)): 1052 if (not response.session_db_table and 1053 global_settings.db_sessions is not True and 1054 response.session_masterapp in global_settings.db_sessions): 1055 global_settings.db_sessions.remove(response.session_masterapp) 1056 # self.clear_session_cookies() 1057 self.save_session_id_cookie() 1058 return False 1059 1060 table = response.session_db_table 1061 record_id = response.session_db_record_id 1062 if response.session_new: 1063 unique_key = web2py_uuid() 1064 else: 1065 unique_key = response.session_db_unique_key 1066 1067 session_pickled = response.session_pickled or cPickle.dumps(self) 1068 1069 dd = dict(locked=False, 1070 client_ip=response.session_client, 1071 modified_datetime=request.now, 1072 session_data=session_pickled, 1073 unique_key=unique_key) 1074 if record_id: 1075 if not table._db(table.id==record_id).update(**dd): 1076 record_id = None 1077 if not record_id: 1078 record_id = table.insert(**dd) 1079 response.session_id = '%s:%s' % (record_id, unique_key) 1080 response.session_db_unique_key = unique_key 1081 response.session_db_record_id = record_id 1082 1083 self.save_session_id_cookie() 1084 return True
1085 1091
1092 - def _try_store_in_file(self, request, response):
1093 try: 1094 if (not response.session_id or self._forget 1095 or self._unchanged(response)): 1096 # self.clear_session_cookies() 1097 self.save_session_id_cookie() 1098 return False 1099 if response.session_new or not response.session_file: 1100 # Tests if the session sub-folder exists, if not, create it 1101 session_folder = os.path.dirname(response.session_filename) 1102 if not os.path.exists(session_folder): os.mkdir(session_folder) 1103 response.session_file = open(response.session_filename, 'wb') 1104 portalocker.lock(response.session_file, portalocker.LOCK_EX) 1105 response.session_locked = True 1106 if response.session_file: 1107 session_pickled = response.session_pickled or cPickle.dumps(self) 1108 response.session_file.write(session_pickled) 1109 response.session_file.truncate() 1110 finally: 1111 self._close(response) 1112 1113 self.save_session_id_cookie() 1114 return True
1115
1116 - def _unlock(self, response):
1117 if response and response.session_file and response.session_locked: 1118 try: 1119 portalocker.unlock(response.session_file) 1120 response.session_locked = False 1121 except: # this should never happen but happens in Windows 1122 pass
1123
1124 - def _close(self, response):
1125 if response and response.session_file: 1126 self._unlock(response) 1127 try: 1128 response.session_file.close() 1129 del response.session_file 1130 except: 1131 pass
1132