#!/usr/bin/env python # -*- coding: utf-8 -*- """ his file is part of a custom distribution of the web2py Web Framework, version 1.99.7, licensed by the copyright holder (Massimo Di Pierro ) to Bosch Rexroth (Germany) for use in embedded devices according to the attached non-exclusive license (web2py-commercial-license-Bosh-2012-07-07.pdf, SHA-1 Hash: 42ed39335a669f6a1ba9a7d169063054b147c49c) The original file distributed with web2py is and remains properly of the copyright holder and licensed under LGPLv3 (http://www.gnu.org/licenses/lgpl.html) Contains the classes for the global used variables: - Request - Response - Session """ from storage import Storage, List from streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE from xmlrpc import handler from contenttype import contenttype from html import xmlescape, TABLE, TR, PRE, URL from http import HTTP, redirect from fileutils import up from serializers import json, custom_json import settings from utils import web2py_uuid from settings import global_settings import hashlib import portalocker import cPickle import cStringIO import datetime import re import Cookie import os import sys import traceback import threading try: from gluon.contrib.minify import minify have_minify = True except ImportError: have_minify = False regex_session_id = re.compile('^([\w\-]+/)?[\w\-\.]+$') __all__ = ['Request', 'Response', 'Session'] current = threading.local() # thread-local storage for request-scope globals css_template = '' js_template = '' css_inline = '' js_inline = '' class Request(Storage): """ defines the request object and the default values of its members - env: environment variables, by gluon.main.wsgibase() - cookies - get_vars - post_vars - vars - folder - application - function - args - extension - now: datetime.datetime.today() - restful() """ def __init__(self): self.wsgi = Storage() # hooks to environ and start_response self.env = Storage() self.cookies = Cookie.SimpleCookie() self.get_vars = Storage() self.post_vars = Storage() self.vars = Storage() self.folder = None self.application = None self.function = None self.args = List() self.extension = 'html' self.now = datetime.datetime.now() self.utcnow = datetime.datetime.utcnow() self.is_restful = False self.is_https = False self.is_local = False self.global_settings = settings.global_settings def compute_uuid(self): self.uuid = '%s/%s.%s.%s' % ( self.application, self.client.replace(':', '_'), self.now.strftime('%Y-%m-%d.%H-%M-%S'), web2py_uuid()) return self.uuid def user_agent(self): from gluon.contrib import user_agent_parser session = current.session user_agent = session._user_agent = session._user_agent or \ user_agent_parser.detect(self.env.http_user_agent) user_agent = Storage(user_agent) for key,value in user_agent.items(): if isinstance(value,dict): user_agent[key] = Storage(value) return user_agent def requires_https(self): """ If request comes in over HTTP, redirect it to HTTPS and secure the session. """ if not global_settings.cronjob and not self.is_https: redirect(URL(scheme='https', args=self.args, vars=self.vars)) current.session.secure() def restful(self): def wrapper(action,self=self): def f(_action=action,_self=self,*a,**b): self.is_restful = True method = _self.env.request_method if len(_self.args) and '.' in _self.args[-1]: _self.args[-1],_self.extension = _self.args[-1].rsplit('.',1) current.response.headers['Content-Type'] = \ contenttype(_self.extension.lower()) if not method in ['GET','POST','DELETE','PUT']: raise HTTP(400,"invalid method") rest_action = _action().get(method,None) if not rest_action: raise HTTP(400,"method not supported") try: return rest_action(*_self.args,**_self.vars) except TypeError, e: exc_type, exc_value, exc_traceback = sys.exc_info() if len(traceback.extract_tb(exc_traceback))==1: raise HTTP(400,"invalid arguments") else: raise e f.__doc__ = action.__doc__ f.__name__ = action.__name__ return f return wrapper class Response(Storage): """ defines the response object and the default values of its members response.write( ) can be used to write in the output html """ def __init__(self): self.status = 200 self.headers = Storage() self.headers['X-Powered-By'] = 'web2py' self.body = cStringIO.StringIO() self.session_id = None self.cookies = Cookie.SimpleCookie() self.postprocessing = [] self.flash = '' # used by the default view layout self.meta = Storage() # used by web2py_ajax.html self.menu = [] # used by the default view layout self.files = [] # used by web2py_ajax.html self.generic_patterns = [] # patterns to allow generic views self.delimiters = ('{{','}}') self._vars = None self._caller = lambda f: f() self._view_environment = None self._custom_commit = None self._custom_rollback = None def write(self, data, escape=True): if not escape: self.body.write(str(data)) else: self.body.write(xmlescape(data)) def render(self, *a, **b): from compileapp import run_view_in if len(a) > 2: raise SyntaxError, 'Response.render can be called with two arguments, at most' elif len(a) == 2: (view, self._vars) = (a[0], a[1]) elif len(a) == 1 and isinstance(a[0], str): (view, self._vars) = (a[0], {}) elif len(a) == 1 and hasattr(a[0], 'read') and callable(a[0].read): (view, self._vars) = (a[0], {}) elif len(a) == 1 and isinstance(a[0], dict): (view, self._vars) = (None, a[0]) else: (view, self._vars) = (None, {}) self._vars.update(b) self._view_environment.update(self._vars) if view: import cStringIO (obody, oview) = (self.body, self.view) (self.body, self.view) = (cStringIO.StringIO(), view) run_view_in(self._view_environment) page = self.body.getvalue() self.body.close() (self.body, self.view) = (obody, oview) else: run_view_in(self._view_environment) page = self.body.getvalue() return page def include_meta(self): s = '' for key,value in (self.meta or {}).items(): s += '' % (key,xmlescape(value)) self.write(s,escape=False) def include_files(self): """ Caching method for writing out files. By default, caches in ram for 5 minutes. To change, response.cache_includes = (cache_method, time_expire). Example: (cache.disk, 60) # caches to disk for 1 minute. """ from gluon import URL files = [] for item in self.files: if not item in files: files.append(item) if have_minify and (self.optimize_css or self.optimize_js): # cache for 5 minutes by default cache = self.cache_includes or (current.cache.ram, 60*5) def call_minify(): return minify.minify(files, URL('static','temp'), current.request.folder, self.optimize_css, self.optimize_js) if cache: cache_model, time_expire = cache files = cache_model('response.files.minified', call_minify, time_expire) else: files = call_minify() s = '' for item in files: if isinstance(item,str): f = item.lower() if f.endswith('.css'): s += css_template % item elif f.endswith('.js'): s += js_template % item elif isinstance(item,(list,tuple)): f = item[0] if f=='css:inline': s += css_inline % item[1] elif f=='js:inline': s += js_inline % item[1] self.write(s, escape=False) def stream( self, stream, chunk_size = DEFAULT_CHUNK_SIZE, request=None, ): """ if a controller function:: return response.stream(file, 100) the file content will be streamed at 100 bytes at the time """ if not request: request = current.request if isinstance(stream, (str, unicode)): stream_file_or_304_or_206(stream, chunk_size=chunk_size, request=request, headers=self.headers) # ## the following is for backward compatibility if hasattr(stream, 'name'): filename = stream.name else: filename = None keys = [item.lower() for item in self.headers] if filename and not 'content-type' in keys: self.headers['Content-Type'] = contenttype(filename) if filename and not 'content-length' in keys: try: self.headers['Content-Length'] = \ os.path.getsize(filename) except OSError: pass # Internet Explorer < 9.0 will not allow downloads over SSL unless caching is enabled if request.is_https and isinstance(request.env.http_user_agent,str) and \ not re.search(r'Opera', request.env.http_user_agent) and \ re.search(r'MSIE [5-8][^0-9]', request.env.http_user_agent): self.headers['Pragma'] = 'cache' self.headers['Cache-Control'] = 'private' if request and request.env.web2py_use_wsgi_file_wrapper: wrapped = request.env.wsgi_file_wrapper(stream, chunk_size) else: wrapped = streamer(stream, chunk_size=chunk_size) return wrapped def download(self, request, db, chunk_size = DEFAULT_CHUNK_SIZE, attachment=True): """ example of usage in controller:: def download(): return response.download(request, db) downloads from http://..../download/filename """ import contenttype as c if not request.args: raise HTTP(404) name = request.args[-1] items = re.compile('(?P.*?)\.(?P.*?)\..*')\ .match(name) if not items: raise HTTP(404) (t, f) = (items.group('table'), items.group('field')) field = db[t][f] try: (filename, stream) = field.retrieve(name) except IOError: raise HTTP(404) self.headers['Content-Type'] = c.contenttype(name) if attachment: self.headers['Content-Disposition'] = \ "attachment; filename=%s" % filename return self.stream(stream, chunk_size = chunk_size, request=request) def json(self, data, default=None): return json(data, default = default or custom_json) def xmlrpc(self, request, methods): """ assuming:: def add(a, b): return a+b if a controller function \"func\":: return response.xmlrpc(request, [add]) the controller will be able to handle xmlrpc requests for the add function. Example:: import xmlrpclib connection = xmlrpclib.ServerProxy('http://hostname/app/contr/func') print connection.add(3, 4) """ return handler(request, self, methods) def toolbar(self): from html import DIV, SCRIPT, BEAUTIFY, TAG, URL BUTTON = TAG.button admin = URL("admin","default","design", args=current.request.application) from gluon.dal import thread if hasattr(thread,'instances'): dbstats = [TABLE(*[TR(PRE(row[0]),'%.2fms' % (row[1]*1000)) \ for row in i.db._timings]) \ for i in thread.instances] else: dbstats = [] # if no db or on GAE u = web2py_uuid() return DIV( BUTTON('design',_onclick="document.location='%s'" % admin), BUTTON('request',_onclick="jQuery('#request-%s').slideToggle()"%u), DIV(BEAUTIFY(current.request),_class="hidden",_id="request-%s"%u), BUTTON('session',_onclick="jQuery('#session-%s').slideToggle()"%u), DIV(BEAUTIFY(current.session),_class="hidden",_id="session-%s"%u), BUTTON('response',_onclick="jQuery('#response-%s').slideToggle()"%u), DIV(BEAUTIFY(current.response),_class="hidden",_id="response-%s"%u), BUTTON('db stats',_onclick="jQuery('#db-stats-%s').slideToggle()"%u), DIV(BEAUTIFY(dbstats),_class="hidden",_id="db-stats-%s"%u), SCRIPT("jQuery('.hidden').hide()") ) class Session(Storage): """ defines the session object and the default values of its members (None) """ def connect( self, request, response, db=None, tablename='web2py_session', masterapp=None, migrate=True, separate = None, check_client=False, ): """ separate can be separate=lambda(session_name): session_name[-2:] and it is used to determine a session prefix. separate can be True and it is set to session_name[-2:] """ if separate == True: separate = lambda session_name: session_name[-2:] self._unlock(response) if not masterapp: masterapp = request.application response.session_id_name = 'session_id_%s' % masterapp.lower() if not db: if global_settings.db_sessions is True or masterapp in global_settings.db_sessions: return response.session_new = False client = request.client and request.client.replace(':', '.') if response.session_id_name in request.cookies: response.session_id = \ request.cookies[response.session_id_name].value if regex_session_id.match(response.session_id): response.session_filename = \ os.path.join(up(request.folder), masterapp, 'sessions', response.session_id) else: response.session_id = None if response.session_id: try: response.session_file = \ open(response.session_filename, 'rb+') try: portalocker.lock(response.session_file,portalocker.LOCK_EX) response.session_locked = True self.update(cPickle.load(response.session_file)) response.session_file.seek(0) oc = response.session_filename.split('/')[-1].split('-')[0] if check_client and client!=oc: raise Exception, "cookie attack" finally: pass #This causes admin login to break. Must find out why. #self._close(response) except: response.session_id = None if not response.session_id: uuid = web2py_uuid() response.session_id = '%s-%s' % (client, uuid) if separate: prefix = separate(response.session_id) response.session_id = '%s/%s' % (prefix,response.session_id) response.session_filename = \ os.path.join(up(request.folder), masterapp, 'sessions', response.session_id) response.session_new = True else: if global_settings.db_sessions is not True: global_settings.db_sessions.add(masterapp) response.session_db = True if response.session_file: self._close(response) if settings.global_settings.web2py_runtime_gae: # in principle this could work without GAE request.tickets_db = db if masterapp == request.application: table_migrate = migrate else: table_migrate = False tname = tablename + '_' + masterapp table = db.get(tname, None) if table is None: table = db.define_table( tname, db.Field('locked', 'boolean', default=False), db.Field('client_ip', length=64), db.Field('created_datetime', 'datetime', default=request.now), db.Field('modified_datetime', 'datetime'), db.Field('unique_key', length=64), db.Field('session_data', 'blob'), migrate=table_migrate, ) try: key = request.cookies[response.session_id_name].value (record_id, unique_key) = key.split(':') if record_id == '0': raise Exception, 'record_id == 0' rows = db(table.id == record_id).select() if len(rows) == 0 or rows[0].unique_key != unique_key: raise Exception, 'No record' # rows[0].update_record(locked=True) session_data = cPickle.loads(rows[0].session_data) self.update(session_data) except Exception: record_id = None unique_key = web2py_uuid() session_data = {} response._dbtable_and_field = \ (response.session_id_name, table, record_id, unique_key) response.session_id = '%s:%s' % (record_id, unique_key) response.cookies[response.session_id_name] = response.session_id response.cookies[response.session_id_name]['path'] = '/' self.__hash = hashlib.md5(str(self)).digest() if self.flash: (response.flash, self.flash) = (self.flash, None) def is_new(self): if self._start_timestamp: return False else: self._start_timestamp = datetime.datetime.today() return True def is_expired(self, seconds = 3600): now = datetime.datetime.today() if not self._last_timestamp or \ self._last_timestamp + datetime.timedelta(seconds = seconds) > now: self._last_timestamp = now return False else: return True def secure(self): self._secure = True def forget(self, response=None): self._close(response) self._forget = True def _try_store_in_db(self, request, response): # don't save if file-based sessions, no session id, or session being forgotten if not response.session_db or not response.session_id or self._forget: return # don't save if no change to session __hash = self.__hash if __hash is not None: del self.__hash if __hash == hashlib.md5(str(self)).digest(): return (record_id_name, table, record_id, unique_key) = \ response._dbtable_and_field dd = dict(locked=False, client_ip=request.client.replace(':','.'), modified_datetime=request.now, session_data=cPickle.dumps(dict(self)), unique_key=unique_key) if record_id: table._db(table.id == record_id).update(**dd) else: record_id = table.insert(**dd) response.cookies[response.session_id_name] = '%s:%s'\ % (record_id, unique_key) response.cookies[response.session_id_name]['path'] = '/' def _try_store_on_disk(self, request, response): # don't save if sessions not not file-based if response.session_db: return # don't save if no change to session __hash = self.__hash if __hash is not None: del self.__hash if __hash == hashlib.md5(str(self)).digest(): self._close(response) return if not response.session_id or self._forget: self._close(response) return if response.session_new: # Tests if the session sub-folder exists, if not, create it session_folder = os.path.dirname(response.session_filename) if not os.path.exists(session_folder): os.mkdir(session_folder) response.session_file = open(response.session_filename, 'wb') portalocker.lock(response.session_file, portalocker.LOCK_EX) response.session_locked = True if response.session_file: cPickle.dump(dict(self), response.session_file) response.session_file.truncate() self._close(response) def _unlock(self, response): if response and response.session_file and response.session_locked: try: portalocker.unlock(response.session_file) response.session_locked = False except: ### this should never happen but happens in Windows pass def _close(self, response): if response and response.session_file: self._unlock(response) try: response.session_file.close() del response.session_file except: pass