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