<commit_before># Copyright 2013-2018 Reahl Software Services (Pty) Ltd. All rights reserved.
#
#    This file is part of Reahl.
#
#    Reahl is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation; version 3 of the License.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Am implementation of Reahl using SqlAlchemy Declarative.

Run 'reahl componentinfo reahl-web-declarative' for configuration information.

"""

import random
import urllib.parse
from datetime import datetime, timedelta



from sqlalchemy import Column, Integer, BigInteger, LargeBinary, PickleType, String, UnicodeText, ForeignKey, DateTime
from sqlalchemy.orm import relationship, deferred, backref
#from sqlalchemy import Column, Integer, ForeignKey, UnicodeText, String, DateTime, Boolean


from reahl.sqlalchemysupport import Session, Base
from reahl.component.eggs import ReahlEgg
from reahl.component.config import Configuration
from reahl.component.context import ExecutionContext
from reahl.component.migration import Migration
from reahl.web.interfaces import UserSessionProtocol, UserInputProtocol, PersistedExceptionProtocol, PersistedFileProtocol
from reahl.web.fw import Url

class InvalidKeyException(Exception):
    pass

class UserSession(Base, UserSessionProtocol):
    """An implementation of :class:`reahl.web.interfaces.UserSessionProtocol` of the Reahl framework."""

    __tablename__ = 'usersession'

    id = Column(Integer, primary_key=True)
    discriminator = Column('row_type', String(40))
    __mapper_args__ = {'polymorphic_on': discriminator}

    idle_lifetime = Column(Integer(), nullable=False, default=0)
    last_activity = Column(DateTime(), nullable=False, default=datetime.now)

    salt = Column(String(40), nullable=False)
    secure_salt = Column(String(40), nullable=False)

    @classmethod
    def remove_dead_sessions(cls, now=None):
        now = now or datetime.now()
        if now.minute > 0 and now.minute < 5:
            config = ExecutionContext.get_context().config
            cutoff = now - timedelta(seconds=config.web.session_lifetime)
            Session.query(cls).filter(cls.last_activity <= cutoff).delete()

    @classmethod
    def for_current_session(cls):
        return ExecutionContext.get_context().session

    @classmethod
    def initialise_web_session_on(cls, context):
        context.session = cls.get_or_create_session()
    
    def __init__(self, **kwargs):
        self.generate_salt()
        self.last_activity = datetime.fromordinal(1)
        self.set_idle_lifetime(False)
        super().__init__(**kwargs)

    def is_secured(self):
        context = ExecutionContext.get_context()
        return self.is_within_timeout(context.config.web.idle_secure_lifetime) \
               and context.request.scheme == 'https' \
               and self.secure_cookie_is_valid()

    def is_active(self):
        return self.is_within_timeout(self.idle_lifetime)

    def is_within_timeout(self, timeout):
        return self.last_activity + timedelta(seconds=timeout) > datetime.now()

    def set_last_activity_time(self):
        self.last_activity = datetime.now()

    def set_idle_lifetime(self, use_max):
        config = ExecutionContext.get_context().config
        self.idle_lifetime = config.web.idle_lifetime_max if use_max else config.web.idle_lifetime

    @classmethod
    def from_key(cls, key):
        try:
            web_session_id, salt = key.split(':')
        except ValueError:
            raise InvalidKeyException()
        sessions = Session.query(cls).filter_by(id=web_session_id)
        if sessions.count() == 1 and sessions.one().salt == salt:
            return sessions.one()
        raise InvalidKeyException()

    def as_key(self):
        Session.flush() # To make sure .id is populated
        return '%s:%s' % (str(self.id), self.salt)

    def secure_cookie_is_valid(self):
        context = ExecutionContext.get_context()
        try:
            salt = context.request.cookies[context.config.web.secure_key_name]
            return self.secure_salt == salt
        except KeyError:
            return False

    @classmethod
    def get_or_create_session(cls):
        web_session = None
        session_key = cls.get_session_key()
        if session_key:
            try:
                web_session = cls.from_key(session_key)
            except InvalidKeyException:
                pass

        if not web_session:
            web_session = cls()
            Session.add(web_session)

        return web_session

    @classmethod
    def get_session_key(cls):
        context = ExecutionContext.get_context()
        try:
            raw_cookie = context.request.cookies[context.config.web.session_key_name]
            return urllib.parse.unquote(raw_cookie)
        except KeyError:
            return None

    def set_session_key(self, response):
        context = ExecutionContext.get_context()
        session_cookie = self.as_key()
        response.set_cookie(context.config.web.session_key_name, urllib.parse.quote(session_cookie), path='/')
        if self.is_secured():
            response.set_cookie(context.config.web.secure_key_name, urllib.parse.quote(self.secure_salt), secure=True, path='/',
                                max_age=context.config.web.idle_secure_lifetime)

    def generate_salt(self):
        alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZqwertyuiopasdfghjklzxcvbnm0123456789'
        self.salt = ''.join([random.choice(alphabet) for x in list(range(40))])
        self.secure_salt = ''.join([random.choice(alphabet) for x in list(range(40))])        

    def get_interface_locale(self):
        context = ExecutionContext.get_context()
        if not hasattr(context, 'request'):
            return 'en_gb'

        url = Url.get_current_url()
        possible_locale, path = url.get_locale_split_path()
        supported_locales = ReahlEgg.get_languages_supported_by_all(context.config.reahlsystem.root_egg)
        if possible_locale:
            if possible_locale in supported_locales:
                return possible_locale<commit_msg>update to reply with samesite requirement for cookies<commit_after># Copyright 2013-2018 Reahl Software Services (Pty) Ltd. All rights reserved.
#
#    This file is part of Reahl.
#
#    Reahl is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation; version 3 of the License.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Am implementation of Reahl using SqlAlchemy Declarative.

Run 'reahl componentinfo reahl-web-declarative' for configuration information.

"""

