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
