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 return DIV( 283 BUTTON('design',_onclick="document.location='%s'" % admin), 284 BUTTON('request',_onclick="jQuery('#request').slideToggle()"), 285 DIV(BEAUTIFY(current.request),_class="hidden",_id="request"), 286 BUTTON('session',_onclick="jQuery('#session').slideToggle()"), 287 DIV(BEAUTIFY(current.session),_class="hidden",_id="session"), 288 BUTTON('response',_onclick="jQuery('#response').slideToggle()"), 289 DIV(BEAUTIFY(current.response),_class="hidden",_id="response"), 290 BUTTON('db stats',_onclick="jQuery('#db-stats').slideToggle()"), 291 DIV(BEAUTIFY(dbstats),_class="hidden",_id="db-stats"), 292 SCRIPT("jQuery('.hidden').hide()") 293 )
294
295 -class Session(Storage):
296 297 """ 298 defines the session object and the default values of its members (None) 299 """ 300
301 - def connect( 302 self, 303 request, 304 response, 305 db=None, 306 tablename='web2py_session', 307 masterapp=None, 308 migrate=True, 309 separate = None, 310 check_client=False, 311 ):
312 """ 313 separate can be separate=lambda(session_name): session_name[-2:] 314 and it is used to determine a session prefix. 315 separate can be True and it is set to session_name[-2:] 316 """ 317 if separate == True: 318 separate = lambda session_name: session_name[-2:] 319 self._unlock(response) 320 if not masterapp: 321 masterapp = request.application 322 response.session_id_name = 'session_id_%s' % masterapp.lower() 323 324 if not db: 325 if global_settings.db_sessions is True or masterapp in global_settings.db_sessions: 326 return 327 response.session_new = False 328 client = request.client.replace(':', '.') 329 if response.session_id_name in request.cookies: 330 response.session_id = \ 331 request.cookies[response.session_id_name].value 332 if regex_session_id.match(response.session_id): 333 response.session_filename = \ 334 os.path.join(up(request.folder), masterapp, 335 'sessions', response.session_id) 336 else: 337 response.session_id = None 338 if response.session_id: 339 try: 340 response.session_file = \ 341 open(response.session_filename, 'rb+') 342 try: 343 portalocker.lock(response.session_file, 344 portalocker.LOCK_EX) 345 response.session_locked = True 346 self.update(cPickle.load(response.session_file)) 347 response.session_file.seek(0) 348 oc = response.session_filename.split('/')[-1].split('-')[0] 349 if check_client and client!=oc: 350 raise Exception, "cookie attack" 351 finally: 352 pass 353 #This causes admin login to break. Must find out why. 354 #self._close(response) 355 except: 356 response.session_id = None 357 if not response.session_id: 358 uuid = web2py_uuid() 359 response.session_id = '%s-%s' % (client, uuid) 360 if separate: 361 prefix = separate(response.session_id) 362 response.session_id = '%s/%s' % (prefix,response.session_id) 363 response.session_filename = \ 364 os.path.join(up(request.folder), masterapp, 365 'sessions', response.session_id) 366 response.session_new = True 367 else: 368 if global_settings.db_sessions is not True: 369 global_settings.db_sessions.add(masterapp) 370 response.session_db = True 371 if response.session_file: 372 self._close(response) 373 if settings.global_settings.web2py_runtime_gae: 374 # in principle this could work without GAE 375 request.tickets_db = db 376 if masterapp == request.application: 377 table_migrate = migrate 378 else: 379 table_migrate = False 380 tname = tablename + '_' + masterapp 381 table = db.get(tname, None) 382 if table is None: 383 table = db.define_table( 384 tname, 385 db.Field('locked', 'boolean', default=False), 386 db.Field('client_ip', length=64), 387 db.Field('created_datetime', 'datetime', 388 default=request.now), 389 db.Field('modified_datetime', 'datetime'), 390 db.Field('unique_key', length=64), 391 db.Field('session_data', 'blob'), 392 migrate=table_migrate, 393 ) 394 try: 395 key = request.cookies[response.session_id_name].value 396 (record_id, unique_key) = key.split(':') 397 if record_id == '0': 398 raise Exception, 'record_id == 0' 399 rows = db(table.id == record_id).select() 400 if len(rows) == 0 or rows[0].unique_key != unique_key: 401 raise Exception, 'No record' 402 403 # rows[0].update_record(locked=True) 404 405 session_data = cPickle.loads(rows[0].session_data) 406 self.update(session_data) 407 except Exception: 408 record_id = None 409 unique_key = web2py_uuid() 410 session_data = {} 411 response._dbtable_and_field = \ 412 (response.session_id_name, table, record_id, unique_key) 413 response.session_id = '%s:%s' % (record_id, unique_key) 414 response.cookies[response.session_id_name] = response.session_id 415 response.cookies[response.session_id_name]['path'] = '/' 416 self.__hash = hashlib.md5(str(self)).digest() 417 if self.flash: 418 (response.flash, self.flash) = (self.flash, None)
419
420 - def is_new(self):
421 if self._start_timestamp: 422 return False 423 else: 424 self._start_timestamp = datetime.datetime.today() 425 return True
426
427 - def is_expired(self, seconds = 3600):
428 now = datetime.datetime.today() 429 if not self._last_timestamp or \ 430 self._last_timestamp + datetime.timedelta(seconds = seconds) > now: 431 self._last_timestamp = now 432 return False 433 else: 434 return True
435
436 - def secure(self):
437 self._secure = True
438
439 - def forget(self, response=None):
440 self._close(response) 441 self._forget = True
442
443 - def _try_store_in_db(self, request, response):
444 445 # don't save if file-based sessions, no session id, or session being forgotten 446 if not response.session_db or not response.session_id or self._forget: 447 return 448 449 # don't save if no change to session 450 __hash = self.__hash 451 if __hash is not None: 452 del self.__hash 453 if __hash == hashlib.md5(str(self)).digest(): 454 return 455 456 (record_id_name, table, record_id, unique_key) = \ 457 response._dbtable_and_field 458 dd = dict(locked=False, client_ip=request.env.remote_addr, 459 modified_datetime=request.now, 460 session_data=cPickle.dumps(dict(self)), 461 unique_key=unique_key) 462 if record_id: 463 table._db(table.id == record_id).update(**dd) 464 else: 465 record_id = table.insert(**dd) 466 response.cookies[response.session_id_name] = '%s:%s'\ 467 % (record_id, unique_key) 468 response.cookies[response.session_id_name]['path'] = '/'
469
470 - def _try_store_on_disk(self, request, response):
471 472 # don't save if sessions not not file-based 473 if response.session_db: 474 return 475 476 # don't save if no change to session 477 __hash = self.__hash 478 if __hash is not None: 479 del self.__hash 480 if __hash == hashlib.md5(str(self)).digest(): 481 self._close(response) 482 return 483 484 if not response.session_id or self._forget: 485 self._close(response) 486 return 487 488 if response.session_new: 489 # Tests if the session sub-folder exists, if not, create it 490 session_folder = os.path.dirname(response.session_filename) 491 if not os.path.exists(session_folder): 492 os.mkdir(session_folder) 493 response.session_file = open(response.session_filename, 'wb') 494 portalocker.lock(response.session_file, portalocker.LOCK_EX) 495 response.session_locked = True 496 497 if response.session_file: 498 cPickle.dump(dict(self), response.session_file) 499 response.session_file.truncate() 500 self._close(response)
501
502 - def _unlock(self, response):
503 if response and response.session_file and response.session_locked: 504 try: 505 portalocker.unlock(response.session_file) 506 response.session_locked = False 507 except: ### this should never happen but happens in Windows 508 pass
509
510 - def _close(self, response):
511 if response and response.session_file: 512 self._unlock(response) 513 try: 514 response.session_file.close() 515 del response.session_file 516 except: 517 pass
518