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