"""
ajaxWM - the free web based window manager for remote terminals
Copyright (C) 2007 Dennis Felsing, Andreas Waidler

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

$Id: Server.py 76 2007-12-15 11:26:24Z r0q $
"""

#{{{ Imports
import os
import mimetypes
import re
import random
import time
import sys

import Files
import Singleton
import Loop
import ajaxwm
from Session import Session
from Error import ID_Error, Login_Error
import qweb
#}}}
class Server(Singleton.Singleton):#{{{
    """
Server handles the initialization of a new client and responses to
requests. There can only be one instance, if you want multiple instances of
ajaxWM running on one and the same computer, just run it with defferent port as
paremeters.
    """
    
    sessions = {}
    # Loop-instances, one for every session.
    loops = {}
    
    MIME = mimetypes.types_map.copy()
    MIME['.html'] = '%s; charset=%s' % (MIME['.html'], ajaxwm.ENCODING)
    # Descriptors, keys and types of keys.
    KEYS = [('session', 's', int), ('event', 'e', str), #{{{
            ('window', 'W', int), ('key', 'k', unicode), 
            ('width', 'w', int), ('height', 'h', int), 
            ('user', 'u', str), ('password', 'p', str)] 
    #}}}
    # The mimetypes for every return to an event.
    EVENT_MIMETYPES = {'sy': 'text/xml', 'kp': 'text/xml', #{{{
                       'nw': 'text/plain', 'cw': 'text/plain', 
                       'li': 'text/plain', 'lo': 'text/plain'}
    #}}}
    # The maximum of sessions is set to the max of an integer on your system.
    # That's a compromise about space and security.
    SESSION_MAX = sys.maxint
        
    def __init__(self, gzip):#{{{
        self.gzip = gzip

        self.sessionExists = lambda sid: \
                             type(sid) == int and \
                             self.sessions.has_key(sid) and \
                             self.sessions[sid]
        self.windowExists = lambda sid, wid: \
                            type(wid) == int and \
                            wid < len(self.sessions[sid].windows) and \
                            self.sessions[sid].windows[wid]
    
        self.files = Files.Files()._content
#        # Read in all files we need to run.
#        for i in ['themes', 'javascript']:
#            os.path.walk(os.path.abspath(i), self.__addFile, None)
#        self.__addFile(None, os.getcwd(), ('index.html',))
    #}}}
