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.delimiters = ('{{','}}') 172 self._vars = None 173 self._caller = lambda f: f() 174 self._view_environment = None 175 self._custom_commit = None 176 self._custom_rollback = None
177
178 - def write(self, data, escape=True):
179 if not escape: 180 self.body.write(str(data)) 181 else: 182 self.body.write(xmlescape(data))
183
184 - def render(self, *a, **b):
185 from compileapp import run_view_in 186 if len(a) > 2: 187 raise SyntaxError, 'Response.render can be called with two arguments, at most' 188 elif len(a) == 2: 189 (view, self._vars) = (a[0], a[1]) 190 elif len(a) == 1 and isinstance(a[0], str): 191 (view, self._vars) = (a[0], {}) 192 elif len(a) == 1 and hasattr(a[0], 'read') and callable(a[0].read): 193 (view, self._vars) = (a[0], {}) 194 elif len(a) == 1 and isinstance(a[0], dict): 195 (view, self._vars) = (None, a[0]) 196 else: 197 (view, self._vars) = (None, {}) 198 self._vars.update(b) 199 self._view_environment.update(self._vars) 200 if view: 201 import cStringIO 202 (obody, oview) = (self.body, self.view) 203 (self.body, self.view) = (cStringIO.StringIO(), view) 204 run_view_in(self._view_environment) 205 page = self.body.getvalue() 206 self.body.close() 207 (self.body, self.view) = (obody, oview) 208 else: 209 run_view_in(self._view_environment) 210 page = self.body.getvalue() 211 return page
212
213 - def include_meta(self):
214 s = '' 215 for key,value in (self.meta or {}).items(): 216 s += '<meta name="%s" content="%s" />' % (key,xmlescape(value)) 217 self.write(s,escape=False)
218
219 - def include_files(self):
220 221 222 """ 223 Caching method for writing out files. 224 By default, caches in ram for 5 minutes. To change, 225 response.cache_includes = (cache_method, time_expire). 226 Example: (cache.disk, 60) # caches to disk for 1 minute. 227 """ 228 from gluon import URL 229 230 files = [] 231 for item in self.files: 232 if not item in files: files.append(item) 233 if have_minify and (self.optimize_css or self.optimize_js): 234 # cache for 5 minutes by default 235 cache = self.cache_includes or (current.cache.ram, 60*5) 236 def call_minify(): 237 return minify.minify(files, 238 URL('static','temp'), 239 current.request.folder, 240 self.optimize_css, 241 self.optimize_js)
242 if cache: 243 cache_model, time_expire = cache 244 files = cache_model('response.files.minified', 245 call_minify, 246 time_expire) 247 else: 248 files = call_minify() 249 s = '' 250 for item in files: 251 if isinstance(item,str): 252 f = item.lower() 253 if f.endswith('.css'): s += css_template % item 254 elif f.endswith('.js'): s += js_template % item 255 elif isinstance(item,(list,tuple)): 256 f = item[0] 257 if f=='css:inline': s += css_inline % item[1] 258 elif f=='js:inline': s += js_inline % item[1] 259 self.write(s, escape=False)
260
261 - def stream( 262 self, 263 stream, 264 chunk_size = DEFAULT_CHUNK_SIZE, 265 request=None, 266 ):
267 """ 268 if a controller function:: 269 270 return response.stream(file, 100) 271 272 the file content will be streamed at 100 bytes at the time 273 """ 274 if not request: 275 request = current.request 276 if isinstance(stream, (str, unicode)): 277 stream_file_or_304_or_206(stream, 278 chunk_size=chunk_size, 279 request=request, 280 headers=self.headers) 281 282 # ## the following is for backward compatibility 283 284 if hasattr(stream, 'name'): 285 filename = stream.name 286 else: 287 filename = None 288 keys = [item.lower() for item in self.headers] 289 if filename and not 'content-type' in keys: 290 self.headers['Content-Type'] = contenttype(filename) 291 if filename and not 'content-length' in keys: 292 try: 293 self.headers['Content-Length'] = \ 294 os.path.getsize(filename) 295 except OSError: 296 pass 297 298 # Internet Explorer < 9.0 will not allow downloads over SSL unless caching is enabled 299 if request.is_https and isinstance(request.env.http_user_agent,str) and \ 300 not re.search(r'Opera', request.env.http_user_agent) and \ 301 re.search(r'MSIE [5-8][^0-9]', request.env.http_user_agent): 302 self.headers['Pragma'] = 'cache' 303 self.headers['Cache-Control'] = 'private' 304 305 if request and request.env.web2py_use_wsgi_file_wrapper: 306 wrapped = request.env.wsgi_file_wrapper(stream, chunk_size) 307 else: 308 wrapped = streamer(stream, chunk_size=chunk_size) 309 return wrapped
310
311 - def download(self, request, db, chunk_size = DEFAULT_CHUNK_SIZE, attachment=True):
312 """ 313 example of usage in controller:: 314 315 def download(): 316 return response.download(request, db) 317 318 downloads from http://..../download/filename 319 """ 320 321 import contenttype as c 322 if not request.args: 323 raise HTTP(404) 324 name = request.args[-1] 325 items = re.compile('(?P<table>.*?)\.(?P<field>.*?)\..*')\ 326 .match(name) 327 if not items: 328 raise HTTP(404) 329 (t, f) = (items.group('table'), items.group('field')) 330 field = db[t][f] 331 try: 332 (filename, stream) = field.retrieve(name) 333 except IOError: 334 raise HTTP(404) 335 self.headers['Content-Type'] = c.contenttype(name) 336 if attachment: 337 self.headers['Content-Disposition'] = \ 338 "attachment; filename=%s" % filename 339 return self.stream(stream, chunk_size = chunk_size, request=request)
340
341 - def json(self, data, default=None):
342 return json(data, default = default or custom_json)
343
344 - def xmlrpc(self, request, methods):
345 """ 346 assuming:: 347 348 def add(a, b): 349 return a+b 350 351 if a controller function \"func\":: 352 353 return response.xmlrpc(request, [add]) 354 355 the controller will be able to handle xmlrpc requests for 356 the add function. Example:: 357 358 import xmlrpclib 359 connection = xmlrpclib.ServerProxy('http://hostname/app/contr/func') 360 print connection.add(3, 4) 361 362 """ 363 364 return handler(request, self, methods)
365
366 - def toolbar(self):
367 from html import DIV, SCRIPT, BEAUTIFY, TAG, URL 368 BUTTON = TAG.button 369 admin = URL("admin","default","design", 370 args=current.request.application) 371 from gluon.dal import thread 372 if hasattr(thread,'instances'): 373 dbstats = [TABLE(*[TR(PRE(row[0]),'%.2fms' % (row[1]*1000)) \ 374 for row in i.db._timings]) \ 375 for i in thread.instances] 376 else: 377 dbstats = [] # if no db or on GAE 378 u = web2py_uuid() 379 return DIV( 380 BUTTON('design',_onclick="document.location='%s'" % admin), 381 BUTTON('request',_onclick="jQuery('#request-%s').slideToggle()"%u), 382 DIV(BEAUTIFY(current.request),_class="hidden",_id="request-%s"%u), 383 BUTTON('session',_onclick="jQuery('#session-%s').slideToggle()"%u), 384 DIV(BEAUTIFY(current.session),_class="hidden",_id="session-%s"%u), 385 BUTTON('response',_onclick="jQuery('#response-%s').slideToggle()"%u), 386 DIV(BEAUTIFY(current.response),_class="hidden",_id="response-%s"%u), 387 BUTTON('db stats',_onclick="jQuery('#db-stats-%s').slideToggle()"%u), 388 DIV(BEAUTIFY(dbstats),_class="hidden",_id="db-stats-%s"%u), 389 SCRIPT("jQuery('.hidden').hide()") 390 )
391
392 -class Session(Storage):
393 394 """ 395 defines the session object and the default values of its members (None) 396 """ 397
398 - def connect( 399 self, 400 request, 401 response, 402 db=None, 403 tablename='web2py_session', 404 masterapp=None, 405 migrate=True, 406 separate = None, 407 check_client=False, 408 ):
409 """ 410 separate can be separate=lambda(session_name): session_name[-2:] 411 and it is used to determine a session prefix. 412 separate can be True and it is set to session_name[-2:] 413 """ 414 if separate == True: 415 separate = lambda session_name: session_name[-2:] 416 self._unlock(response) 417 if not masterapp: 418 masterapp = request.application 419 response.session_id_name = 'session_id_%s' % masterapp.lower() 420 421 if not db: 422 if global_settings.db_sessions is True or masterapp in global_settings.db_sessions: 423 return 424 response.session_new = False 425 client = request.client and request.client.replace(':', '.') 426 if response.session_id_name in request.cookies: 427 response.session_id = \ 428 request.cookies[response.session_id_name].value 429 if regex_session_id.match(response.session_id): 430 response.session_filename = \ 431 os.path.join(up(request.folder), masterapp, 432 'sessions', response.session_id) 433 else: 434 response.session_id = None 435 if response.session_id: 436 try: 437 response.session_file = \ 438 open(response.session_filename, 'rb+') 439 try: 440 portalocker.lock(response.session_file,portalocker.LOCK_EX) 441 response.session_locked = True 442 self.update(cPickle.load(response.session_file)) 443 response.session_file.seek(0) 444 oc = response.session_filename.split('/')[-1].split('-')[0] 445 if check_client and client!=oc: 446 raise Exception, "cookie attack" 447 finally: 448 pass 449 #This causes admin login to break. Must find out why. 450 #self._close(response) 451 except: 452 response.session_id = None 453 if not response.session_id: 454 uuid = web2py_uuid() 455 response.session_id = '%s-%s' % (client, uuid) 456 if separate: 457 prefix = separate(response.session_id) 458 response.session_id = '%s/%s' % (prefix,response.session_id) 459 response.session_filename = \ 460 os.path.join(up(request.folder), masterapp, 461 'sessions', response.session_id) 462 response.session_new = True 463 else: 464 if global_settings.db_sessions is not True: 465 global_settings.db_sessions.add(masterapp) 466 response.session_db = True 467 if response.session_file: 468 self._close(response) 469 if settings.global_settings.web2py_runtime_gae: 470 # in principle this could work without GAE 471 request.tickets_db = db 472 if masterapp == request.application: 473 table_migrate = migrate 474 else: 475 table_migrate = False 476 tname = tablename + '_' + masterapp 477 table = db.get(tname, None) 478 if table is None: 479 table = db.define_table( 480 tname, 481 db.Field('locked', 'boolean', default=False), 482 db.Field('client_ip', length=64), 483 db.Field('created_datetime', 'datetime', 484 default=request.now), 485 db.Field('modified_datetime', 'datetime'), 486 db.Field('unique_key', length=64), 487 db.Field('session_data', 'blob'), 488 migrate=table_migrate, 489 ) 490 try: 491 key = request.cookies[response.session_id_name].value 492 (record_id, unique_key) = key.split(':') 493 if record_id == '0': 494 raise Exception, 'record_id == 0' 495 rows = db(table.id == record_id).select() 496 if len(rows) == 0 or rows[0].unique_key != unique_key: 497 raise Exception, 'No record' 498 499 # rows[0].update_record(locked=True) 500 501 session_data = cPickle.loads(rows[0].session_data) 502 self.update(session_data) 503 except Exception: 504 record_id = None 505 unique_key = web2py_uuid() 506 session_data = {} 507 response._dbtable_and_field = \ 508 (response.session_id_name, table, record_id, unique_key) 509 response.session_id = '%s:%s' % (record_id, unique_key) 510 response.cookies[response.session_id_name] = response.session_id 511 response.cookies[response.session_id_name]['path'] = '/' 512 self.__hash = hashlib.md5(str(self)).digest() 513 if self.flash: 514 (response.flash, self.flash) = (self.flash, None)
515
516 - def is_new(self):
517 if self._start_timestamp: 518 return False 519 else: 520 self._start_timestamp = datetime.datetime.today() 521 return True
522
523 - def is_expired(self, seconds = 3600):
524 now = datetime.datetime.today() 525 if not self._last_timestamp or \ 526 self._last_timestamp + datetime.timedelta(seconds = seconds) > now: 527 self._last_timestamp = now 528 return False 529 else: 530 return True
531
532 - def secure(self):
533 self._secure = True
534
535 - def forget(self, response=None):
536 self._close(response) 537 self._forget = True
538
539 - def _try_store_in_db(self, request, response):
540 541 # don't save if file-based sessions, no session id, or session being forgotten 542 if not response.session_db or not response.session_id or self._forget: 543 return 544 545 # don't save if no change to session 546 __hash = self.__hash 547 if __hash is not None: 548 del self.__hash 549 if __hash == hashlib.md5(str(self)).digest(): 550 return 551 552 (record_id_name, table, record_id, unique_key) = \ 553 response._dbtable_and_field 554 dd = dict(locked=False, client_ip=request.client.replace(':','.'), 555 modified_datetime=request.now, 556 session_data=cPickle.dumps(dict(self)), 557 unique_key=unique_key) 558 if record_id: 559 table._db(table.id == record_id).update(**dd) 560 else: 561 record_id = table.insert(**dd) 562 response.cookies[response.session_id_name] = '%s:%s'\ 563 % (record_id, unique_key) 564 response.cookies[response.session_id_name]['path'] = '/'
565
566 - def _try_store_on_disk(self, request, response):
567 568 # don't save if sessions not not file-based 569 if response.session_db: 570 return 571 572 # don't save if no change to session 573 __hash = self.__hash 574 if __hash is not None: 575 del self.__hash 576 if __hash == hashlib.md5(str(self)).digest(): 577 self._close(response) 578 return 579 580 if not response.session_id or self._forget: 581 self._close(response) 582 return 583 584 if response.session_new: 585 # Tests if the session sub-folder exists, if not, create it 586 session_folder = os.path.dirname(response.session_filename) 587 if not os.path.exists(session_folder): 588 os.mkdir(session_folder) 589 response.session_file = open(response.session_filename, 'wb') 590 portalocker.lock(response.session_file, portalocker.LOCK_EX) 591 response.session_locked = True 592 593 if response.session_file: 594 cPickle.dump(dict(self), response.session_file) 595 response.session_file.truncate() 596 self._close(response)
597
598 - def _unlock(self, response):
599 if response and response.session_file and response.session_locked: 600 try: 601 portalocker.unlock(response.session_file) 602 response.session_locked = False 603 except: ### this should never happen but happens in Windows 604 pass
605
606 - def _close(self, response):
607 if response and response.session_file: 608 self._unlock(response) 609 try: 610 response.session_file.close() 611 del response.session_file 612 except: 613 pass
614