Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/django/views/debug.py: 18%
339 statements
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
1import functools
2import itertools
3import re
4import sys
5import types
6import warnings
7from pathlib import Path
9from django.conf import settings
10from django.http import Http404, HttpResponse, HttpResponseNotFound
11from django.template import Context, Engine, TemplateDoesNotExist
12from django.template.defaultfilters import pprint
13from django.urls import resolve
14from django.utils import timezone
15from django.utils.datastructures import MultiValueDict
16from django.utils.encoding import force_str
17from django.utils.module_loading import import_string
18from django.utils.regex_helper import _lazy_re_compile
19from django.utils.version import PY311, get_docs_version
21# Minimal Django templates engine to render the error templates
22# regardless of the project's TEMPLATES setting. Templates are
23# read directly from the filesystem so that the error handler
24# works even if the template loader is broken.
25DEBUG_ENGINE = Engine(
26 debug=True,
27 libraries={"i18n": "django.templatetags.i18n"},
28)
31def builtin_template_path(name):
32 """
33 Return a path to a builtin template.
35 Avoid calling this function at the module level or in a class-definition
36 because __file__ may not exist, e.g. in frozen environments.
37 """
38 return Path(__file__).parent / "templates" / name
41class ExceptionCycleWarning(UserWarning):
42 pass
45class CallableSettingWrapper:
46 """
47 Object to wrap callable appearing in settings.
48 * Not to call in the debug page (#21345).
49 * Not to break the debug page if the callable forbidding to set attributes
50 (#23070).
51 """
53 def __init__(self, callable_setting):
54 self._wrapped = callable_setting
56 def __repr__(self):
57 return repr(self._wrapped)
60def technical_500_response(request, exc_type, exc_value, tb, status_code=500):
61 """
62 Create a technical server error response. The last three arguments are
63 the values returned from sys.exc_info() and friends.
64 """
65 reporter = get_exception_reporter_class(request)(request, exc_type, exc_value, tb)
66 if request.accepts("text/html"):
67 html = reporter.get_traceback_html()
68 return HttpResponse(html, status=status_code)
69 else:
70 text = reporter.get_traceback_text()
71 return HttpResponse(
72 text, status=status_code, content_type="text/plain; charset=utf-8"
73 )
76@functools.lru_cache
77def get_default_exception_reporter_filter():
78 # Instantiate the default filter for the first time and cache it.
79 return import_string(settings.DEFAULT_EXCEPTION_REPORTER_FILTER)()
82def get_exception_reporter_filter(request):
83 default_filter = get_default_exception_reporter_filter()
84 return getattr(request, "exception_reporter_filter", default_filter)
87def get_exception_reporter_class(request):
88 default_exception_reporter_class = import_string(
89 settings.DEFAULT_EXCEPTION_REPORTER
90 )
91 return getattr(
92 request, "exception_reporter_class", default_exception_reporter_class
93 )
96def get_caller(request):
97 resolver_match = request.resolver_match
98 if resolver_match is None:
99 try:
100 resolver_match = resolve(request.path)
101 except Http404:
102 pass
103 return "" if resolver_match is None else resolver_match._func_path
106class SafeExceptionReporterFilter:
107 """
108 Use annotations made by the sensitive_post_parameters and
109 sensitive_variables decorators to filter out sensitive information.
110 """
112 cleansed_substitute = "********************"
113 hidden_settings = _lazy_re_compile(
114 "API|TOKEN|KEY|SECRET|PASS|SIGNATURE|HTTP_COOKIE", flags=re.I
115 )
117 def cleanse_setting(self, key, value):
118 """
119 Cleanse an individual setting key/value of sensitive content. If the
120 value is a dictionary, recursively cleanse the keys in that dictionary.
121 """
122 if key == settings.SESSION_COOKIE_NAME:
123 is_sensitive = True
124 else:
125 try:
126 is_sensitive = self.hidden_settings.search(key)
127 except TypeError:
128 is_sensitive = False
130 if is_sensitive:
131 cleansed = self.cleansed_substitute
132 elif isinstance(value, dict):
133 cleansed = {k: self.cleanse_setting(k, v) for k, v in value.items()}
134 elif isinstance(value, list):
135 cleansed = [self.cleanse_setting("", v) for v in value]
136 elif isinstance(value, tuple):
137 cleansed = tuple([self.cleanse_setting("", v) for v in value])
138 else:
139 cleansed = value
141 if callable(cleansed):
142 cleansed = CallableSettingWrapper(cleansed)
144 return cleansed
146 def get_safe_settings(self):
147 """
148 Return a dictionary of the settings module with values of sensitive
149 settings replaced with stars (*********).
150 """
151 settings_dict = {}
152 for k in dir(settings):
153 if k.isupper():
154 settings_dict[k] = self.cleanse_setting(k, getattr(settings, k))
155 return settings_dict
157 def get_safe_request_meta(self, request):
158 """
159 Return a dictionary of request.META with sensitive values redacted.
160 """
161 if not hasattr(request, "META"):
162 return {}
163 return {k: self.cleanse_setting(k, v) for k, v in request.META.items()}
165 def get_safe_cookies(self, request):
166 """
167 Return a dictionary of request.COOKIES with sensitive values redacted.
168 """
169 if not hasattr(request, "COOKIES"):
170 return {}
171 return {k: self.cleanse_setting(k, v) for k, v in request.COOKIES.items()}
173 def is_active(self, request):
174 """
175 This filter is to add safety in production environments (i.e. DEBUG
176 is False). If DEBUG is True then your site is not safe anyway.
177 This hook is provided as a convenience to easily activate or
178 deactivate the filter on a per request basis.
179 """
180 return settings.DEBUG is False
182 def get_cleansed_multivaluedict(self, request, multivaluedict):
183 """
184 Replace the keys in a MultiValueDict marked as sensitive with stars.
185 This mitigates leaking sensitive POST parameters if something like
186 request.POST['nonexistent_key'] throws an exception (#21098).
187 """
188 sensitive_post_parameters = getattr(request, "sensitive_post_parameters", [])
189 if self.is_active(request) and sensitive_post_parameters:
190 multivaluedict = multivaluedict.copy()
191 for param in sensitive_post_parameters:
192 if param in multivaluedict:
193 multivaluedict[param] = self.cleansed_substitute
194 return multivaluedict
196 def get_post_parameters(self, request):
197 """
198 Replace the values of POST parameters marked as sensitive with
199 stars (*********).
200 """
201 if request is None:
202 return {}
203 else:
204 sensitive_post_parameters = getattr(
205 request, "sensitive_post_parameters", []
206 )
207 if self.is_active(request) and sensitive_post_parameters:
208 cleansed = request.POST.copy()
209 if sensitive_post_parameters == "__ALL__":
210 # Cleanse all parameters.
211 for k in cleansed:
212 cleansed[k] = self.cleansed_substitute
213 return cleansed
214 else:
215 # Cleanse only the specified parameters.
216 for param in sensitive_post_parameters:
217 if param in cleansed:
218 cleansed[param] = self.cleansed_substitute
219 return cleansed
220 else:
221 return request.POST
223 def cleanse_special_types(self, request, value):
224 try:
225 # If value is lazy or a complex object of another kind, this check
226 # might raise an exception. isinstance checks that lazy
227 # MultiValueDicts will have a return value.
228 is_multivalue_dict = isinstance(value, MultiValueDict)
229 except Exception as e:
230 return "{!r} while evaluating {!r}".format(e, value)
232 if is_multivalue_dict:
233 # Cleanse MultiValueDicts (request.POST is the one we usually care about)
234 value = self.get_cleansed_multivaluedict(request, value)
235 return value
237 def get_traceback_frame_variables(self, request, tb_frame):
238 """
239 Replace the values of variables marked as sensitive with
240 stars (*********).
241 """
242 # Loop through the frame's callers to see if the sensitive_variables
243 # decorator was used.
244 current_frame = tb_frame.f_back
245 sensitive_variables = None
246 while current_frame is not None:
247 if (
248 current_frame.f_code.co_name == "sensitive_variables_wrapper"
249 and "sensitive_variables_wrapper" in current_frame.f_locals
250 ):
251 # The sensitive_variables decorator was used, so we take note
252 # of the sensitive variables' names.
253 wrapper = current_frame.f_locals["sensitive_variables_wrapper"]
254 sensitive_variables = getattr(wrapper, "sensitive_variables", None)
255 break
256 current_frame = current_frame.f_back
258 cleansed = {}
259 if self.is_active(request) and sensitive_variables:
260 if sensitive_variables == "__ALL__":
261 # Cleanse all variables
262 for name in tb_frame.f_locals:
263 cleansed[name] = self.cleansed_substitute
264 else:
265 # Cleanse specified variables
266 for name, value in tb_frame.f_locals.items():
267 if name in sensitive_variables:
268 value = self.cleansed_substitute
269 else:
270 value = self.cleanse_special_types(request, value)
271 cleansed[name] = value
272 else:
273 # Potentially cleanse the request and any MultiValueDicts if they
274 # are one of the frame variables.
275 for name, value in tb_frame.f_locals.items():
276 cleansed[name] = self.cleanse_special_types(request, value)
278 if (
279 tb_frame.f_code.co_name == "sensitive_variables_wrapper"
280 and "sensitive_variables_wrapper" in tb_frame.f_locals
281 ):
282 # For good measure, obfuscate the decorated function's arguments in
283 # the sensitive_variables decorator's frame, in case the variables
284 # associated with those arguments were meant to be obfuscated from
285 # the decorated function's frame.
286 cleansed["func_args"] = self.cleansed_substitute
287 cleansed["func_kwargs"] = self.cleansed_substitute
289 return cleansed.items()
292class ExceptionReporter:
293 """Organize and coordinate reporting on exceptions."""
295 @property
296 def html_template_path(self):
297 return builtin_template_path("technical_500.html")
299 @property
300 def text_template_path(self):
301 return builtin_template_path("technical_500.txt")
303 def __init__(self, request, exc_type, exc_value, tb, is_email=False):
304 self.request = request
305 self.filter = get_exception_reporter_filter(self.request)
306 self.exc_type = exc_type
307 self.exc_value = exc_value
308 self.tb = tb
309 self.is_email = is_email
311 self.template_info = getattr(self.exc_value, "template_debug", None)
312 self.template_does_not_exist = False
313 self.postmortem = None
315 def _get_raw_insecure_uri(self):
316 """
317 Return an absolute URI from variables available in this request. Skip
318 allowed hosts protection, so may return insecure URI.
319 """
320 return "{scheme}://{host}{path}".format(
321 scheme=self.request.scheme,
322 host=self.request._get_raw_host(),
323 path=self.request.get_full_path(),
324 )
326 def get_traceback_data(self):
327 """Return a dictionary containing traceback information."""
328 if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist):
329 self.template_does_not_exist = True
330 self.postmortem = self.exc_value.chain or [self.exc_value]
332 frames = self.get_traceback_frames()
333 for i, frame in enumerate(frames):
334 if "vars" in frame:
335 frame_vars = []
336 for k, v in frame["vars"]:
337 v = pprint(v)
338 # Trim large blobs of data
339 if len(v) > 4096:
340 v = "%s… <trimmed %d bytes string>" % (v[0:4096], len(v))
341 frame_vars.append((k, v))
342 frame["vars"] = frame_vars
343 frames[i] = frame
345 unicode_hint = ""
346 if self.exc_type and issubclass(self.exc_type, UnicodeError):
347 start = getattr(self.exc_value, "start", None)
348 end = getattr(self.exc_value, "end", None)
349 if start is not None and end is not None:
350 unicode_str = self.exc_value.args[1]
351 unicode_hint = force_str(
352 unicode_str[max(start - 5, 0) : min(end + 5, len(unicode_str))],
353 "ascii",
354 errors="replace",
355 )
356 from django import get_version
358 if self.request is None:
359 user_str = None
360 else:
361 try:
362 user_str = str(self.request.user)
363 except Exception:
364 # request.user may raise OperationalError if the database is
365 # unavailable, for example.
366 user_str = "[unable to retrieve the current user]"
368 c = {
369 "is_email": self.is_email,
370 "unicode_hint": unicode_hint,
371 "frames": frames,
372 "request": self.request,
373 "request_meta": self.filter.get_safe_request_meta(self.request),
374 "request_COOKIES_items": self.filter.get_safe_cookies(self.request).items(),
375 "user_str": user_str,
376 "filtered_POST_items": list(
377 self.filter.get_post_parameters(self.request).items()
378 ),
379 "settings": self.filter.get_safe_settings(),
380 "sys_executable": sys.executable,
381 "sys_version_info": "%d.%d.%d" % sys.version_info[0:3],
382 "server_time": timezone.now(),
383 "django_version_info": get_version(),
384 "sys_path": sys.path,
385 "template_info": self.template_info,
386 "template_does_not_exist": self.template_does_not_exist,
387 "postmortem": self.postmortem,
388 }
389 if self.request is not None:
390 c["request_GET_items"] = self.request.GET.items()
391 c["request_FILES_items"] = self.request.FILES.items()
392 c["request_insecure_uri"] = self._get_raw_insecure_uri()
393 c["raising_view_name"] = get_caller(self.request)
395 # Check whether exception info is available
396 if self.exc_type:
397 c["exception_type"] = self.exc_type.__name__
398 if self.exc_value:
399 c["exception_value"] = str(self.exc_value)
400 if exc_notes := getattr(self.exc_value, "__notes__", None):
401 c["exception_notes"] = "\n" + "\n".join(exc_notes)
402 if frames:
403 c["lastframe"] = frames[-1]
404 return c
406 def get_traceback_html(self):
407 """Return HTML version of debug 500 HTTP error page."""
408 with self.html_template_path.open(encoding="utf-8") as fh:
409 t = DEBUG_ENGINE.from_string(fh.read())
410 c = Context(self.get_traceback_data(), use_l10n=False)
411 return t.render(c)
413 def get_traceback_text(self):
414 """Return plain text version of debug 500 HTTP error page."""
415 with self.text_template_path.open(encoding="utf-8") as fh:
416 t = DEBUG_ENGINE.from_string(fh.read())
417 c = Context(self.get_traceback_data(), autoescape=False, use_l10n=False)
418 return t.render(c)
420 def _get_source(self, filename, loader, module_name):
421 source = None
422 if hasattr(loader, "get_source"):
423 try:
424 source = loader.get_source(module_name)
425 except ImportError:
426 pass
427 if source is not None:
428 source = source.splitlines()
429 if source is None:
430 try:
431 with open(filename, "rb") as fp:
432 source = fp.read().splitlines()
433 except OSError:
434 pass
435 return source
437 def _get_lines_from_file(
438 self, filename, lineno, context_lines, loader=None, module_name=None
439 ):
440 """
441 Return context_lines before and after lineno from file.
442 Return (pre_context_lineno, pre_context, context_line, post_context).
443 """
444 source = self._get_source(filename, loader, module_name)
445 if source is None:
446 return None, [], None, []
448 # If we just read the source from a file, or if the loader did not
449 # apply tokenize.detect_encoding to decode the source into a
450 # string, then we should do that ourselves.
451 if isinstance(source[0], bytes):
452 encoding = "ascii"
453 for line in source[:2]:
454 # File coding may be specified. Match pattern from PEP-263
455 # (https://www.python.org/dev/peps/pep-0263/)
456 match = re.search(rb"coding[:=]\s*([-\w.]+)", line)
457 if match:
458 encoding = match[1].decode("ascii")
459 break
460 source = [str(sline, encoding, "replace") for sline in source]
462 lower_bound = max(0, lineno - context_lines)
463 upper_bound = lineno + context_lines
465 try:
466 pre_context = source[lower_bound:lineno]
467 context_line = source[lineno]
468 post_context = source[lineno + 1 : upper_bound]
469 except IndexError:
470 return None, [], None, []
471 return lower_bound, pre_context, context_line, post_context
473 def _get_explicit_or_implicit_cause(self, exc_value):
474 explicit = getattr(exc_value, "__cause__", None)
475 suppress_context = getattr(exc_value, "__suppress_context__", None)
476 implicit = getattr(exc_value, "__context__", None)
477 return explicit or (None if suppress_context else implicit)
479 def get_traceback_frames(self):
480 # Get the exception and all its causes
481 exceptions = []
482 exc_value = self.exc_value
483 while exc_value:
484 exceptions.append(exc_value)
485 exc_value = self._get_explicit_or_implicit_cause(exc_value)
486 if exc_value in exceptions:
487 warnings.warn(
488 "Cycle in the exception chain detected: exception '%s' "
489 "encountered again." % exc_value,
490 ExceptionCycleWarning,
491 )
492 # Avoid infinite loop if there's a cyclic reference (#29393).
493 break
495 frames = []
496 # No exceptions were supplied to ExceptionReporter
497 if not exceptions:
498 return frames
500 # In case there's just one exception, take the traceback from self.tb
501 exc_value = exceptions.pop()
502 tb = self.tb if not exceptions else exc_value.__traceback__
503 while True:
504 frames.extend(self.get_exception_traceback_frames(exc_value, tb))
505 try:
506 exc_value = exceptions.pop()
507 except IndexError:
508 break
509 tb = exc_value.__traceback__
510 return frames
512 def get_exception_traceback_frames(self, exc_value, tb):
513 exc_cause = self._get_explicit_or_implicit_cause(exc_value)
514 exc_cause_explicit = getattr(exc_value, "__cause__", True)
515 if tb is None:
516 yield {
517 "exc_cause": exc_cause,
518 "exc_cause_explicit": exc_cause_explicit,
519 "tb": None,
520 "type": "user",
521 }
522 while tb is not None:
523 # Support for __traceback_hide__ which is used by a few libraries
524 # to hide internal frames.
525 if tb.tb_frame.f_locals.get("__traceback_hide__"):
526 tb = tb.tb_next
527 continue
528 filename = tb.tb_frame.f_code.co_filename
529 function = tb.tb_frame.f_code.co_name
530 lineno = tb.tb_lineno - 1
531 loader = tb.tb_frame.f_globals.get("__loader__")
532 module_name = tb.tb_frame.f_globals.get("__name__") or ""
533 (
534 pre_context_lineno,
535 pre_context,
536 context_line,
537 post_context,
538 ) = self._get_lines_from_file(
539 filename,
540 lineno,
541 7,
542 loader,
543 module_name,
544 )
545 if pre_context_lineno is None:
546 pre_context_lineno = lineno
547 pre_context = []
548 context_line = "<source code not available>"
549 post_context = []
551 colno = tb_area_colno = ""
552 if PY311:
553 _, _, start_column, end_column = next(
554 itertools.islice(
555 tb.tb_frame.f_code.co_positions(), tb.tb_lasti // 2, None
556 )
557 )
558 if start_column and end_column:
559 underline = "^" * (end_column - start_column)
560 spaces = " " * (start_column + len(str(lineno + 1)) + 2)
561 colno = f"\n{spaces}{underline}"
562 tb_area_spaces = " " * (
563 4
564 + start_column
565 - (len(context_line) - len(context_line.lstrip()))
566 )
567 tb_area_colno = f"\n{tb_area_spaces}{underline}"
568 yield {
569 "exc_cause": exc_cause,
570 "exc_cause_explicit": exc_cause_explicit,
571 "tb": tb,
572 "type": "django" if module_name.startswith("django.") else "user",
573 "filename": filename,
574 "function": function,
575 "lineno": lineno + 1,
576 "vars": self.filter.get_traceback_frame_variables(
577 self.request, tb.tb_frame
578 ),
579 "id": id(tb),
580 "pre_context": pre_context,
581 "context_line": context_line,
582 "post_context": post_context,
583 "pre_context_lineno": pre_context_lineno + 1,
584 "colno": colno,
585 "tb_area_colno": tb_area_colno,
586 }
587 tb = tb.tb_next
590def technical_404_response(request, exception):
591 """Create a technical 404 error response. `exception` is the Http404."""
592 try:
593 error_url = exception.args[0]["path"]
594 except (IndexError, TypeError, KeyError):
595 error_url = request.path_info[1:] # Trim leading slash
597 try:
598 tried = exception.args[0]["tried"]
599 except (IndexError, TypeError, KeyError):
600 resolved = True
601 tried = request.resolver_match.tried if request.resolver_match else None
602 else:
603 resolved = False
604 if not tried or ( # empty URLconf
605 request.path == "/"
606 and len(tried) == 1
607 and len(tried[0]) == 1 # default URLconf
608 and getattr(tried[0][0], "app_name", "")
609 == getattr(tried[0][0], "namespace", "")
610 == "admin"
611 ):
612 return default_urlconf(request)
614 urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF)
615 if isinstance(urlconf, types.ModuleType):
616 urlconf = urlconf.__name__
618 with builtin_template_path("technical_404.html").open(encoding="utf-8") as fh:
619 t = DEBUG_ENGINE.from_string(fh.read())
620 reporter_filter = get_default_exception_reporter_filter()
621 c = Context(
622 {
623 "urlconf": urlconf,
624 "root_urlconf": settings.ROOT_URLCONF,
625 "request_path": error_url,
626 "urlpatterns": tried,
627 "resolved": resolved,
628 "reason": str(exception),
629 "request": request,
630 "settings": reporter_filter.get_safe_settings(),
631 "raising_view_name": get_caller(request),
632 }
633 )
634 return HttpResponseNotFound(t.render(c))
637def default_urlconf(request):
638 """Create an empty URLconf 404 error response."""
639 with builtin_template_path("default_urlconf.html").open(encoding="utf-8") as fh:
640 t = DEBUG_ENGINE.from_string(fh.read())
641 c = Context(
642 {
643 "version": get_docs_version(),
644 }
645 )
647 return HttpResponse(t.render(c))