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