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