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