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