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.now() 159 - utcnow : datetime.datetime.utcnow() 160 - is_local 161 - is_https 162 - restful() 163 """ 164
165 - def __init__(self, env):
166 Storage.__init__(self) 167 self.env = Storage(env) 168 self.env.web2py_path = global_settings.applications_parent 169 self.env.update(global_settings) 170 self.cookies = Cookie.SimpleCookie() 171 self._get_vars = None 172 self._post_vars = None 173 self._vars = None 174 self._body = None 175 self.folder = None 176 self.application = None 177 self.function = None 178 self.args = List() 179 self.extension = 'html' 180 self.now = datetime.datetime.now() 181 self.utcnow = datetime.datetime.utcnow() 182 self.is_restful = False 183 self.is_https = False 184 self.is_local = False 185 self.global_settings = settings.global_settings
186 187
188 - def parse_get_vars(self):
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 #if content-type is application/json, we must read the body 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 # incoherent request bodies can still be parsed "ad-hoc" 213 json_vars = {} 214 pass 215 # update vars and get_vars with what was posted as json 216 if isinstance(json_vars, dict): 217 post_vars.update(json_vars) 218 219 body.seek(0) 220 221 # parse POST variables on POST, PUT, BOTH only in post_vars 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 # The same detection used by FieldStorage to detect multipart POSTs 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 # not sure why cgi.FieldStorage returns None key 243 dpk = dpost[key] 244 # if an element is not a file replace it with 245 # its value else leave it alone 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
255 - def body(self):
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
263 - def parse_all_vars(self):
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
276 - def get_vars(self):
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
292 - def vars(self):
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
299 - def compute_uuid(self):
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
307 - def user_agent(self):
308 from gluon.contrib import user_agent_parser 309 session = current.session 310 user_agent = session._user_agent 311 if user_agent: 312 return user_agent 313 user_agent = user_agent_parser.detect(self.env.http_user_agent) 314 for key, value in user_agent.items(): 315 if isinstance(value, dict): 316 user_agent[key] = Storage(value) 317 user_agent = session._user_agent = Storage(user_agent) 318 return user_agent
319
320 - def requires_https(self):
321 """ 322 If request comes in over HTTP, redirects it to HTTPS 323 and secures the session. 324 """ 325 cmd_opts = global_settings.cmd_options 326 #checking if this is called within the scheduler or within the shell 327 #in addition to checking if it's not a cronjob 328 if ((cmd_opts and (cmd_opts.shell or cmd_opts.scheduler)) 329 or global_settings.cronjob or self.is_https): 330 current.session.secure() 331 else: 332 current.session.forget() 333 redirect(URL(scheme='https', args=self.args, vars=self.vars))
334
335 - def restful(self):
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
361 362 -class Response(Storage):
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
369 - def __init__(self):
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 = '' # used by the default view layout 379 self.meta = Storage() # used by web2py_ajax.html 380 self.menu = [] # used by the default view layout 381 self.files = [] # used by web2py_ajax.html 382 self.generic_patterns = [] # patterns to allow generic views 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):
391 if not escape: 392 self.body.write(str(data)) 393 else: 394 self.body.write(xmlescape(data))
395
396 - def render(self, *a, **b):
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
426 - def include_meta(self):
427 s = '\n'.join( 428 '<meta name="%s" content="%s" />\n' % (k, xmlescape(v)) 429 for k, v in (self.meta or {}).iteritems()) 430 self.write(s, escape=False)
431
432 - def include_files(self, extensions=None):
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 # cache for 5 minutes by default 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 # http://www.typescriptlang.org/ 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 # for attachment settings and backward compatibility 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 # ## the following is for backward compatibility 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 # Internet Explorer < 9.0 will not allow downloads over SSL unless caching is enabled 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
577 - def download(self, request, db, chunk_size=DEFAULT_CHUNK_SIZE, attachment=True, download_filename=None):
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):
614 return json(data, default=default or custom_json)
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
639 - def toolbar(self):
640 from html import DIV, SCRIPT, BEAUTIFY, TAG, URL, A 641 BUTTON = TAG.button 642 admin = URL("admin", "default", "design", extension='html', 643 args=current.request.application) 644 from gluon.dal import DAL 645 dbstats = [] 646 dbtables = {} 647 infos = DAL.get_instances() 648 for k,v in infos.iteritems(): 649 dbstats.append(TABLE(*[TR(PRE(row[0]),'%.2fms' % 650 (row[1]*1000)) 651 for row in v['dbstats']])) 652 dbtables[k] = dict(defined=v['dbtables']['defined'] or '[no defined tables]', 653 lazy=v['dbtables']['lazy'] or '[no lazy tables]') 654 u = web2py_uuid() 655 backtotop = A('Back to top', _href="#totop-%s" % u) 656 # Convert lazy request.vars from property to Storage so they 657 # will be displayed in the toolbar. 658 request = copy.copy(current.request) 659 request.update(vars=current.request.vars, 660 get_vars=current.request.get_vars, 661 post_vars=current.request.post_vars) 662 return DIV( 663 BUTTON('design', _onclick="document.location='%s'" % admin), 664 BUTTON('request', 665 _onclick="jQuery('#request-%s').slideToggle()" % u), 666 BUTTON('response', 667 _onclick="jQuery('#response-%s').slideToggle()" % u), 668 BUTTON('session', 669 _onclick="jQuery('#session-%s').slideToggle()" % u), 670 BUTTON('db tables', 671 _onclick="jQuery('#db-tables-%s').slideToggle()" % u), 672 BUTTON('db stats', 673 _onclick="jQuery('#db-stats-%s').slideToggle()" % u), 674 DIV(BEAUTIFY(request), backtotop, 675 _class="hidden", _id="request-%s" % u), 676 DIV(BEAUTIFY(current.session), backtotop, 677 _class="hidden", _id="session-%s" % u), 678 DIV(BEAUTIFY(current.response), backtotop, 679 _class="hidden", _id="response-%s" % u), 680 DIV(BEAUTIFY(dbtables), backtotop, _class="hidden", 681 _id="db-tables-%s" % u), 682 DIV(BEAUTIFY( 683 dbstats), backtotop, _class="hidden", _id="db-stats-%s" % u), 684 SCRIPT("jQuery('.hidden').hide()"), _id="totop-%s" % u 685 )
686
687 688 -class Session(Storage):
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 # check if there is a session_id in cookies 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 # if we are supposed to use cookie based session data 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 # why do we do this? 786 # because connect may be called twice, by web2py and in models. 787 # the first time there is no db yet so it should do nothing 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 # check if there is session data in cookies 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 # else if we are supposed to use file based sessions 806 elif response.session_storage_type == 'file': 807 response.session_new = False 808 response.session_file = None 809 # check if the session_id points to a valid sesion filename 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 # else the session goes in db 843 elif response.session_storage_type == 'db': 844 if global_settings.db_sessions is not True: 845 global_settings.db_sessions.add(masterapp) 846 # if had a session on file alreday, close it (yes, can happen) 847 if response.session_file: 848 self._close(response) 849 # if on GAE tickets go also in DB 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] # to allow for lazy table 872 response.session_db_table = table 873 if response.session_id: 874 # Get session data out of the database 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 # Select from database 882 if record_id: 883 row = table(record_id, unique_key=unique_key) 884 # Make sure the session data exists in the database 885 if row: 886 # rows[0].update_record(locked=True) 887 # Unpickle the data 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 # if there is no session id yet, we'll need to create a 901 # new session 902 else: 903 response.session_new = True 904 905 # set the cookie now if you know the session_id so user can set 906 # cookie attributes in controllers/models 907 # cookie will be reset later 908 # yet cookie may be reset later 909 # Removed comparison between old and new session ids - should send 910 # the cookie all the time 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):
926 927 if clear_session: 928 self.clear() 929 930 request = current.request 931 response = current.response 932 session = response.session 933 masterapp = response.session_masterapp 934 cookies = request.cookies 935 936 if response.session_storage_type == 'cookie': 937 return 938 939 # if the session goes in file 940 if response.session_storage_type == 'file': 941 self._close(response) 942 uuid = web2py_uuid() 943 response.session_id = '%s-%s' % (response.session_client, uuid) 944 separate = (lambda s: s[-2:]) if session and response.session_id[2:3]=="/" else None 945 if separate: 946 prefix = separate(response.session_id) 947 response.session_id = '%s/%s' % \ 948 (prefix, response.session_id) 949 response.session_filename = \ 950 os.path.join(up(request.folder), masterapp, 951 'sessions', response.session_id) 952 response.session_new = True 953 954 # else the session goes in db 955 elif response.session_storage_type == 'db': 956 table = response.session_db_table 957 958 # verify that session_id exists 959 if response.session_file: 960 self._close(response) 961 if response.session_new: 962 return 963 # Get session data out of the database 964 if response.session_id is None: 965 return 966 (record_id, sep, unique_key) = response.session_id.partition(':') 967 968 if record_id.isdigit() and long(record_id)>0: 969 new_unique_key = web2py_uuid() 970 row = table(record_id) 971 if row and row.unique_key==unique_key: 972 table._db(table.id==record_id).update(unique_key=new_unique_key) 973 else: 974 record_id = None 975 if record_id: 976 response.session_id = '%s:%s' % (record_id, new_unique_key) 977 response.session_db_record_id = record_id 978 response.session_db_unique_key = new_unique_key 979 else: 980 response.session_new = True
981
982 - def _fixup_before_save(self):
983 response = current.response 984 rcookies = response.cookies 985 if self._forget and response.session_id_name in rcookies: 986 del rcookies[response.session_id_name] 987 elif self._secure and response.session_id_name in rcookies: 988 rcookies[response.session_id_name]['secure'] = True
989
990 - def clear_session_cookies(sefl):
991 request = current.request 992 response = current.response 993 session = response.session 994 masterapp = response.session_masterapp 995 cookies = request.cookies 996 rcookies = response.cookies 997 # if not cookie_key, but session_data_name in cookies 998 # expire session_data_name from cookies 999 if response.session_data_name in cookies: 1000 rcookies[response.session_data_name] = 'expired' 1001 rcookies[response.session_data_name]['path'] = '/' 1002 rcookies[response.session_data_name]['expires'] = PAST 1003 if response.session_id_name in rcookies: 1004 del rcookies[response.session_id_name]
1005 1029
1030 - def clear(self):
1031 Storage.clear(self)
1032
1033 - def is_new(self):
1034 if self._start_timestamp: 1035 return False 1036 else: 1037 self._start_timestamp = datetime.datetime.today() 1038 return True
1039
1040 - def is_expired(self, seconds=3600):
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
1049 - def secure(self):
1050 self._secure = True
1051
1052 - def forget(self, response=None):
1053 self._close(response) 1054 self._forget = True
1055 1076
1077 - def _unchanged(self,response):
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
1083 - def _try_store_in_db(self, request, response):
1084 # don't save if file-based sessions, 1085 # no session id, or session being forgotten 1086 # or no changes to session (Unless the session is new) 1087 if (not response.session_db_table or 1088 self._forget or 1089 (self._unchanged(response) and not response.session_new)): 1090 if (not response.session_db_table and 1091 global_settings.db_sessions is not True and 1092 response.session_masterapp in global_settings.db_sessions): 1093 global_settings.db_sessions.remove(response.session_masterapp) 1094 # self.clear_session_cookies() 1095 self.save_session_id_cookie() 1096 return False 1097 1098 table = response.session_db_table 1099 record_id = response.session_db_record_id 1100 if response.session_new: 1101 unique_key = web2py_uuid() 1102 else: 1103 unique_key = response.session_db_unique_key 1104 1105 session_pickled = response.session_pickled or cPickle.dumps(self) 1106 1107 dd = dict(locked=False, 1108 client_ip=response.session_client, 1109 modified_datetime=request.now, 1110 session_data=session_pickled, 1111 unique_key=unique_key) 1112 if record_id: 1113 if not table._db(table.id==record_id).update(**dd): 1114 record_id = None 1115 if not record_id: 1116 record_id = table.insert(**dd) 1117 response.session_id = '%s:%s' % (record_id, unique_key) 1118 response.session_db_unique_key = unique_key 1119 response.session_db_record_id = record_id 1120 1121 self.save_session_id_cookie() 1122 return True
1123 1129
1130 - def _try_store_in_file(self, request, response):
1131 try: 1132 if (not response.session_id or self._forget 1133 or self._unchanged(response)): 1134 # self.clear_session_cookies() 1135 self.save_session_id_cookie() 1136 return False 1137 if response.session_new or not response.session_file: 1138 # Tests if the session sub-folder exists, if not, create it 1139 session_folder = os.path.dirname(response.session_filename) 1140 if not os.path.exists(session_folder): os.mkdir(session_folder) 1141 response.session_file = open(response.session_filename, 'wb') 1142 portalocker.lock(response.session_file, portalocker.LOCK_EX) 1143 response.session_locked = True 1144 if response.session_file: 1145 session_pickled = response.session_pickled or cPickle.dumps(self) 1146 response.session_file.write(session_pickled) 1147 response.session_file.truncate() 1148 finally: 1149 self._close(response) 1150 1151 self.save_session_id_cookie() 1152 return True
1153
1154 - def _unlock(self, response):
1155 if response and response.session_file and response.session_locked: 1156 try: 1157 portalocker.unlock(response.session_file) 1158 response.session_locked = False 1159 except: # this should never happen but happens in Windows 1160 pass
1161
1162 - def _close(self, response):
1163 if response and response.session_file: 1164 self._unlock(response) 1165 try: 1166 response.session_file.close() 1167 del response.session_file 1168 except: 1169 pass
1170