from Common import *
import string, time, traceback, types, whrandom, sys, MimeWriter, smtplib, StringIO
from time import asctime, localtime
from MiscUtils.Funcs import dateForEmail
from WebUtils.HTMLForException import HTMLForException
from WebUtils.Funcs import htmlForDict, htmlEncode
from HTTPResponse import HTTPResponse
from types import DictType, StringType

class singleton: pass


class ExceptionHandler(Object):
    """
    ExceptionHandler is a utility class for Application that is created
    to handle a particular exception. The object is a one-shot deal.
    After handling an exception, it should be removed.

    At some point, the exception handler sends "writeExceptionReport"
    to the transaction (if present), which in turn sends it to the other
    transactional objects (application, request, response, etc.)
    The handler is the single argument for this message.

    Classes may find it useful to do things like this:

    exceptionReportAttrs = 'foo bar baz'.split()
    def writeExceptionReport(self, handler):
        handler.writeTitle(self.__class__.__name__)
        handler.writeAttrs(self, self.exceptionReportAttrs)

    The handler write methods that may be useful are:
        def write(self, s):
        def writeln(self, s):
        def writeTitle(self, s):
        def writeDict(self, d):
        def writeTable(self, listOfDicts, keys=None):
        def writeAttrs(self, obj, attrNames):

    Derived classes must not assume that the error occured in a
    transaction.  self._tra may be None for exceptions outside
    of transactions.

    See the WebKit.html documentation for other information.


    HOW TO CREATE A CUSTOM EXCEPTION HANDLER

    In the __init__.py of your context:

        from WebKit.ExceptionHandler import ExceptionHandler as _ExceptionHandler

        class ExceptionHandler(_ExceptionHandler):

            hideValuesForFields = _ExceptionHandler.hideValuesForFields + ['foo', 'bar']

            def work(self):
                _ExceptionHandler.work(self)
                # do whatever
                # override other methods if you like

        def contextInitialize(app, ctxPath):
            app._exceptionHandlerClass = ExceptionHandler
    """

    hideValuesForFields = ['creditcard', 'credit card', 'cc', 'password', 'passwd']
        # ^ keep all lower case to support case insensitivity
    if 0: # for testing
        hideValuesForFields.extend('application uri http_accept userid'.split())

    hiddenString = '*** hidden ***'


    ## Init ##

    def __init__(self, application, transaction, excInfo):
        Object.__init__(self)

        # Keep references to the objects
        self._app = application
        self._tra = transaction
        self._exc = excInfo
        if self._tra:
            self._req = self._tra.request()
            self._res = self._tra.response()
        else:
            self._req = self._res = None

        # Make some repairs, if needed. We use the transaction & response to get the error page back out
        # @@ 2000-05-09 ce: Maybe a fresh transaction and response should always be made for that purpose
        ## @@ 2003-01-10 sd: This requires a transaction which we do not have.
        ## Making remaining code safe for no transaction.
        ##
                ##if self._res is None:
        ##  self._res = HTTPResponse()
        ##  self._tra.setResponse(self._res)

        # Cache MaxValueLengthInExceptionReport for speed
        self._maxValueLength = self.setting('MaxValueLengthInExceptionReport')

        # exception occurance time. (overridden by response.endTime())
        self._time = time.time()

        # Get to work
        self.work()


    ## Utilities ##

    def setting(self, name):
        return self._app.setting(name)

    def servletPathname(self):
        try:
            return self._tra.request().serverSidePath()
        except:
            
            return None

    def basicServletName(self):
        name = self.servletPathname()
        if name is None:
            return 'unknown'
        else:
            return os.path.basename(name)


    ## Exception handling ##

    def work(self):
        ''' Invoked by __init__ to do the main work. '''

        if self._res:
            self._res.recordEndTime()
            self._time = self._res.endTime()
            
        self.logExceptionToConsole()

        # write the error page out to the response if available.
        if self._res and (not self._res.isCommitted() or self._res.header('Content-type', None)=='text/html'):
            if not self._res.isCommitted():
                self._res.reset()
            if self.setting('ShowDebugInfoOnErrors')==1:
                publicErrorPage = self.privateErrorPage()
            else:
                publicErrorPage = self.publicErrorPage()
            self._res.write(publicErrorPage)

        privateErrorPage = None
        if self.setting('SaveErrorMessages'):
            privateErrorPage = self.privateErrorPage()
            filename = self.saveErrorPage(privateErrorPage)
        else:
            filename = ''

        self.logExceptionToDisk(errorMsgFilename=filename)

        if self.setting('EmailErrors'):
            if privateErrorPage is None:
                privateErrorPage = self.privateErrorPage()
            self.emailException(privateErrorPage)

    def logExceptionToConsole(self, stderr=None):
        ''' Logs the time, servlet name and traceback to the console (typically stderr). This usually results in the information appearing in console/terminal from which AppServer was launched. '''
        if stderr is None:
            stderr = sys.stderr
        stderr.write('[%s] [error] WebKit: Error while executing script %s\n' % (
            asctime(localtime(self._time)), self.servletPathname()))
        traceback.print_exc(file=stderr)

    def publicErrorPage(self):
        return '''<html>
    <head>
        <title>Error</title>
    </head>
    <body fgcolor=black bgcolor=white>
        %s
        <p> %s
    </body>
</html>
''' % (htTitle('Error'), self.setting('UserErrorMessage'))

    def privateErrorPage(self):
        ''' Returns an HTML page intended for the developer with useful information such as the traceback. '''
        html = ['''
<html>
    <head>
        <title>Error</title>
    </head>
    <body fgcolor=black bgcolor=white>
%s
<p> %s''' % (htTitle('Error'), self.setting('UserErrorMessage'))]

        html.append(self.htmlDebugInfo())

        html.append('</body></html>')
        return string.join(html, '')

    def htmlDebugInfo(self):
        ''' Return HTML-formatted debugging information about the current exception. '''
        self.html = []
        self.writeHTML()
        html = ''.join(self.html)
        self.html = None
        return html

    def writeHTML(self):
        self.writeTraceback()
        self.writeMiscInfo()
        self.writeTransaction()
        self.writeEnvironment()
        self.writeIds()
        self.writeFancyTraceback()


    ## Write utility methods ##

    def write(self, s):
        self.html.append(str(s))

    def writeln(self, s):
        self.html.append(str(s))
        self.html.append('\n')

    def writeTitle(self, s):
        self.writeln(htTitle(s))

    def writeDict(self, d):
        self.writeln(htmlForDict(d, filterValueCallBack=self.filterDictValue, maxValueLength=self._maxValueLength))

    def writeTable(self, listOfDicts, keys=None):
        """
        Writes a table whose contents are given by listOfDicts. The
        keys of each dictionary are expected to be the same. If the
        keys arg is None, the headings are taken in alphabetical order
        from the first dictionary. If listOfDicts is "false", nothing
        happens.

        The keys and values are already considered to be HTML.

        Caveat: There's no way to influence the formatting or to use
        column titles that are different than the keys.

        Note: Used by writeAttrs().
        """
        if not listOfDicts:
            return

        if keys is None:
            keys = listOfDicts[0].keys()
            keys.sort()

        wr = self.writeln
        wr('<table>\n<tr>')
        for key in keys:
            wr('<td bgcolor=#F0F0F0><b>%s</b></td>' % key)
        wr('</tr>\n')

        for row in listOfDicts:
            wr('<tr>')
            for key in keys:
                wr('<td bgcolor=#F0F0F0>%s</td>' % self.filterTableValue(row[key], key, row, listOfDicts))
            wr('</tr>\n')

        wr('</table>')

    def writeAttrs(self, obj, attrNames):
        """
        Writes the attributes of the object as given by attrNames.
        Tries obj._name first, followed by obj.name(). Is resilient
        regarding exceptions so as not to spoil the exception report.
        """
        rows = []
        for name in attrNames:
            value = getattr(obj, '_'+name, singleton) # go for data attribute
            try:
                if value is singleton:
                    value = getattr(obj, name, singleton) # go for method
                    if value is singleton:
                        value = '(could not find attribute or method)'
                    else:
                        try:
                            if callable(value):
                                value = value()
                        except Exception, e:
                            value = '(exception during method call: %s: %s)' % (e.__class__.__name__, e)
                        value = self.repr(value)
                else:
                    value = self.repr(value)
            except Exception, e:
                value = '(exception during value processing: %s: %s)' % (e.__class__.__name__, e)
            rows.append({'attr': name, 'value': value})
        self.writeTable(rows, ('attr', 'value'))


    ## Write specific parts ##

    def writeTraceback(self):
        self.writeTitle('Traceback')
        self.write('<p> <i>%s</i>' % self.servletPathname())
        self.write(HTMLForException(self._exc))

    def writeMiscInfo(self):
        self.writeTitle('MiscInfo')
        info = {
            'time':          asctime(localtime(self._time)),
            'filename':      self.servletPathname(),
            'os.getcwd()':   os.getcwd(),
            'sys.path':      sys.path
        }
        self.writeDict(info)

    def writeTransaction(self):
        if self._tra:
            self._tra.writeExceptionReport(self)
        else:
            self.writeTitle("No current Transaction")
            

    def writeEnvironment(self):
        self.writeTitle('Environment')
        self.writeDict(os.environ)

    def writeIds(self):
        self.writeTitle('Ids')
        self.writeTable(osIdTable(), ['name', 'value'])

    def writeFancyTraceback(self):
        if self.setting('IncludeFancyTraceback'):
            self.writeTitle('Fancy Traceback')
            try:
                from WebUtils.ExpansiveHTMLForException import ExpansiveHTMLForException
                self.write(ExpansiveHTMLForException(context=self.setting('FancyTracebackContext')))
            except:
                self.write('Unable to generate a fancy traceback! (uncaught exception)')
                try:
                    self.write(HTMLForException(sys.exc_info()))
                except:
                    self.write('<br>Unable to even generate a normal traceback of the exception in fancy traceback!')

    def saveErrorPage(self, html):
        ''' Saves the given HTML error page for later viewing by the developer, and returns the filename used. '''
        filename = self._app.serverSidePath(os.path.join(self.setting('ErrorMessagesDir'), self.errorPageFilename()))
        f = open(filename, 'w')
        f.write(html)
        f.close()
        return filename

    def errorPageFilename(self):
        ''' Construct a filename for an HTML error page, not including the 'ErrorMessagesDir' setting. '''
        return 'Error-%s-%s-%d.html' % (
            self.basicServletName(),
            string.join(map(lambda x: '%02d' % x, localtime(self._time)[:6]), '-'),
            whrandom.whrandom().randint(10000, 99999))
            # @@ 2000-04-21 ce: Using the timestamp & a random number is a poor technique for filename uniqueness, but this works for now

    def logExceptionToDisk(self, errorMsgFilename=''):
        ''' Writes a tuple containing (date-time, filename, pathname, exception-name, exception-data,error report filename) to the errors file (typically 'Errors.csv') in CSV format. Invoked by handleException(). '''
        logline = (
            asctime(localtime(self._time)),
            self.basicServletName(),
            self.servletPathname(),
            str(self._exc[0]),
            str(self._exc[1]),
            errorMsgFilename)
        filename = self._app.serverSidePath(self.setting('ErrorLogFilename'))
        if os.path.exists(filename):
            f = open(filename, 'a')
        else:
            f = open(filename, 'w')
            f.write('time,filename,pathname,exception name,exception data,error report filename\n')
            
        def fixElement(element):
            element = str(element)
            if string.find(element, ',') or string.find(element, '"'):
                element = string.replace(str(element), '"', '""')
                element = '"' + element + '"'
            return element
        logline = map(fixElement, logline)
        
        f.write(string.join(logline, ','))
        f.write('\n')
        f.close()

    def emailException(self, htmlErrMsg):
        message = StringIO.StringIO()
        writer = MimeWriter.MimeWriter(message)

        ## Construct the message headers
        headers = self.setting('ErrorEmailHeaders').copy()
        headers['Date'] = dateForEmail()
        headers['Subject'] = headers.get('Subject','[WebKit Error]') + ' ' \
                     + str(sys.exc_info()[0]) + ': ' \
                     + str(sys.exc_info()[1])
        for h,v in headers.items():
            if isinstance(v, types.ListType):
                v = ','.join(v)
            writer.addheader(h, v)

        ## Construct the message body

        if self.setting('EmailErrorReportAsAttachment'):
            writer.startmultipartbody('mixed')
            # start off with a text/plain part
            part = writer.nextpart()
            body = part.startbody('text/plain')
            body.write(
                'WebKit caught an exception while processing ' +
                'a request for "%s" ' % self.servletPathname() +
                'at %s (timestamp: %s).  ' %
                (asctime(localtime(self._time)), self._time) +
                'The plain text traceback from Python is printed below and ' +
                'the full HTML error report from WebKit is attached.\n\n'
                )
            traceback.print_exc(file=body)
            
            # now add htmlErrMsg
            part = writer.nextpart()
            part.addheader('Content-Transfer-Encoding', '7bit')
            part.addheader('Content-Description', 'HTML version of WebKit error message')
            body = part.startbody('text/html; name=WebKitErrorMsg.html')
            body.write(htmlErrMsg)
            
            # finish off
            writer.lastpart()
        else:
            body = writer.startbody('text/html')
            body.write(htmlErrMsg)
            
        # Send the message
        server = smtplib.SMTP(self.setting('ErrorEmailServer'))
        server.set_debuglevel(0)
        server.sendmail(headers['From'], headers['To'], message.getvalue())
        server.quit()


    ## Filtering Values ##

    def filterDictValue(self, value, key, dict):
        return self.filterValue(value, key)

    def filterTableValue(self, value, key, row, table):
        """
        Invoked by writeTable() to afford the opportunity to filter
        the values written in tables. These values are already HTML
        when they arrive here. Use the extra key, row and table
        args as necessary.
        """
        if row.has_key('attr') and key!='attr':
            return self.filterValue(value, row['attr'])
        else:
            return self.filterValue(value, key)

    def filterValue(self, value, key):
        """
        This is the core filter method that is used in all filtering.
        By default, it simply returns self.hiddenString if the key is
        in self.hideValuesForField (case insensitive). Subclasses
        could override for more elaborate filtering techniques.
        """
        if key.lower() in self.hideValuesForFields:
            return self.hiddenString
        else:
            return value


    ## Self utility ##

    def repr(self, x):
        """
        Returns the repr() of x already html encoded. As a special case, dictionaries are nicely formatted in table.

        This is a utility method for writeAttrs.
        """
        if type(x) is DictType:
            return htmlForDict(x, filterValueCallBack=self.filterDictValue, maxValueLength=self._maxValueLength)
        else:
            rep = repr(x)
            if self._maxValueLength and len(rep) > self._maxValueLength:
                rep = rep[:self._maxValueLength] + '...'
            return htmlEncode(rep)


# Some misc functions
def htTitle(name):
    return '''
<p> <br> <table border=0 cellpadding=4 cellspacing=0 bgcolor=#A00000 width=100%%> <tr> <td align=center>
    <font face="Tahoma, Arial, Helvetica" color=white> <b> %s </b> </font>
</td> </tr> </table>''' % name


def osIdTable():
    ''' Returns a list of dictionaries contained id information such as uid, gid, etc.,
        all obtained from the os module. Dictionary keys are 'name' and 'value'. '''
    funcs = ['getegid', 'geteuid', 'getgid', 'getpgrp', 'getpid', 'getppid', 'getuid']
    table = []
    for funcName in funcs:
        if hasattr(os, funcName):
            value = getattr(os, funcName)()
            table.append({'name': funcName, 'value': value})
    return table