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

Source Code for Module web2py.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 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 
 22  from http import HTTP 
 23  from fileutils import up 
 24  from serializers import json, custom_json 
 25  import settings 
 26  from utils import web2py_uuid 
 27  from settings import global_settings 
 28   
 29  import hashlib 
 30  import portalocker 
 31  import cPickle 
 32  import cStringIO 
 33  import datetime 
 34  import re 
 35  import Cookie 
 36  import os 
 37  import sys 
 38  import traceback 
 39  import threading 
 40   
 41  regex_session_id = re.compile('^([\w\-]+/)?[\w\-\.]+$') 
 42   
 43  __all__ = ['Request', 'Response', 'Session'] 
 44   
 45  current = threading.local()  # thread-local storage for request-scope globals 
 46   
47 -class Request(Storage):
48 49 """ 50 defines the request object and the default values of its members 51 52 - env: environment variables, by gluon.main.wsgibase() 53 - cookies 54 - get_vars 55 - post_vars 56 - vars 57 - folder 58 - application 59 - function 60 - args 61 - extension 62 - now: datetime.datetime.today() 63 - restful() 64 """ 65
66 - def __init__(self):
67 self.wsgi = Storage() # hooks to environ and start_response 68 self.env = Storage() 69 self.cookies = Cookie.SimpleCookie() 70 self.get_vars = Storage() 71 self.post_vars = Storage() 72 self.vars = Storage() 73 self.folder = None 74 self.application = None 75 self.function = None 76 self.args = List() 77 self.extension = None 78 self.now = datetime.datetime.now() 79 self.is_restful = False
80
81 - def compute_uuid(self):
82 self.uuid = '%s/%s.%s.%s' % ( 83 self.application, 84 self.client.replace(':', '_'), 85 self.now.strftime('%Y-%m-%d.%H-%M-%S'), 86 web2py_uuid()) 87 return self.uuid
88
89 - def restful(self):
90 def wrapper(action,self=self): 91 def f(_action=action,_self=self,*a,**b): 92 self.is_restful = True 93 method = _self.env.request_method 94 if len(_self.args) and '.' in _self.args[-1]: 95 _self.args[-1],_self.extension = _self.args[-1].rsplit('.',1) 96 if not method in ['GET','POST','DELETE','PUT']: 97 raise HTTP(400,"invalid method") 98 rest_action = _action().get(method,None) 99 if not rest_action: 100 raise HTTP(400,"method not supported") 101 try: 102 return rest_action(*_self.args,**_self.vars) 103 except TypeError, e: 104 exc_type, exc_value, exc_traceback = sys.exc_info() 105 if len(traceback.extract_tb(exc_traceback))==1: 106 raise HTTP(400,"invalid arguments") 107 else: 108 raise e
109 f.__doc__ = action.__doc__ 110 f.__name__ = action.__name__ 111 return f
112 return wrapper 113 114
115 -class Response(Storage):
116 117 """ 118 defines the response object and the default values of its members 119 response.write( ) can be used to write in the output html 120 """ 121
122 - def __init__(self):
123 self.status = 200 124 self.headers = Storage() 125 self.headers['X-Powered-By'] = 'web2py' 126 self.body = cStringIO.StringIO() 127 self.session_id = None 128 self.cookies = Cookie.SimpleCookie() 129 self.postprocessing = [] 130 self.flash = '' # used by the default view layout 131 self.meta = Storage() # used by web2py_ajax.html 132 self.menu = [] # used by the default view layout 133 self.files = [] # used by web2py_ajax.html 134 self._vars = None 135 self._caller = lambda f: f() 136 self._view_environment = None 137 self._custom_commit = None 138 self._custom_rollback = None
139
140 - def write(self, data, escape=True):
141 if not escape: 142 self.body.write(str(data)) 143 else: 144 self.body.write(xmlescape(data))
145
146 - def render(self, *a, **b):
147 from compileapp import run_view_in 148 if len(a) > 2: 149 raise SyntaxError, 'Response.render can be called with two arguments, at most' 150 elif len(a) == 2: 151 (view, self._vars) = (a[0], a[1]) 152 elif len(a) == 1 and isinstance(a[0], str): 153 (view, self._vars) = (a[0], {}) 154 elif len(a) == 1 and hasattr(a[0], 'read') and callable(a[0].read): 155 (view, self._vars) = (a[0], {}) 156 elif len(a) == 1 and isinstance(a[0], dict): 157 (view, self._vars) = (None, a[0]) 158 else: 159 (view, self._vars) = (None, {}) 160 self._vars.update(b) 161 self._view_environment.update(self._vars) 162 if view: 163 import cStringIO 164 (obody, oview) = (self.body, self.view) 165 (self.body, self.view) = (cStringIO.StringIO(), view) 166 run_view_in(self._view_environment) 167 page = self.body.getvalue() 168 self.body.close() 169 (self.body, self.view) = (obody, oview) 170 else: 171 run_view_in(self._view_environment) 172 page = self.body.getvalue() 173 return page
174
175 - def stream( 176 self, 177 stream, 178 chunk_size = DEFAULT_CHUNK_SIZE, 179 request=None, 180 ):
181 """ 182 if a controller function:: 183 184 return response.stream(file, 100) 185 186 the file content will be streamed at 100 bytes at the time 187 """ 188 189 if isinstance(stream, (str, unicode)): 190 stream_file_or_304_or_206(stream, 191 chunk_size=chunk_size, 192 request=request, 193 headers=self.headers) 194 195 # ## the following is for backward compatibility 196 197 if hasattr(stream, 'name'): 198 filename = stream.name 199 else: 200 filename = None 201 keys = [item.lower() for item in self.headers] 202 if filename and not 'content-type' in keys: 203 self.headers['Content-Type'] = contenttype(filename) 204 if filename and not 'content-length' in keys: 205 try: 206 self.headers['Content-Length'] = \ 207 os.path.getsize(filename) 208 except OSError: 209 pass 210 if request and request.env.web2py_use_wsgi_file_wrapper: 211 wrapped = request.env.wsgi_file_wrapper(stream, chunk_size) 212 else: 213 wrapped = streamer(stream, chunk_size=chunk_size) 214 return wrapped
215
216 - def download(self, request, db, chunk_size = DEFAULT_CHUNK_SIZE, attachment=True):
217 """ 218 example of usage in controller:: 219 220 def download(): 221 return response.download(request, db) 222 223 downloads from http://..../download/filename 224 """ 225 226 import contenttype as c 227 if not request.args: 228 raise HTTP(404) 229 name = request.args[-1] 230 items = re.compile('(?P<table>.*?)\.(?P<field>.*?)\..*')\ 231 .match(name) 232 if not items: 233 raise HTTP(404) 234 (t, f) = (items.group('table'), items.group('field')) 235 field = db[t][f] 236 try: 237 (filename, stream) = field.retrieve(name) 238 except IOError: 239 raise HTTP(404) 240 self.headers['Content-Type'] = c.contenttype(name) 241 if attachment: 242 self.headers['Content-Disposition'] = \ 243 "attachment; filename=%s" % filename 244 return self.stream(stream, chunk_size = chunk_size, request=request)
245
246 - def json(self, data, default=None):
247 return json(data, default = default or custom_json)
248
249 - def xmlrpc(self, request, methods):
250 """ 251 assuming:: 252 253 def add(a, b): 254 return a+b 255 256 if a controller function \"func\":: 257 258 return response.xmlrpc(request, [add]) 259 260 the controller will be able to handle xmlrpc requests for 261 the add function. Example:: 262 263 import xmlrpclib 264 connection = xmlrpclib.ServerProxy('http://hostname/app/contr/func') 265 print connection.add(3, 4) 266 267 """ 268 269 return handler(request, self, methods)
270
271 - def toolbar(self):
272 from html import DIV, SCRIPT, BEAUTIFY, TAG, URL 273 BUTTON = TAG.button 274 admin = URL("admin","default","design", 275 args=current.request.application) 276 from gluon.dal import thread 277 dbstats = [TABLE(*[TR(PRE(row[0]),'%.2fms' % (row[1]*1000)) \ 278 for row in i.db._timings]) \ 279 for i in thread.instances] 280 return DIV( 281 BUTTON('design',_onclick="document.location='%s'" % admin), 282 BUTTON('request',_onclick="jQuery('#request').slideToggle()"), 283 DIV(BEAUTIFY(current.request),_class="hidden",_id="request"), 284 BUTTON('session',_onclick="jQuery('#session').slideToggle()"), 285 DIV(BEAUTIFY(current.session),_class="hidden",_id="session"), 286 BUTTON('response',_onclick="jQuery('#response').slideToggle()"), 287 DIV(BEAUTIFY(current.response),_class="hidden",_id="response"), 288 BUTTON('db stats',_onclick="jQuery('#db-stats').slideToggle()"), 289 DIV(BEAUTIFY(dbstats),_class="hidden",_id="db-stats"), 290 SCRIPT("jQuery('.hidden').hide()") 291 )
292
293 -class Session(Storage):
294 295 """ 296 defines the session object and the default values of its members (None) 297 """ 298
299 - def connect( 300 self, 301 request, 302 response, 303 db=None, 304 tablename='web2py_session', 305 masterapp=None, 306 migrate=True, 307 separate = None, 308 check_client=False, 309 ):
310 """ 311 separate can be separate=lambda(session_name): session_name[-2:] 312 and it is used to determine a session prefix. 313 separate can be True and it is set to session_name[-2:] 314 """ 315 if separate == True: 316 separate = lambda session_name: session_name[-2:] 317 self._unlock(response) 318 if not masterapp: 319 masterapp = request.application 320 response.session_id_name = 'session_id_%s' % masterapp.lower() 321 322 if not db: 323 if global_settings.db_sessions is True or masterapp in global_settings.db_sessions: 324 return 325 response.session_new = False 326 client = request.client.replace(':', '.') 327 if response.session_id_name in request.cookies: 328 response.session_id = \ 329 request.cookies[response.session_id_name].value 330 if regex_session_id.match(response.session_id): 331 response.session_filename = \ 332 os.path.join(up(request.folder), masterapp, 333 'sessions', response.session_id) 334 else: 335 response.session_id = None 336 if response.session_id: 337 try: 338 response.session_file = \ 339 open(response.session_filename, 'rb+') 340 portalocker.lock(response.session_file, 341 portalocker.LOCK_EX) 342 response.session_locked = True 343 self.update(cPickle.load(response.session_file)) 344 response.session_file.seek(0) 345 oc = response.session_filename.split('/')[-1].split('-')[0] 346 if check_client and client!=oc: 347 raise Exception, "cookie attack" 348 except: 349 self._close(response) 350 response.session_id = None 351 if not response.session_id: 352 uuid = web2py_uuid() 353 response.session_id = '%s-%s' % (client, uuid) 354 if separate: 355 prefix = separate(response.session_id) 356 response.session_id = '%s/%s' % (prefix,response.session_id) 357 response.session_filename = \ 358 os.path.join(up(request.folder), masterapp, 359 'sessions', response.session_id) 360 response.session_new = True 361 else: 362 if global_settings.db_sessions is not True: 363 global_settings.db_sessions.add(masterapp) 364 response.session_db = True 365 if response.session_file: 366 self._close(response) 367 if settings.global_settings.web2py_runtime_gae: 368 # in principle this could work without GAE 369 request.tickets_db = db 370 if masterapp == request.application: 371 table_migrate = migrate 372 else: 373 table_migrate = False 374 tname = tablename + '_' + masterapp 375 table = db.get(tname, None) 376 if table is None: 377 table = db.define_table( 378 tname, 379 db.Field('locked', 'boolean', default=False), 380 db.Field('client_ip', length=64), 381 db.Field('created_datetime', 'datetime', 382 default=request.now), 383 db.Field('modified_datetime', 'datetime'), 384 db.Field('unique_key', length=64), 385 db.Field('session_data', 'blob'), 386 migrate=table_migrate, 387 ) 388 try: 389 key = request.cookies[response.session_id_name].value 390 (record_id, unique_key) = key.split(':') 391 if record_id == '0': 392 raise Exception, 'record_id == 0' 393 rows = db(table.id == record_id).select() 394 if len(rows) == 0 or rows[0].unique_key != unique_key: 395 raise Exception, 'No record' 396 397 # rows[0].update_record(locked=True) 398 399 session_data = cPickle.loads(rows[0].session_data) 400 self.update(session_data) 401 except Exception: 402 record_id = None 403 unique_key = web2py_uuid() 404 session_data = {} 405 response._dbtable_and_field = \ 406 (response.session_id_name, table, record_id, unique_key) 407 response.session_id = '%s:%s' % (record_id, unique_key) 408 response.cookies[response.session_id_name] = response.session_id 409 response.cookies[response.session_id_name]['path'] = '/' 410 self.__hash = hashlib.md5(str(self)).digest() 411 if self.flash: 412 (response.flash, self.flash) = (self.flash, None)
413
414 - def is_new(self):
415 if self._start_timestamp: 416 return False 417 else: 418 self._start_timestamp = datetime.datetime.today() 419 return True
420
421 - def is_expired(self, seconds = 3600):
422 now = datetime.datetime.today() 423 if not self._last_timestamp or \ 424 self._last_timestamp + datetime.timedelta(seconds = seconds) > now: 425 self._last_timestamp = now 426 return False 427 else: 428 return True
429
430 - def secure(self):
431 self._secure = True
432
433 - def forget(self, response=None):
434 self._close(response) 435 self._forget = True
436
437 - def _try_store_in_db(self, request, response):
438 439 # don't save if file-based sessions, no session id, or session being forgotten 440 if not response.session_db or not response.session_id or self._forget: 441 return 442 443 # don't save if no change to session 444 __hash = self.__hash 445 if __hash is not None: 446 del self.__hash 447 if __hash == hashlib.md5(str(self)).digest(): 448 return 449 450 (record_id_name, table, record_id, unique_key) = \ 451 response._dbtable_and_field 452 dd = dict(locked=False, client_ip=request.env.remote_addr, 453 modified_datetime=request.now, 454 session_data=cPickle.dumps(dict(self)), 455 unique_key=unique_key) 456 if record_id: 457 table._db(table.id == record_id).update(**dd) 458 else: 459 record_id = table.insert(**dd) 460 response.cookies[response.session_id_name] = '%s:%s'\ 461 % (record_id, unique_key) 462 response.cookies[response.session_id_name]['path'] = '/'
463
464 - def _try_store_on_disk(self, request, response):
465 466 # don't save if sessions not not file-based 467 if response.session_db: 468 return 469 470 # don't save if no change to session 471 __hash = self.__hash 472 if __hash is not None: 473 del self.__hash 474 if __hash == hashlib.md5(str(self)).digest(): 475 self._close(response) 476 return 477 478 if not response.session_id or self._forget: 479 self._close(response) 480 return 481 482 if response.session_new: 483 # Tests if the session sub-folder exists, if not, create it 484 session_folder = os.path.dirname(response.session_filename) 485 if not os.path.exists(session_folder): 486 os.mkdir(session_folder) 487 response.session_file = open(response.session_filename, 'wb') 488 portalocker.lock(response.session_file, portalocker.LOCK_EX) 489 response.session_locked = True 490 491 if response.session_file: 492 cPickle.dump(dict(self), response.session_file) 493 response.session_file.truncate() 494 self._close(response)
495
496 - def _unlock(self, response):
497 if response and response.session_file and response.session_locked: 498 try: 499 portalocker.unlock(response.session_file) 500 response.session_locked = False 501 except: ### this should never happen but happens in Windows 502 pass
503
504 - def _close(self, response):
505 if response and response.session_file: 506 self._unlock(response) 507 try: 508 response.session_file.close() 509 del response.session_file 510 except: 511 pass
512