#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
This file is part of the web2py Web Framework
Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
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
from http import HTTP
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

regex_session_id = re.compile('^([\w\-]+/)?[\w\-\.]+$')

__all__ = ['Request', 'Response', 'Session']

current = threading.local()  # thread-local storage for request-scope globals

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 = None
        self.now = datetime.datetime.now()
        self.is_restful = False
        self.is_https = False
        self.is_local = False

    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
        session._user_agent = session._user_agent or \
            user_agent_parser.detect(self.env.http_user_agent)
        return session._user_agent

    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)
                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._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 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 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
        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<table>.*?)\.(?P<field>.*?)\..*')\
                           .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
        dbstats = [TABLE(*[TR(PRE(row[0]),'%.2fms' % (row[1]*1000)) \
                               for row in i.db._timings]) \
                       for i in thread.instances]        
        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.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.env.remote_addr,
                  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