#    def __addFile(self, arg, directory, names):#{{{
#        """
#Adds a File to the self.files-list, so it can be accessed later
#when a client connects.
#        """
#        for name in names:
#            # os.getcwd() gets the current working directory, which is the 
#            # virtual root of the webserver
#            path = os.path.join(directory, name) 
#            if not os.path.isdir(path):
#                # Save all found files and set the current working directory
#                # as / for requests to them.
#                self.files[path[len(os.getcwd()):]] = file(path).read()
#                extension = os.path.splitext(name)[1]
#                # Use default mimetype if available, else octet-stream.
#                self.MIME[extension] = '%s; charset=%s' % \
#                                       (self.MIME.get(extension.lower(), \
#                                       'application/octet-stream'), \
#                                       ajaxwm.ENCODING)
#    #}}}
    def __call__(self, environment, startResponse):#{{{
        """
Gets called by the webserver of the qweb-framework on each request.

Format of the call:
key
    value1
    value2
    ...

actions describe what a event should do someday.
cw and kw for example can do the same while in development phase.


 s # session id
 e # event that happened or should be executed
    li # login
       # response: session id or -1 if login fails
    lo # logout
       # response: none?
    nw # new window
       # parameters: width and height (w, h)
       # response is the new windows id or -1 on failure
    cw # close window
       # response: 1 or 0 (true/false)
    sy # synchronize window. parameters: W [w, h]
       # response: new contents/<idem></idem>
    kp # key press, needs additional parameter (see below)
       # see e=sy

# some commands need a target, by now only a window (desktop may come 
  later)
W # window (target of the event (e.g. keypress))
    [0-9]* # value is a integer from 0 (first window) until buffer 
             overflow :P

# some additional parameters that are used by some commands:
k # key, only needed by e=kp
    !!! see ajaxterm for values
w # width, used (optional) by e=sy
    [0-9]* # chars
h # height, used (optional) by e=sy
    [0-9]* # lines
u # user login name, needed by e=li
p # password needed for login
        """
        
        # Parameters and values from request from client.
        request = qweb.QWebRequest(environment, startResponse, session=None)
        
        # query (POST or GET, both work)
        # GET should be faster than POST, because POST always has to send at
        # at least 2 packets, while for GET 1 is enough (reduces the traffic
        # to the server by 50%) According to w3.org GET-Requests also are
        # encrypted when using SSL, so there should be no security problems
        #
        # can be tested by using wget: (POST)
        # wget --post-data="s=123456&e=kp&k=x" localhost:8022/event
        # or: (GET)
        # wget localhost:8022/event?s=123456\&e=kp\&k=x
        if request.PATH_INFO.endswith('/event'):
            keys = {}
            for key, ident, typ in Server.KEYS:
                try: 
                    if typ == unicode:
                        keys[key] = typ(request.REQUEST[ident], 
                                        ajaxwm.ENCODING)
                    else:
                        keys[key] = typ(request.REQUEST[ident])
                except ValueError:
                    # Seems like the key was not valid. Will happen often
                    # because every key is tried to read in while some keys
                    # have no use on some events.
                    if typ == int:
                        keys[key] = None
                    else:
                        keys[key] = typ('')
            
            try:
                request.response_headers['Content-Type'] = \
                Server.EVENT_MIMETYPES[keys['event']]
            except KeyError: 
                # Don't do anything on an invalid key (for now).
                pass
            
            if self.sessionExists(keys['session']):
                ssn = self.sessions[keys['session']]
                loop = self.loops[keys['session']]
                if self.windowExists(keys['session'], keys['window']):
                    window = ssn.windows[keys['window']]
                else:
                    window = None
            else:
                ssn = None
            
            #{{{ Sync and Keypress
            if keys['event'] in ('sy', 'kp'):
                try:
                    if not ssn:
                        raise ID_Error, ID_Error.SESSION
                    if not window:
                        raise ID_Error, ID_Error.WINDOW
                    
                    if keys['event'] == 'sy':
                        if keys['window'] and keys['height']:
                            window.resize(keys['width'], keys['height'])
                    else: # keys['event'] == 'kp'
                        window.write(keys['key'])
                        # Because the virtual terminal needs some time to
                        # handle the key(s) we have to wait for that time.
                        # Else the client would receive nothing at all and
                        # would have to send another sync just to get thes
                        # results of his keypresses.
                        time.sleep(0.001)
                    ssn.sync(keys['window'])
                    # Reset killing time.
                    loop.newTime()
                    r = window.html()
                except ID_Error, e:
                    r = e.value
                # Python 2.5+
                #finally:
                #   request.write((u''.join((XMLSTRING, r)))\
                #                 .encode(ajaxwm.ENCODING))
                request.write((u''.join((ajaxwm.XMLSTRING, r)))\
                              .encode(ajaxwm.ENCODING))
            #}}}
            #{{{ New window
            elif keys['event'] == 'nw':
                if ssn:
                    # Return the window id, -1 on error.
                    request.write(str(ssn.newWindow(keys['width'], 
                                                    keys['height'])))
                else:
                    request.write('-1')
            #}}}
            #{{{ Close window
            elif keys['event'] == 'cw':
                if ssn and window:
                    ssn.deleteWindow(keys['window'])
                    request.write('1')
                else:
                    request.write('0')
            #}}}
            #{{{ Login
            elif keys['event'] == 'li':
                try:
                    # Check for valid UNIX username and non-empty password.
                    if not (re.match('^[0-9A-Za-z-_. ]+$', keys['user']) \
                    and keys['password']):
                        raise Login_Error, -1

                    sessionId = self.SESSION_MAX
                    while sessionId != -1 \
                    and (sessionId == self.SESSION_MAX \
                    or not self.sessionExists(sessionId)):
                        sessionId = random.randint(0, self.SESSION_MAX - 1)
                        if self.loops.has_key(sessionId):
                            # Wait for the loop to finish (max: one second).
                            self.loops[sessionId].join()
                        self.sessions[sessionId] = \
                        Session(u'localhost', keys['user'], 
                                        keys['password'])
                    login = self.sessions[sessionId].tryLogin() 
                    if login < 0:
                        del self.sessions[sessionId]
                        raise Login_Error, login
                except Login_Error, e:
                    sessionId = e.value
                else:
                    # Also create a new loop, which is assigned to the session.
                    self.loops[sessionId] = Loop.Loop(sessionId)
                    self.loops[sessionId].start()
                request.write(str(sessionId))
            #}}}
            #{{{ Logout
            elif keys['event'] == 'lo':
                if ssn:
                    # Both have to be deleted in order to call the
                    # __del__-method of the Session object.
                    del ssn
                    del self.sessions[keys['session']]
                    # We already killed the session, so tell the loopto abort.
                    loop.abort()
                    # Not deleting the loop, so at the login can check if the 
                    # one second the loop needs already is over.
                    
                    request.write('1')
                else:
                    request.write('0')
            #}}}
        
        else:
            file = ""
            if self.files.has_key(request.PATH_INFO):
                file = request.PATH_INFO

            elif request.PATH_INFO == '/':
                file = '/index.html'

            else:
                # Redirect anything else to the 404-error.
                request.http_404()

            if file:
                request.response_headers['Content-Type'] = \
                self.MIME[os.path.splitext(file)[1]]

                request.write(self.files[file])
            
        if not 'text/plain' in request.response_headers.get('Content-Type'):
            request.response_gzencode = self.gzip
        
        return request
    #}}}
#}}}

#------------------------------------------------------------------------------
# vim: fdm=marker ts=4 sw=4 expandtab 
