##################################################
# SPYCE - Python-based HTML Scripting
# Copyright (c) 2002 Rimon Barr.
#
# Refer to spyce.py
# CVS: $Id: session.py,v 1.1 2006/04/27 16:15:15 jwnmulder Exp $
##################################################

from spyceModule import spyceModule
import re, time, string, random
import spyceLock
try:
  import cPickle
  pickle = cPickle
except:
  import pickle

__doc__ = '''Session module provides support for session management - the
storage of variables on the server between requests under some short
identifier. 

A user must call setHandler() to determine how the sessions are stored, before
using the other session methods. The get(), set() and delete() methods provide
access to the session information. 

The autoSession() method will turn on the automatic session management
(loading and saving the session). When automatic session management is turned
on the session information, identifier, parameter name and browser method are
stored in the variables called auto, autoID, autoName and autoMethod,
respectively.'''

class session(spyceModule):
  def start(self):
    "Initialise the session module variables."
    self._serverobject = self._api.getServerObject()
    if 'session' not in dir(self._serverobject):
      self._serverobject.session = sessionHandlerRegistry()
    self._handler = None
    self._clearAutoSession()
  def finish(self, theError=None):
    "Save the session, if automatic session management is turned on."
    if self.autoID:
      self.set(self.auto, self.autoExpire, self.autoID)
      if self.autoMethod=='cookie':
        self._api.getModule('cookie').set(self.autoName, self.autoID)
    sessionCleanup(self._serverobject.session)
  def init(self, handler=None, *args, **kwargs):
    if handler: 
      session = apply(self.setHandler, (handler,)+args)
      if kwargs.has_key('auto') and kwargs['auto']:
        auto = kwargs['auto']
        if type(auto) != type(()): 
          auto = (auto,)
        apply(session.autoSession, auto)
  def setHandler(self, file_name, *params):
    "Select a session handler."
    file_name = string.split(file_name, ':')
    if len(file_name)==1: file, name = None, file_name[0]
    else: file, name = file_name[:2]
    if file: handler = self._api.loadModule(name, file, self._api.getFilename())
    else: handler = eval(name)
    self._handler = apply(handler, (self,)+params)
    self._serverobject.session.add(self._handler)
    return self
  def get(self, id):  # method deletes session, if stale
    "Retrieve session information."
    if not self._handler: raise 'call setHandler to initialise'
    return self._handler.get(id)
  def delete(self, id=None):
    "Delete session information."
    if not self._handler: raise 'call setHandler to initialise'
    if not id:
      id = self.autoID
      self._clearAutoSession()
    return self._handler.delete(id)
  def set(self, state, expire, id=None):
    "Set session information."
    if not self._handler: raise 'call setHandler to initialise'
    return self._handler.set(state, expire, id)
  def clear(self):
    "Clear all session information in current handler."
    if not self._handler: raise 'call setHandler to initialise'
    return self._handler.clear()
  def autoSession(self, expire, method='cookie', name='spyceSession'):
    "Turn on automatic session management."
    if not self._handler: raise 'call setHandler to initialise'
    method = string.lower(method)
    if method=='cookie': self.autoID = self._api.getModule('cookie').get(name)
    elif method=='post': self.autoID = self._api.getModule('request').post1(name)
    elif method=='get': self.autoID = self._api.getModule('request').get1(name)
    else: raise 'runtime error: invalid autosession method'
    self.autoMethod = method
    self.autoName = name
    self.autoExpire = expire
    self.auto = None
    if self.autoID:
      self.auto = self.get(self.autoID)
      if not self.auto: self.autoID = None
    if not self.autoID:  # generate a sessionid
      self.autoID = self.set(None, self.autoExpire)
  def _clearAutoSession(self):
    self.auto = None
    self.autoID = None
    self.autoMethod = None
    self.autoName = None
    self.autoExpire = None
    
##################################################
# Cleanup
#

# expire sessions every n requests in expectation
SESSION_EXPIRE_CHECK = 50

class sessionHandlerRegistry:
  "Registry of all used session handlers."
  def __init__(self):
    self.handlers = {}
  def add(self, handler):
    self.handlers[handler.getHandlerID()] = handler
  def list(self):
    return self.handlers.values()

def sessionCleanup(registry):
  """Iterates through all session handlers and sessions to perform session
  cleanup"""
  if random.randrange(SESSION_EXPIRE_CHECK): return
  for handler in registry.list():
    sessions = handler.keys()
    for s in sessions:
      handler.get(s)   # will delete stale sessions

##################################################
# Session handlers
#

class sessionHandler:
  '''All session handlers should subclass this, and implement the methods
  marked: 'not implemented'.'''
  def __init__(self, sessionModule):
    self.childnum = sessionModule._api.getServerID()
  def getHandlerID(self):
    raise 'not implemented'
  def get(self, id):  # method should delete, if session is stale
    raise 'not implemented'
  def delete(self, id):
    raise 'not implemented'
  def clear(self):
    raise 'not implemented'
  def set(self, state, expire, id=None):
    raise 'not implemented'
  def keys(self):
    raise 'not implemented'
  def __getitem__(self, key):
    return self.get(key)
  def __delitem__(self, key):
    return self.delete(key)

##################################################
# File-based session handler
#