import random
import urllib.parse
from datetime import datetime, timedelta



from sqlalchemy import Column, Integer, BigInteger, LargeBinary, PickleType, String, UnicodeText, ForeignKey, DateTime
from sqlalchemy.orm import relationship, deferred, backref
#from sqlalchemy import Column, Integer, ForeignKey, UnicodeText, String, DateTime, Boolean


from reahl.sqlalchemysupport import Session, Base
from reahl.component.eggs import ReahlEgg
from reahl.component.config import Configuration
from reahl.component.context import ExecutionContext
from reahl.component.migration import Migration
from reahl.web.interfaces import UserSessionProtocol, UserInputProtocol, PersistedExceptionProtocol, PersistedFileProtocol
from reahl.web.fw import Url

class InvalidKeyException(Exception):
    pass

class UserSession(Base, UserSessionProtocol):
    """An implementation of :class:`reahl.web.interfaces.UserSessionProtocol` of the Reahl framework."""

    __tablename__ = 'usersession'

    id = Column(Integer, primary_key=True)
    discriminator = Column('row_type', String(40))
    __mapper_args__ = {'polymorphic_on': discriminator}

    idle_lifetime = Column(Integer(), nullable=False, default=0)
    last_activity = Column(DateTime(), nullable=False, default=datetime.now)

    salt = Column(String(40), nullable=False)
    secure_salt = Column(String(40), nullable=False)

    @classmethod
    def remove_dead_sessions(cls, now=None):
        now = now or datetime.now()
        if now.minute > 0 and now.minute < 5:
            config = ExecutionContext.get_context().config
            cutoff = now - timedelta(seconds=config.web.session_lifetime)
            Session.query(cls).filter(cls.last_activity <= cutoff).delete()

    @classmethod
    def for_current_session(cls):
        return ExecutionContext.get_context().session

    @classmethod
    def initialise_web_session_on(cls, context):
        context.session = cls.get_or_create_session()
    
    def __init__(self, **kwargs):
        self.generate_salt()
        self.last_activity = datetime.fromordinal(1)
        self.set_idle_lifetime(False)
        super().__init__(**kwargs)

    def is_secured(self):
        context = ExecutionContext.get_context()
        return self.is_within_timeout(context.config.web.idle_secure_lifetime) \
               and context.request.scheme == 'https' \
               and self.secure_cookie_is_valid()

    def is_active(self):
        return self.is_within_timeout(self.idle_lifetime)

    def is_within_timeout(self, timeout):
        return self.last_activity + timedelta(seconds=timeout) > datetime.now()

    def set_last_activity_time(self):
        self.last_activity = datetime.now()

    def set_idle_lifetime(self, use_max):
        config = ExecutionContext.get_context().config
        self.idle_lifetime = config.web.idle_lifetime_max if use_max else config.web.idle_lifetime

    @classmethod
    def from_key(cls, key):
        try:
            web_session_id, salt = key.split(':')
        except ValueError:
            raise InvalidKeyException()
        sessions = Session.query(cls).filter_by(id=web_session_id)
        if sessions.count() == 1 and sessions.one().salt == salt:
            return sessions.one()
        raise InvalidKeyException()

    def as_key(self):
        Session.flush() # To make sure .id is populated
        return '%s:%s' % (str(self.id), self.salt)

    def secure_cookie_is_valid(self):
        context = ExecutionContext.get_context()
        try:
            salt = context.request.cookies[context.config.web.secure_key_name]
            return self.secure_salt == salt
        except KeyError:
            return False

    @classmethod
    def get_or_create_session(cls):
        web_session = None
        session_key = cls.get_session_key()
        if session_key:
            try:
                web_session = cls.from_key(session_key)
            except InvalidKeyException:
                pass

        if not web_session:
            web_session = cls()
            Session.add(web_session)

        return web_session

    @classmethod
    def get_session_key(cls):
        context = ExecutionContext.get_context()
        try:
            raw_cookie = context.request.cookies[context.config.web.session_key_name]
            return urllib.parse.unquote(raw_cookie)
        except KeyError:
            return None

    def set_session_key(self, response):
        context = ExecutionContext.get_context()
        session_cookie = self.as_key()
        response.set_cookie(context.config.web.session_key_name, urllib.parse.quote(session_cookie), path='/', samesite='Strict')
        if self.is_secured():
            response.set_cookie(context.config.web.secure_key_name, urllib.parse.quote(self.secure_salt), secure=True, path='/',
                                max_age=context.config.web.idle_secure_lifetime, samesite='Strict')

    def generate_salt(self):
        alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZqwertyuiopasdfghjklzxcvbnm0123456789'
        self.salt = ''.join([random.choice(alphabet) for x in list(range(40))])
        self.secure_salt = ''.join([random.choice(alphabet) for x in list(range(40))])        

    def get_interface_locale(self):
        context = ExecutionContext.get_context()
        if not hasattr(context, 'request'):
            return 'en_gb'

        url = Url.get_current_url()
        possible_locale, path = url.get_locale_split_path()
        supported_locales = ReahlEgg.get_languages_supported_by_all(context.config.reahlsystem.root_egg)
        if possible_locale:
            if possible_locale in supported_locales:
                return possible_locale
{'fraction_non_alphanumeric': 0.08743169398907104, 'fraction_numerical': 0.0051608986035215545, 'mean_word_length': 3.1489294710327456, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1086000', 'n_tokens_mistral': 3868, 'n_tokens_neox': 3733, 'n_words': 918}