web2py.gluon.globals

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