class session_dir(sessionHandler):
  def __init__(self, sessionModule, dir):
    sessionHandler.__init__(self, sessionModule)
    if not os.path.exists(dir):
      raise "session directory '%s' does not exist" % dir
    self.dir = dir
    self.prefix = 'spy'
    self.BINARY_MODE = 1
  def getHandlerID(self):
    return 'session_dir', self.childnum, self.dir
  def get(self, id):
    if not id: return None
    filename = os.path.join(self.dir, self.prefix+id)
    f=None
    sessionInfo = None
    try:
      f=open(filename, 'r')
      sessionInfo = pickle.load(f)
      f.close()
    except:
      try:
        if f: f.close()
        os.unlink(filename)
      except: pass
    if sessionInfo:
      if time.time() > sessionInfo['expire']:
        self.delete(id)
        return None
      else: return sessionInfo['state']
    else: return None
  def delete(self, id):
    try:
      filename = os.path.join(self.dir, self.prefix+id)
      os.remove(filename)
    except: pass
  def clear(self):
    for id in self.keys():
      self.delete(id)
  def set(self, state, expire, id=None):
    f=None
    try:
      if id:
        filename = os.path.join(self.dir, self.prefix+id)
        f=open(filename, 'w')
      else:
        filename, f, id = openUniqueFile(self.dir, self.prefix, ('%d_' % self.childnum))
      sessionInfo = {}
      sessionInfo['expire'] = int(time.time())+expire
      sessionInfo['state'] = state
      pickle.dump(sessionInfo, f, self.BINARY_MODE)
      f.close()
    except:
      try:
        if f: f.close()
      except: pass
      raise
    return id
  def keys(self):
    sessions = os.listdir(self.dir)
    sessions = filter(lambda s, p=self.prefix: s[:len(p)]==p, sessions)
    sessions = map(lambda s, self=self: s[len(self.prefix):], sessions)
    return sessions

# requires unique (dir, prefix)
def openUniqueFile(dir, prefix, unique, mode='w', max=1000000):
  filelock = spyceLock.fileLock(os.path.join(dir, prefix))
  filelock.lock(1)
  try:
    id = "%06d"%random.randrange(max)
    filename = os.path.join(dir, prefix+unique+id)
    while os.path.exists(filename):
      id = str(random.randrange(max))
      filename = os.path.join(dir, prefix+unique+id)
    f = None
    f = open(filename, mode)
    return filename, f, unique+id
  finally:
    filelock.unlock()

##################################################
# Hash file session handlers
#

class sessionHandlerDBM(sessionHandler):
  def __init__(self, sessionModule, filename):
    sessionHandler.__init__(self, sessionModule)
    self.filename = filename
    self.dbm = None
    self.BINARY_MODE = 1
    self.dbm_type = None  # redefine in subclass
  def getHandlerID(self):
    return 'session_'+self.dbm_type, self.childnum, self.filename
  def _open(self):
    raise 'need to implement'
  def _close(self):
    if self.dbm:
      self.dbm.close()
      self.dbm = None
  def get(self, id):
    if not id: return None
    self._open()
    try:
      expire, state = None, None
      if self.dbm.has_key(id):
        expire, state = pickle.loads(self.dbm[id])
      if expire!=None and time.time() > expire:
        self.delete(id)
        state = None
      return state
    finally:
      self._close()
  def delete(self, id):
    self._open()
    try:
      if self.dbm.has_key(id):
        del self.dbm[id]
    finally:
      self._close()
  def clear(self):
    if os.path.exists(self.filename):
      os.unlink(self.filename)
  def set(self, state, expire, id=None):
    self._open()
    try:
      if not id:
        id = generateKey(self.dbm, self.childnum)
      value = pickle.dumps( (int(time.time())+expire, state), self.BINARY_MODE)
      self.dbm[id] = value
      return id
    finally:
      self._close()
  def keys(self):
    self._open()
    try:
      return self.dbm.keys()
    finally:
      self._close()

def opendb(dbm_session_handler, module, filename, flags):
  mod = __import__(module)
  if not dbm_session_handler.dbm:
    dbm_session_handler.dbm = mod.open(filename, flags)
  
class session_gdbm(sessionHandlerDBM):
  def __init__(self, sessionModule, filename):
    sessionHandlerDBM.__init__(self, sessionModule, filename)
    self.dbm_type = 'gdbm'
  def _open(self):
    opendb(self, self.dbm_type, self.filename, 'cu')

class session_bsddb(sessionHandlerDBM):
  def __init__(self, sessionModule, filename):
    sessionHandlerDBM.__init__(self, sessionModule, filename)
    self.dbm_type = 'bsddb'
  def _open(self):
    opendb(self, 'dbhash', self.filename, 'c')

def generateKey(hash, prefix, max = 1000000):
  prefix = str(prefix)+'_'
  key = random.randrange(max)
  while hash.has_key(prefix+str(key)):
    key = random.randrange(max)
  key = prefix+str(key)
  hash[key] = pickle.dumps(None, 1)
  return key


##################################################
# User callback session handlers
#

class session_user(sessionHandler):
  '''User-callback session handler'''
  def __init__(self, sessionModule, getf, setf, delf, idsf, info=None):
    self.serverID = sessionModule._api.getServerID()
    self.info = info
    self.getf = getf
    self.setf = setf
    self.delf = delf
    self.idsf = idsf
  def getHandlerID(self):
    return 'session_user', self.serverID, self.info
  def get(self, id):  # method should delete, if session is stale
    return self.getf(self.info, id)
  def set(self, state, expire, id):
    return self.setf(self.info, state, expire, self.serverID, id)
  def delete(self, id):
    return self.delf(self.info, id)
  def keys(self):
    return self.idsf(self.info)
  def clear(self):
    for id in self.keys():
      self.delete(id)

##################################################
# database-based session handlers
#

# rimtodo: database-based session handler

