#!/usr/bin/env python # -*- coding: utf-8 -*- """ This file is part of the web2py Web Framework Copyrighted by Massimo Di Pierro License: 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, secure_dumps, secure_loads from settings import global_settings import hashlib import portalocker import cPickle from pickle import Pickler, MARK, DICT, EMPTY_DICT from types import DictionaryType import cStringIO import datetime import re import Cookie import os import sys import traceback import threading import cgi import copy import tempfile from cache import CacheInRam from fileutils import copystream FMT = '%a, %d-%b-%Y %H:%M:%S PST' PAST = 'Sat, 1-Jan-1971 00:00:00' FUTURE = 'Tue, 1-Dec-2999 23:59:59' try: from gluon.contrib.minify import minify have_minify = True except ImportError: have_minify = False try: import simplejson as sj #external installed library except: try: import json as sj #standard installed library except: import contrib.simplejson as sj #pure python library 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 = '' coffee_template = '' typescript_template = '' less_template = '' css_inline = '' js_inline = '' # IMPORTANT: # this is required so that pickled dict(s) and class.__dict__ # are sorted and web2py can detect without ambiguity when a session changes class SortingPickler(Pickler): def save_dict(self, obj): self.write(EMPTY_DICT if self.bin else MARK+DICT) self.memoize(obj) self._batch_setitems([(key,obj[key]) for key in sorted(obj)]) SortingPickler.dispatch = copy.copy(Pickler.dispatch) SortingPickler.dispatch[DictionaryType] = SortingPickler.save_dict def sorting_dumps(obj, protocol=None): file = cStringIO.StringIO() SortingPickler(file, protocol).dump(obj) return file.getvalue() # END ##################################################################### def copystream_progress(request, chunk_size=10 ** 5): """ copies request.env.wsgi_input into request.body and stores progress upload status in cache_ram X-Progress-ID:length and X-Progress-ID:uploaded """ env = request.env if not env.get('CONTENT_LENGTH', None): return cStringIO.StringIO() source = env['wsgi.input'] try: size = int(env['CONTENT_LENGTH']) except ValueError: raise HTTP(400, "Invalid Content-Length header") try: # Android requires this dest = tempfile.NamedTemporaryFile() except NotImplementedError: # and GAE this dest = tempfile.TemporaryFile() if not 'X-Progress-ID' in request.get_vars: copystream(source, dest, size, chunk_size) return dest cache_key = 'X-Progress-ID:' + request.get_vars['X-Progress-ID'] cache_ram = CacheInRam(request) # same as cache.ram because meta_storage cache_ram(cache_key + ':length', lambda: size, 0) cache_ram(cache_key + ':uploaded', lambda: 0, 0) while size > 0: if size < chunk_size: data = source.read(size) cache_ram.increment(cache_key + ':uploaded', size) else: data = source.read(chunk_size) cache_ram.increment(cache_key + ':uploaded', chunk_size) length = len(data) if length > size: (data, length) = (data[:size], size) size -= length if length == 0: break dest.write(data) if length < chunk_size: break dest.seek(0) cache_ram(cache_key + ':length', None) cache_ram(cache_key + ':uploaded', None) return dest 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, env): Storage.__init__(self) self.env = Storage(env) self.env.web2py_path = global_settings.applications_parent self.env.update(global_settings) self.cookies = Cookie.SimpleCookie() self._get_vars = None self._post_vars = None self._vars = None self._body = None 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 parse_get_vars(self): query_string = self.env.get('QUERY_STRING','') dget = cgi.parse_qs(query_string, keep_blank_values=1) get_vars = self._get_vars = Storage(dget) for (key, value) in get_vars.iteritems(): if isinstance(value,list) and len(value)==1: get_vars[key] = value[0] def parse_post_vars(self): env = self.env post_vars = self._post_vars = Storage() body = self.body #if content-type is application/json, we must read the body is_json = env.get('content_type', '')[:16] == 'application/json' if is_json: try: json_vars = sj.load(body) except: # incoherent request bodies can still be parsed "ad-hoc" json_vars = {} pass # update vars and get_vars with what was posted as json if isinstance(json_vars, dict): post_vars.update(json_vars) body.seek(0) # parse POST variables on POST, PUT, BOTH only in post_vars if (body and env.request_method in ('POST', 'PUT', 'DELETE', 'BOTH') and not is_json): query_string = env.pop('QUERY_STRING') if 'QUERY_STRING' in env else None dpost = cgi.FieldStorage(fp=body, environ=env, keep_blank_values=1) post_vars.update(dpost) if query_string is not None: env['QUERY_STRING'] = query_string # The same detection used by FieldStorage to detect multipart POSTs body.seek(0) def listify(a): return (not isinstance(a, list) and [a]) or a try: keys = sorted(dpost) except TypeError: keys = [] for key in keys: if key is None: continue # not sure why cgi.FieldStorage returns None key dpk = dpost[key] # if an element is not a file replace it with its value else leave it alone if isinstance(dpk, list): value = [] for _dpk in dpk: if not _dpk.filename: value.append(_dpk.value) else: value.append(_dpk) elif not dpk.filename: value = dpk.value else: value = dpk pvalue = listify(value) if len(pvalue): post_vars[key] = (len(pvalue) > 1 and pvalue) or pvalue[0] @property def body(self): if self._body is None: try: self._body = copystream_progress(self) except IOError: raise HTTP(400, "Bad Request - HTTP body is incomplete") return self._body def parse_all_vars(self): self._vars = copy.copy(self.get_vars) for key,value in self.post_vars.iteritems(): if not key in self._vars: self._vars[key] = value else: if not isinstance(self._vars[key],list): self._vars[key] = [self._vars[key]] self._vars[key] += value if isinstance(value,list) else [value] @property def get_vars(self): "lazily parse the query string into get_vars" if self._get_vars is None: self.parse_get_vars() return self._get_vars @property def post_vars(self): "lazily parse the body into post_vars" if self._post_vars is None: self.parse_post_vars() return self._post_vars @property def vars(self): "lazily parse all get_vars and post_vars to fill vars" if self._vars is None: self.parse_all_vars() return self._vars 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 or \ user_agent_parser.detect(self.env.http_user_agent) if session: session._user_agent = 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. """ cmd_opts = global_settings.cmd_options #checking if this is called within the scheduler or within the shell #in addition to checking if it's not a cronjob if ((cmd_opts and (cmd_opts.shell or cmd_opts.scheduler)) or global_settings.cronjob or self.is_https): current.session.secure() else: current.session.forget() redirect(URL(scheme='https', args=self.args, vars=self.vars)) 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].rpartition('.') 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): Storage.__init__(self) self.status = 200 self.headers = dict() 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 = '\n'.join( '\n' % (k, xmlescape(v)) for k, v in (self.meta or {}).iteritems()) self.write(s, escape=False) def include_files(self, extensions=None): """ 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 = [] has_js = has_css = False for item in self.files: if extensions and not item.split('.')[-1] in extensions: continue if item in files: continue if item.endswith('.js'): has_js = True if item.endswith('.css'): has_css = True files.append(item) if have_minify and ((self.optimize_css and has_css) or (self.optimize_js and has_js)): # cache for 5 minutes by default key = hashlib.md5(repr(files)).hexdigest() cache = self.cache_includes or (current.cache.ram, 60 * 5) def call_minify(files=files): 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/' + key, call_minify, time_expire) else: files = call_minify() s = '' for item in files: if isinstance(item, str): f = item.lower().split('?')[0] if self.static_version: item = item.replace( '/static/', '/static/_%s/' % self.static_version, 1) if f.endswith('.css'): s += css_template % item elif f.endswith('.js'): s += js_template % item elif f.endswith('.coffee'): s += coffee_template % item elif f.endswith('.ts'): # http://www.typescriptlang.org/ s += typescript_template % item elif f.endswith('.less'): s += less_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, attachment=False, filename=None ): """ if a controller function:: return response.stream(file, 100) the file content will be streamed at 100 bytes at the time Optional kwargs: (for custom stream calls) attachment=True # Send as attachment. Usually creates a # pop-up download window on browsers filename=None # The name for the attachment Note: for using the stream name (filename) with attachments the option must be explicitly set as function parameter(will default to the last request argument otherwise) """ headers = self.headers # for attachment settings and backward compatibility keys = [item.lower() for item in headers] if attachment: if filename is None: attname = "" else: attname = filename headers["Content-Disposition"] = \ "attachment;filename=%s" % attname 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=headers, status=self.status) # ## the following is for backward compatibility if hasattr(stream, 'name'): filename = stream.name if filename and not 'content-type' in keys: headers['Content-Type'] = contenttype(filename) if filename and not 'content-length' in keys: try: headers['Content-Length'] = \ os.path.getsize(filename) except OSError: pass env = request.env # Internet Explorer < 9.0 will not allow downloads over SSL unless caching is enabled if request.is_https and isinstance(env.http_user_agent, str) and \ not re.search(r'Opera', env.http_user_agent) and \ re.search(r'MSIE [5-8][^0-9]', env.http_user_agent): headers['Pragma'] = 'cache' headers['Cache-Control'] = 'private' if request and env.web2py_use_wsgi_file_wrapper: wrapped = 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, download_filename=None): """ example of usage in controller:: def download(): return response.download(request, db) downloads from http://..../download/filename """ current.session.forget(current.response) 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')) try: field = db[t][f] except AttributeError: raise HTTP(404) try: (filename, stream) = field.retrieve(name,nameonly=True) except IOError: raise HTTP(404) headers = self.headers headers['Content-Type'] = contenttype(name) if download_filename == None: download_filename = filename if attachment: headers['Content-Disposition'] = \ 'attachment; filename="%s"' % download_filename.replace('"','\"') 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, A BUTTON = TAG.button admin = URL("admin", "default", "design", extension='html', args=current.request.application) from gluon.dal import DAL dbstats = [] dbtables = {} infos = DAL.get_instances() for k,v in infos.iteritems(): dbstats.append(TABLE(*[TR(PRE(row[0]),'%.2fms' % (row[1]*1000)) for row in v['dbstats']])) dbtables[k] = dict(defined=v['dbtables']['defined'] or '[no defined tables]', lazy=v['dbtables']['lazy'] or '[no lazy tables]') u = web2py_uuid() backtotop = A('Back to top', _href="#totop-%s" % u) # Convert lazy request.vars from property to Storage so they # will be displayed in the toolbar. request = copy.copy(current.request) request.update(vars=current.request.vars, get_vars=current.request.get_vars, post_vars=current.request.post_vars) return DIV( BUTTON('design', _onclick="document.location='%s'" % admin), BUTTON('request', _onclick="jQuery('#request-%s').slideToggle()" % u), BUTTON('response', _onclick="jQuery('#response-%s').slideToggle()" % u), BUTTON('session', _onclick="jQuery('#session-%s').slideToggle()" % u), BUTTON('db tables', _onclick="jQuery('#db-tables-%s').slideToggle()" % u), BUTTON('db stats', _onclick="jQuery('#db-stats-%s').slideToggle()" % u), DIV(BEAUTIFY(request), backtotop, _class="hidden", _id="request-%s" % u), DIV(BEAUTIFY(current.session), backtotop, _class="hidden", _id="session-%s" % u), DIV(BEAUTIFY(current.response), backtotop, _class="hidden", _id="response-%s" % u), DIV(BEAUTIFY(dbtables), backtotop, _class="hidden", _id="db-tables-%s" % u), DIV(BEAUTIFY( dbstats), backtotop, _class="hidden", _id="db-stats-%s" % u), SCRIPT("jQuery('.hidden').hide()"), _id="totop-%s" % u ) class Session(Storage): """ defines the session object and the default values of its members (None) response.session_storage_type : 'file', 'db', or 'cookie' response.session_cookie_compression_level : response.session_cookie_expires : cookie expiration response.session_cookie_key : for encrypted sessions in cookies response.session_id : a number or None if no session response.session_id_name : response.session_locked : response.session_masterapp : response.session_new : a new session obj is being created response.session_hash : hash of the pickled loaded session response.session_pickled : picked session if session in cookie: response.session_data_name : name of the cookie for session data if session in db: response.session_db_record_id : response.session_db_table : response.session_db_unique_key : if session in file: response.session_file : response.session_filename : """ def connect( self, request=None, response=None, db=None, tablename='web2py_session', masterapp=None, migrate=True, separate=None, check_client=False, cookie_key=None, cookie_expires=None, compression_level=None ): """ 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:] """ request = request or current.request response = response or current.response masterapp = masterapp or request.application cookies = request.cookies self._unlock(response) response.session_masterapp = masterapp response.session_id_name = 'session_id_%s' % masterapp.lower() response.session_data_name = 'session_data_%s' % masterapp.lower() response.session_cookie_expires = cookie_expires response.session_client = str(request.client).replace(':', '.') response.session_cookie_key = cookie_key response.session_cookie_compression_level = compression_level # check if there is a session_id in cookies try: response.session_id = cookies[response.session_id_name].value except KeyError: response.session_id = None # if we are supposed to use cookie based session data if cookie_key: response.session_storage_type = 'cookie' elif db: response.session_storage_type = 'db' else: response.session_storage_type = 'file' # why do we do this? # because connect may be called twice, by web2py and in models. # the first time there is no db yet so it should do nothing if (global_settings.db_sessions is True or masterapp in global_settings.db_sessions): return if response.session_storage_type == 'cookie': # check if there is session data in cookies if response.session_data_name in cookies: session_cookie_data = cookies[response.session_data_name].value else: session_cookie_data = None if session_cookie_data: data = secure_loads(session_cookie_data, cookie_key, compression_level=compression_level) if data: self.update(data) response.session_id = True # else if we are supposed to use file based sessions elif response.session_storage_type == 'file': response.session_new = False response.session_file = None # check if the session_id points to a valid sesion filename if response.session_id: if not regex_session_id.match(response.session_id): response.session_id = None else: response.session_filename = \ os.path.join(up(request.folder), masterapp, 'sessions', response.session_id) try: response.session_file = \ open(response.session_filename, 'rb+') 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 response.session_client != oc: raise Exception("cookie attack") except: response.session_id = None if not response.session_id: uuid = web2py_uuid() response.session_id = '%s-%s' % (response.session_client, uuid) separate = separate and (lambda session_name: session_name[-2:]) 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 the session goes in db elif response.session_storage_type == 'db': if global_settings.db_sessions is not True: global_settings.db_sessions.add(masterapp) # if had a session on file alreday, close it (yes, can happen) if response.session_file: self._close(response) # if on GAE tickets go also in DB if settings.global_settings.web2py_runtime_gae: request.tickets_db = db table_migrate = (masterapp == request.application) tname = tablename + '_' + masterapp table = db.get(tname, None) Field = db.Field if table is None: db.define_table( tname, Field('locked', 'boolean', default=False), Field('client_ip', length=64), Field('created_datetime', 'datetime', default=request.now), Field('modified_datetime', 'datetime'), Field('unique_key', length=64), Field('session_data', 'blob'), migrate=table_migrate, ) table = db[tname] # to allow for lazy table response.session_db_table = table if response.session_id: # Get session data out of the database try: (record_id, unique_key) = response.session_id.split(':') record_id = long(record_id) except (TypeError,ValueError): record_id = None # Select from database if record_id: row = table(record_id) #,unique_key=unique_key) # Make sure the session data exists in the database if row: # rows[0].update_record(locked=True) # Unpickle the data session_data = cPickle.loads(row.session_data) self.update(session_data) else: record_id = None if record_id: response.session_id = '%s:%s' % (record_id, unique_key) response.session_db_unique_key = unique_key response.session_db_record_id = record_id else: response.session_id = None response.session_new = True if self.flash: (response.flash, self.flash) = (self.flash, None) session_pickled = cPickle.dumps(self) response.session_hash = hashlib.md5(session_pickled).hexdigest() def renew(self, clear_session=False): if clear_session: self.clear() request = current.request response = current.response session = response.session masterapp = response.session_masterapp cookies = request.cookies if response.session_storage_type == 'cookie': return # if the session goes in file if response.session_storage_type == 'file': self._close(response) uuid = web2py_uuid() response.session_id = '%s-%s' % (response.session_client, uuid) separate = (lambda s: s[-2:]) if session and response.session_id[2:3]=="/" else None 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 the session goes in db elif response.session_storage_type == 'db': table = response.session_db_table # verify that session_id exists if response.session_file: self._close(response) if response.session_new: return # Get session data out of the database if response.session_id is None: return (record_id, sep, unique_key) = response.session_id.partition(':') if record_id.isdigit() and long(record_id)>0: new_unique_key = web2py_uuid() row = table(record_id) if row and row.unique_key==unique_key: table._db(table.id==record_id).update(unique_key=new_unique_key) else: record_id = None if record_id: response.session_id = '%s:%s' % (record_id, unique_key) response.session_db_record_id = record_id response.session_db_unique_key = new_unique_key else: response.session_new = True def save_session_id_cookie(self): request = current.request response = current.response session = response.session masterapp = response.session_masterapp cookies = request.cookies rcookies = response.cookies # if not cookie_key, but session_data_name in cookies # expire session_data_name from cookies if response.session_data_name in cookies: rcookies[response.session_data_name] = 'expired' rcookies[response.session_data_name]['path'] = '/' rcookies[response.session_data_name]['expires'] = PAST if response.session_id: rcookies[response.session_id_name] = response.session_id rcookies[response.session_id_name]['path'] = '/' if isinstance(response.session_cookie_expires,datetime.datetime): rcookies[response.session_id_name]['expires'] = \ response.session_cookie_expires.strftime(FMT) elif isinstance(response.session_cookie_expires,str): rcookies[response.session_id_name]['expires'] = \ response.session_cookie_expires def clear(self): Storage.clear(self) 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_cookie(self, request, response): if self._forget or self._unchanged(response): self.save_session_id_cookie() return False name = response.session_data_name compression_level = response.session_cookie_compression_level value = secure_dumps(dict(self), response.session_cookie_key, compression_level=compression_level) expires = response.session_cookie_expires rcookies = response.cookies rcookies.pop(name, None) rcookies[name] = value rcookies[name]['path'] = '/' if expires: rcookies[name]['expires'] = expires.strftime(FMT) return True def _unchanged(self,response): session_pickled = cPickle.dumps(self) response.session_pickled = session_pickled session_hash = hashlib.md5(session_pickled).hexdigest() return response.session_hash == session_hash def _try_store_in_db(self, request, response): # don't save if file-based sessions, # no session id, or session being forgotten # or no changes to session if not response.session_db_table or self._forget or self._unchanged(response): if (not response.session_db_table and global_settings.db_sessions is not True and response.session_masterapp in global_settings.db_sessions): global_settings.db_sessions.remove(response.session_masterapp) self.save_session_id_cookie() return False table = response.session_db_table record_id = response.session_db_record_id if response.session_new: unique_key = web2py_uuid() else: unique_key = response.session_db_unique_key session_pickled = response.session_pickled or cPickle.dumps(self) dd = dict(locked=False, client_ip=response.session_client, modified_datetime=request.now, session_data=session_pickled, unique_key=unique_key) if record_id: if not table._db(table.id==record_id).update(**dd): record_id = None if not record_id: record_id = table.insert(**dd) response.session_id = '%s:%s' % (record_id, unique_key) response.session_db_unique_key = unique_key response.session_db_record_id = record_id self.save_session_id_cookie() return True def _try_store_in_cookie_or_file(self, request, response): if response.session_storage_type == 'file': return self._try_store_in_file(request, response) if response.session_storage_type == 'cookie': return self._try_store_in_cookie(request, response) def _try_store_in_file(self, request, response): try: if not response.session_id or self._forget or self._unchanged(response): self.save_session_id_cookie() return False if response.session_new or not response.session_file: # 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: session_pickled = response.session_pickled or cPickle.dumps(self) response.session_file.write(session_pickled) response.session_file.truncate() finally: self._close(response) self.save_session_id_cookie() return True 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