#
# Copyright 2001 by Object Craft P/L, Melbourne, Australia.
#
# LICENCE - see LICENCE file distributed with this software for details.
#

# Note: session_dir should not be a publically readable directory, or 
# local users will be able to steal sessions.

import os
import stat
import sys
import binascii
import random
import errno
import struct
import os.path

try:
    import Cookie
except ImportError:
    pass

from albatross.app import AppContext, Application, PageObjectMixin,\
     PageModuleMixin
from albatross.context import SessionBase, NameRecorderMixin,\
     PickleSignMixin, CachingTemplateLoaderMixin
from albatross.randompage import RandomPageModuleMixin
from albatross.common import *


# File based session recording
class SessionFileContextMixin(SessionBase):

    def __init__(self):
        SessionBase.__init__(self)
        self.__sesid = None

    def _get_sesid_from_cookie(self):
        hdr = self.request.get_header('Cookie')
        if hdr:
            c = Cookie.SimpleCookie(hdr)
            try:
                return os.path.basename(c[self.app.ses_appid()].value)
            except (OSError, KeyError):
                pass
        return None

    def sesid(self):
        return self.__sesid

    def load_session(self):
        sesid = self._get_sesid_from_cookie()
        text = None
        if sesid:
            text = self.app.get_session(sesid)
        if not text:
            sesid = self.app.new_session()
        self.__sesid = sesid
        if text:
            try:
                self.decode_session(text)
            except:
                self.app.del_session(sesid)
                raise
        c = Cookie.SimpleCookie()
        c[self.app.ses_appid()] = self.__sesid
        path = self.absolute_base_url()
        c[self.app.ses_appid()]['path'] = path
        prefix_len = len('Set-Cookie: ')
        self.set_header('Set-Cookie', str(c)[prefix_len:])

    def save_session(self):
        if self.should_save_session():
            text = self.encode_session()
            self.app.put_session(self.__sesid, text)

    def remove_session(self):
        SessionBase.remove_session(self)
        if self.__sesid is not None:
            self.app.del_session(self.__sesid)


class SessionFileAppMixin:

    def __init__(self, appid, session_dir):
        self.__appid = appid
        self.__session_dir = session_dir

    def ses_appid(self):
        return self.__appid

    def get_session(self, sesid):
        text = None
        try:
            text = open(self._ses_filename(sesid), "rb").read()
        except IOError, (eno, estr):
            if eno != errno.ENOENT:
                self.del_session(sesid)
                raise ServerError("read session file: %s: %s" %
                                  (self._ses_filename(sesid), estr))
        return text

    def new_session(self):
        while 1:
            try:
                text = open('/dev/urandom').read(8)
            except (OSError, IOError):
                text = struct.pack('d', random.random())
            sesid = binascii.hexlify(text)

            try:
                fd = os.open(self._ses_filename(sesid),
                             os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0700)
            except OSError, (eno, estr):
                if eno == errno.ENOENT:
                    try:
                        os.makedirs(self.__session_dir, 0700)
                    except OSError, (eno, estr):
                        raise ServerError("create session dir: %s: %s" %
                                          (self.__session_dir, estr))
                    continue
                if eno != errno.EEXIST:
                    raise ServerError("new_session: %s: %s" %
                                      (self._ses_filename(sesid), estr))
                continue

            os.close(fd)
            return sesid

    def put_session(self, sesid, text):
        try:
            file = open(self._ses_filename(sesid), "wb")
            file.write(text)
            file.close()
        except IOError, (eno, estr):
            raise ServerError("write session file: %s: %s" %
                              (self._ses_filename(sesid), estr))

    def del_session(self, sesid):
        try:
            os.remove(self._ses_filename(sesid))
        except OSError:
            pass

    def _ses_filename(self, sesid):
        if os.path.dirname(sesid) != '':
            raise SecurityError("bad session id: %s" % (sesid))
        return os.path.join(self.__session_dir, sesid + '.als')


class SessionFileAppContext(AppContext,
                            NameRecorderMixin,
                            SessionFileContextMixin):

    def __init__(self, app):
        AppContext.__init__(self, app)
        NameRecorderMixin.__init__(self)
        SessionFileContextMixin.__init__(self)


class SimpleSessionFileApp(PickleSignMixin,
                            Application,
                            CachingTemplateLoaderMixin,
                            PageObjectMixin,
                            SessionFileAppMixin):

    def __init__(self, base_url, template_path, start_page, secret,
                 session_appid, session_dir):
        Application.__init__(self, base_url)
        PickleSignMixin.__init__(self, secret)
        CachingTemplateLoaderMixin.__init__(self, template_path)
        PageObjectMixin.__init__(self, start_page)
        SessionFileAppMixin.__init__(self, session_appid, session_dir)

    def create_context(self):
        return SessionFileAppContext(self)


class ModularSessionFileApp(PickleSignMixin,
                            Application,
                            CachingTemplateLoaderMixin,
                            PageModuleMixin,
                            SessionFileAppMixin):

    def __init__(self, base_url, module_path, template_path, start_page, secret,
                 session_appid, session_dir):
        Application.__init__(self, base_url)
        PickleSignMixin.__init__(self, secret)
        CachingTemplateLoaderMixin.__init__(self, template_path)
        PageModuleMixin.__init__(self, module_path, start_page)
        SessionFileAppMixin.__init__(self, session_appid, session_dir)

    def create_context(self):
        return SessionFileAppContext(self)


class RandomModularSessionFileApp(PickleSignMixin,
                                  Application,
                                  CachingTemplateLoaderMixin,
                                  RandomPageModuleMixin,
                                  SessionFileAppMixin):

    def __init__(self, base_url, page_path, start_page, secret,
                 session_appid, session_dir):
        Application.__init__(self, base_url)
        PickleSignMixin.__init__(self, secret)
        CachingTemplateLoaderMixin.__init__(self, page_path)
        RandomPageModuleMixin.__init__(self, page_path, start_page)
        SessionFileAppMixin.__init__(self, session_appid, session_dir)

    def create_context(self):
        return SessionFileAppContext(self)

