1 """TurboGears root controller base class catching errors, displaying custom error pages and sending email notifications."""
2
3
4
5 import datetime
6 import logging
7 import socket
8 import StringIO
9 import traceback
10
11 import cherrypy
12 from turbogears import config, controllers, identity, util
13
14 try:
15 import turbomail
16 has_turbomail = True
17 except ImportError:
18 has_turbomail = False
19 from email.mime.multipart import MIMEMultipart
20 from email.mime.text import MIMEText
21 from email.Utils import formatdate
22
23 __all__ = ['ErrorCatcher']
24
25 log = logging.getLogger("turbogears.controllers")
26
27
37
44
56
58 """Base class for root controllers catching errors and showing error page.
59
60 To use enable custom error pages, the root controller must subclass the
61 ``ErrorCatcher`` class and the CherryPy filter hook must be enabled by
62 setting ``error_catcher.on = True`` in the deployment configuration.
63
64 When the error catcher is enabled and an HTML error (including an
65 unhandled exception) occurs in the controller, an error page is displayed
66 using a template, whose name is looked up in the ``_error_page_templates``
67 class attribute by the HTML status code.
68
69 Currently, there are default templates for the status codes 401, 403 and
70 404, called ``401_error``, ``403_error`` and ``404_error`` resp. and
71 ``unhandled_exception`` for all other errors. The templates are searched
72 in the ``templates`` sub-package of the application.
73
74 Also, if ``mail.on`` is ``True`` sends an email to the admin,
75 when an error occurs. No email is sent if the HTML status code is
76 contained in the list set by the option ``error_catcher.no_email_on``.
77 The default is not to send emails for 401, 403 and 404 errors.
78
79 For email sending to work, at least the configuration options
80 'error_catcher.sender_email' and 'error_catcher.admin_email' must be
81 set to valid email addresses.
82
83 See the docstring for the method 'send_exception_email' for more email
84 related configuration information.
85
86 """
87 _error_codes = {
88 None: u'Unknown Error',
89 400: u'400 - Bad Request',
90 401: u'401 - Unauthorized',
91 403: u'403 - Forbidden',
92 404: u'404 - Not Found',
93 500: u'500 - Internal Server Error',
94 501: u'501 - Not Implemented',
95 502: u'502 - Bad Gateway',
96 }
97 _error_page_templates = {
98 None: '.templates.unhandled_exception',
99 401: '.templates.401_error',
100 403: '.templates.403_error',
101 404: '.templates.404_error',
102 }
103 _error_mail_templates = {
104 None: 'cheetah:.templates.email_unhandled_exception',
105 }
106 admin_group_name = 'admin'
107
109 super(ErrorCatcher, self).__init__(*args, **kw)
110 self.sender_email = config.get('error_catcher.sender_email')
111 self.admin_email = config.get('error_catcher.admin_email')
112 self.smtp_server = config.get('mail.server', 'localhost')
113 self.smtp_username = config.get('mail.username')
114 self.smtp_password = config.get('mail.password')
115 self.no_email_on = config.get('error_catcher.no_email_on',
116 (401, 403, 404))
117
119 """Handle HTTP errors by sending an error page and email."""
120 try:
121 cherrypy._cputil._cp_on_http_error(status, message)
122 error_msg = self._get_error_message(status, message)
123 url = cherrypy.request.requestLine
124 log.exception("CherryPy %s error (%s) for request '%s'", status,
125 error_msg, url)
126
127
128 buf = StringIO.StringIO()
129 traceback.print_exc(file=buf)
130 exc_traceback = buf.getvalue()
131 buf.close()
132
133
134 req = cherrypy.request
135 data = dict(
136 error_msg = error_msg,
137 header_info = format_request_headers(req),
138 is_admin = identity.in_group(self.admin_group_name),
139 message = message,
140 request_info = format_request_info(req),
141 server = req.headers.get('host', socket.getfqdn()),
142 status = status,
143 url = url,
144 timestamp = datetime.datetime.now(),
145 traceback = exc_traceback,
146 user_info = format_user_info(identity.current.user),
147 )
148
149 if config.get('mail.on') and status not in self.no_email_on:
150 try:
151 self.send_exception_email(**data)
152 data['email_sent'] = True
153 except Exception, exc:
154 log.exception('Error email failed: %s', exc)
155 data['email_sent'] = False
156 else:
157 data['email_sent'] = False
158
159 self.send_error_page(**data)
160
161 except StandardError, exc:
162 log.exception('Error handler failed: %s', exc)
163
164
165 if config.get('error_catcher.on', False):
166 _cp_on_http_error = cp_on_http_error
167
168 - def send_error_page(self, **data):
169 """Render error page using template looked up in self._error_templates.
170 """
171 template = self._error_page_templates.get(data['status'],
172 self._error_page_templates.get(None))
173 body = self._render_error_template(template, data=data)
174 cherrypy.response.headers['Content-Length'] = len(body)
175 cherrypy.response.body = body
176
178 """Send an email with the error info to the admin.
179
180 Uses TurboMail if installed and activated, otherwise tries to send
181 email with the ``smtplib`` module. The SMTP settings can be configured
182 with the following settings:
183
184 ``mail.server`` - Mail server to connect to (default 'localhost').
185 ``mail.username`` - User name for SMTP authentication. If the value
186 is unset or evaluates to False no SMTP login is
187 performed.
188 ``mail.password`` - Password for SMTP authentication. may be an empty
189 string.
190
191 See also the class docstring for information on setting the
192 sender and recipient address.
193
194 """
195 if not self.sender_email or not self.admin_email:
196 msg = ('Configuration error: could not send error notification '
197 'because sender and/or admin email address is not set.')
198 log.exception(msg)
199 raise RuntimeError(msg)
200
201 template = self._error_mail_templates.get(data['status'],
202 self._error_mail_templates.get(None))
203 subject = '%(status)s ERROR on server %(server)s' % data
204 body = self._render_error_template(template, 'plain', 'text/plain',
205 data)
206
207 if has_turbomail:
208 msg = turbomail.Message(
209 self.sender_email, self.admin_email, subject)
210 msg.plain = body
211 turbomail.enqueue(msg)
212 else:
213 msg = MIMEMultipart()
214 msg['From'] = self.sender_email
215 msg['To'] = self.admin_email
216 msg['Date'] = formatdate(localtime=True)
217 msg['Subject'] = subject
218 msg.attach(MIMEText(body))
219 self._send_email_by_smtp(self.sender_email, self.admin_email,
220 msg.as_string())
221
223 """Send email via SMTP."""
224 import smtplib
225 smtp = smtplib.SMTP(self.smtp_server)
226 if self.smtp_username and self.smtp_password is not None:
227 smtp.login(self.smtp_username, self.smtp_passwordd)
228 smtp.sendmail(from_addr, to_addr, message)
229 smtp.close()
230
234
237 if ':' in template:
238 prefix, template = template.split(':', 1)
239 prefix += ':'
240 else:
241 prefix = ''
242 if template.startswith('.'):
243 package = util.get_package_name()
244 else:
245 package = ''
246 template = "%s%s%s" % (prefix, package, template)
247 return controllers._process_output(data, template, format,
248 content_type, None)
249