Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/django/views/debug.py: 18%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

346 statements  

1import functools 

2import inspect 

3import itertools 

4import re 

5import sys 

6import types 

7import warnings 

8from pathlib import Path 

9 

10from django.conf import settings 

11from django.http import Http404, HttpResponse, HttpResponseNotFound 

12from django.template import Context, Engine, TemplateDoesNotExist 

13from django.template.defaultfilters import pprint 

14from django.urls import resolve 

15from django.utils import timezone 

16from django.utils.datastructures import MultiValueDict 

17from django.utils.encoding import force_str 

18from django.utils.module_loading import import_string 

19from django.utils.regex_helper import _lazy_re_compile 

20from django.utils.version import PY311, get_docs_version 

21from django.views.decorators.debug import coroutine_functions_to_sensitive_variables 

22 

23# Minimal Django templates engine to render the error templates 

24# regardless of the project's TEMPLATES setting. Templates are 

25# read directly from the filesystem so that the error handler 

26# works even if the template loader is broken. 

27DEBUG_ENGINE = Engine( 

28 debug=True, 

29 libraries={"i18n": "django.templatetags.i18n"}, 

30) 

31 

32 

33def builtin_template_path(name): 

34 """ 

35 Return a path to a builtin template. 

36 

37 Avoid calling this function at the module level or in a class-definition 

38 because __file__ may not exist, e.g. in frozen environments. 

39 """ 

40 return Path(__file__).parent / "templates" / name 

41 

42 

43class ExceptionCycleWarning(UserWarning): 

44 pass 

45 

46 

47class CallableSettingWrapper: 

48 """ 

49 Object to wrap callable appearing in settings. 

50 * Not to call in the debug page (#21345). 

51 * Not to break the debug page if the callable forbidding to set attributes 

52 (#23070). 

53 """ 

54 

55 def __init__(self, callable_setting): 

56 self._wrapped = callable_setting 

57 

58 def __repr__(self): 

59 return repr(self._wrapped) 

60 

61 

62def technical_500_response(request, exc_type, exc_value, tb, status_code=500): 

63 """ 

64 Create a technical server error response. The last three arguments are 

65 the values returned from sys.exc_info() and friends. 

66 """ 

67 reporter = get_exception_reporter_class(request)(request, exc_type, exc_value, tb) 

68 if request.accepts("text/html"): 

69 html = reporter.get_traceback_html() 

70 return HttpResponse(html, status=status_code) 

71 else: 

72 text = reporter.get_traceback_text() 

73 return HttpResponse( 

74 text, status=status_code, content_type="text/plain; charset=utf-8" 

75 ) 

76 

77 

78@functools.lru_cache 

79def get_default_exception_reporter_filter(): 

80 # Instantiate the default filter for the first time and cache it. 

81 return import_string(settings.DEFAULT_EXCEPTION_REPORTER_FILTER)() 

82 

83 

84def get_exception_reporter_filter(request): 

85 default_filter = get_default_exception_reporter_filter() 

86 return getattr(request, "exception_reporter_filter", default_filter) 

87 

88 

89def get_exception_reporter_class(request): 

90 default_exception_reporter_class = import_string( 

91 settings.DEFAULT_EXCEPTION_REPORTER 

92 ) 

93 return getattr( 

94 request, "exception_reporter_class", default_exception_reporter_class 

95 ) 

96 

97 

98def get_caller(request): 

99 resolver_match = request.resolver_match 

100 if resolver_match is None: 

101 try: 

102 resolver_match = resolve(request.path) 

103 except Http404: 

104 pass 

105 return "" if resolver_match is None else resolver_match._func_path 

106 

107 

108class SafeExceptionReporterFilter: 

109 """ 

110 Use annotations made by the sensitive_post_parameters and 

111 sensitive_variables decorators to filter out sensitive information. 

112 """ 

113 

114 cleansed_substitute = "********************" 

115 hidden_settings = _lazy_re_compile( 

116 "API|AUTH|TOKEN|KEY|SECRET|PASS|SIGNATURE|HTTP_COOKIE", flags=re.I 

117 ) 

118 

119 def cleanse_setting(self, key, value): 

120 """ 

121 Cleanse an individual setting key/value of sensitive content. If the 

122 value is a dictionary, recursively cleanse the keys in that dictionary. 

123 """ 

124 if key == settings.SESSION_COOKIE_NAME: 

125 is_sensitive = True 

126 else: 

127 try: 

128 is_sensitive = self.hidden_settings.search(key) 

129 except TypeError: 

130 is_sensitive = False 

131 

132 if is_sensitive: 

133 cleansed = self.cleansed_substitute 

134 elif isinstance(value, dict): 

135 cleansed = {k: self.cleanse_setting(k, v) for k, v in value.items()} 

136 elif isinstance(value, list): 

137 cleansed = [self.cleanse_setting("", v) for v in value] 

138 elif isinstance(value, tuple): 

139 cleansed = tuple([self.cleanse_setting("", v) for v in value]) 

140 else: 

141 cleansed = value 

142 

143 if callable(cleansed): 

144 cleansed = CallableSettingWrapper(cleansed) 

145 

146 return cleansed 

147 

148 def get_safe_settings(self): 

149 """ 

150 Return a dictionary of the settings module with values of sensitive 

151 settings replaced with stars (*********). 

152 """ 

153 settings_dict = {} 

154 for k in dir(settings): 

155 if k.isupper(): 

156 settings_dict[k] = self.cleanse_setting(k, getattr(settings, k)) 

157 return settings_dict 

158 

159 def get_safe_request_meta(self, request): 

160 """ 

161 Return a dictionary of request.META with sensitive values redacted. 

162 """ 

163 if not hasattr(request, "META"): 

164 return {} 

165 return {k: self.cleanse_setting(k, v) for k, v in request.META.items()} 

166 

167 def get_safe_cookies(self, request): 

168 """ 

169 Return a dictionary of request.COOKIES with sensitive values redacted. 

170 """ 

171 if not hasattr(request, "COOKIES"): 

172 return {} 

173 return {k: self.cleanse_setting(k, v) for k, v in request.COOKIES.items()} 

174 

175 def is_active(self, request): 

176 """ 

177 This filter is to add safety in production environments (i.e. DEBUG 

178 is False). If DEBUG is True then your site is not safe anyway. 

179 This hook is provided as a convenience to easily activate or 

180 deactivate the filter on a per request basis. 

181 """ 

182 return settings.DEBUG is False 

183 

184 def get_cleansed_multivaluedict(self, request, multivaluedict): 

185 """ 

186 Replace the keys in a MultiValueDict marked as sensitive with stars. 

187 This mitigates leaking sensitive POST parameters if something like 

188 request.POST['nonexistent_key'] throws an exception (#21098). 

189 """ 

190 sensitive_post_parameters = getattr(request, "sensitive_post_parameters", []) 

191 if self.is_active(request) and sensitive_post_parameters: 

192 multivaluedict = multivaluedict.copy() 

193 for param in sensitive_post_parameters: 

194 if param in multivaluedict: 

195 multivaluedict[param] = self.cleansed_substitute 

196 return multivaluedict 

197 

198 def get_post_parameters(self, request): 

199 """ 

200 Replace the values of POST parameters marked as sensitive with 

201 stars (*********). 

202 """ 

203 if request is None: 

204 return {} 

205 else: 

206 sensitive_post_parameters = getattr( 

207 request, "sensitive_post_parameters", [] 

208 ) 

209 if self.is_active(request) and sensitive_post_parameters: 

210 cleansed = request.POST.copy() 

211 if sensitive_post_parameters == "__ALL__": 

212 # Cleanse all parameters. 

213 for k in cleansed: 

214 cleansed[k] = self.cleansed_substitute 

215 return cleansed 

216 else: 

217 # Cleanse only the specified parameters. 

218 for param in sensitive_post_parameters: 

219 if param in cleansed: 

220 cleansed[param] = self.cleansed_substitute 

221 return cleansed 

222 else: 

223 return request.POST 

224 

225 def cleanse_special_types(self, request, value): 

226 try: 

227 # If value is lazy or a complex object of another kind, this check 

228 # might raise an exception. isinstance checks that lazy 

229 # MultiValueDicts will have a return value. 

230 is_multivalue_dict = isinstance(value, MultiValueDict) 

231 except Exception as e: 

232 return "{!r} while evaluating {!r}".format(e, value) 

233 

234 if is_multivalue_dict: 

235 # Cleanse MultiValueDicts (request.POST is the one we usually care about) 

236 value = self.get_cleansed_multivaluedict(request, value) 

237 return value 

238 

239 def get_traceback_frame_variables(self, request, tb_frame): 

240 """ 

241 Replace the values of variables marked as sensitive with 

242 stars (*********). 

243 """ 

244 sensitive_variables = None 

245 

246 # Coroutines don't have a proper `f_back` so they need to be inspected 

247 # separately. Handle this by stashing the registered sensitive 

248 # variables in a global dict indexed by `hash(file_path:line_number)`. 

249 if ( 

250 tb_frame.f_code.co_flags & inspect.CO_COROUTINE != 0 

251 and tb_frame.f_code.co_name != "sensitive_variables_wrapper" 

252 ): 

253 key = hash( 

254 f"{tb_frame.f_code.co_filename}:{tb_frame.f_code.co_firstlineno}" 

255 ) 

256 sensitive_variables = coroutine_functions_to_sensitive_variables.get( 

257 key, None 

258 ) 

259 

260 if sensitive_variables is None: 

261 # Loop through the frame's callers to see if the 

262 # sensitive_variables decorator was used. 

263 current_frame = tb_frame 

264 while current_frame is not None: 

265 if ( 

266 current_frame.f_code.co_name == "sensitive_variables_wrapper" 

267 and "sensitive_variables_wrapper" in current_frame.f_locals 

268 ): 

269 # The sensitive_variables decorator was used, so take note 

270 # of the sensitive variables' names. 

271 wrapper = current_frame.f_locals["sensitive_variables_wrapper"] 

272 sensitive_variables = getattr(wrapper, "sensitive_variables", None) 

273 break 

274 current_frame = current_frame.f_back 

275 

276 cleansed = {} 

277 if self.is_active(request) and sensitive_variables: 

278 if sensitive_variables == "__ALL__": 

279 # Cleanse all variables 

280 for name in tb_frame.f_locals: 

281 cleansed[name] = self.cleansed_substitute 

282 else: 

283 # Cleanse specified variables 

284 for name, value in tb_frame.f_locals.items(): 

285 if name in sensitive_variables: 

286 value = self.cleansed_substitute 

287 else: 

288 value = self.cleanse_special_types(request, value) 

289 cleansed[name] = value 

290 else: 

291 # Potentially cleanse the request and any MultiValueDicts if they 

292 # are one of the frame variables. 

293 for name, value in tb_frame.f_locals.items(): 

294 cleansed[name] = self.cleanse_special_types(request, value) 

295 

296 if ( 

297 tb_frame.f_code.co_name == "sensitive_variables_wrapper" 

298 and "sensitive_variables_wrapper" in tb_frame.f_locals 

299 ): 

300 # For good measure, obfuscate the decorated function's arguments in 

301 # the sensitive_variables decorator's frame, in case the variables 

302 # associated with those arguments were meant to be obfuscated from 

303 # the decorated function's frame. 

304 cleansed["func_args"] = self.cleansed_substitute 

305 cleansed["func_kwargs"] = self.cleansed_substitute 

306 

307 return cleansed.items() 

308 

309 

310class ExceptionReporter: 

311 """Organize and coordinate reporting on exceptions.""" 

312 

313 @property 

314 def html_template_path(self): 

315 return builtin_template_path("technical_500.html") 

316 

317 @property 

318 def text_template_path(self): 

319 return builtin_template_path("technical_500.txt") 

320 

321 def __init__(self, request, exc_type, exc_value, tb, is_email=False): 

322 self.request = request 

323 self.filter = get_exception_reporter_filter(self.request) 

324 self.exc_type = exc_type 

325 self.exc_value = exc_value 

326 self.tb = tb 

327 self.is_email = is_email 

328 

329 self.template_info = getattr(self.exc_value, "template_debug", None) 

330 self.template_does_not_exist = False 

331 self.postmortem = None 

332 

333 def _get_raw_insecure_uri(self): 

334 """ 

335 Return an absolute URI from variables available in this request. Skip 

336 allowed hosts protection, so may return insecure URI. 

337 """ 

338 return "{scheme}://{host}{path}".format( 

339 scheme=self.request.scheme, 

340 host=self.request._get_raw_host(), 

341 path=self.request.get_full_path(), 

342 ) 

343 

344 def get_traceback_data(self): 

345 """Return a dictionary containing traceback information.""" 

346 if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist): 

347 self.template_does_not_exist = True 

348 self.postmortem = self.exc_value.chain or [self.exc_value] 

349 

350 frames = self.get_traceback_frames() 

351 for i, frame in enumerate(frames): 

352 if "vars" in frame: 

353 frame_vars = [] 

354 for k, v in frame["vars"]: 

355 v = pprint(v) 

356 # Trim large blobs of data 

357 if len(v) > 4096: 

358 v = "%s… <trimmed %d bytes string>" % (v[0:4096], len(v)) 

359 frame_vars.append((k, v)) 

360 frame["vars"] = frame_vars 

361 frames[i] = frame 

362 

363 unicode_hint = "" 

364 if self.exc_type and issubclass(self.exc_type, UnicodeError): 

365 start = getattr(self.exc_value, "start", None) 

366 end = getattr(self.exc_value, "end", None) 

367 if start is not None and end is not None: 

368 unicode_str = self.exc_value.args[1] 

369 unicode_hint = force_str( 

370 unicode_str[max(start - 5, 0) : min(end + 5, len(unicode_str))], 

371 "ascii", 

372 errors="replace", 

373 ) 

374 from django import get_version 

375 

376 if self.request is None: 

377 user_str = None 

378 else: 

379 try: 

380 user_str = str(self.request.user) 

381 except Exception: 

382 # request.user may raise OperationalError if the database is 

383 # unavailable, for example. 

384 user_str = "[unable to retrieve the current user]" 

385 

386 c = { 

387 "is_email": self.is_email, 

388 "unicode_hint": unicode_hint, 

389 "frames": frames, 

390 "request": self.request, 

391 "request_meta": self.filter.get_safe_request_meta(self.request), 

392 "request_COOKIES_items": self.filter.get_safe_cookies(self.request).items(), 

393 "user_str": user_str, 

394 "filtered_POST_items": list( 

395 self.filter.get_post_parameters(self.request).items() 

396 ), 

397 "settings": self.filter.get_safe_settings(), 

398 "sys_executable": sys.executable, 

399 "sys_version_info": "%d.%d.%d" % sys.version_info[0:3], 

400 "server_time": timezone.now(), 

401 "django_version_info": get_version(), 

402 "sys_path": sys.path, 

403 "template_info": self.template_info, 

404 "template_does_not_exist": self.template_does_not_exist, 

405 "postmortem": self.postmortem, 

406 } 

407 if self.request is not None: 

408 c["request_GET_items"] = self.request.GET.items() 

409 c["request_FILES_items"] = self.request.FILES.items() 

410 c["request_insecure_uri"] = self._get_raw_insecure_uri() 

411 c["raising_view_name"] = get_caller(self.request) 

412 

413 # Check whether exception info is available 

414 if self.exc_type: 

415 c["exception_type"] = self.exc_type.__name__ 

416 if self.exc_value: 

417 c["exception_value"] = str(self.exc_value) 

418 if exc_notes := getattr(self.exc_value, "__notes__", None): 

419 c["exception_notes"] = "\n" + "\n".join(exc_notes) 

420 if frames: 

421 c["lastframe"] = frames[-1] 

422 return c 

423 

424 def get_traceback_html(self): 

425 """Return HTML version of debug 500 HTTP error page.""" 

426 with self.html_template_path.open(encoding="utf-8") as fh: 

427 t = DEBUG_ENGINE.from_string(fh.read()) 

428 c = Context(self.get_traceback_data(), use_l10n=False) 

429 return t.render(c) 

430 

431 def get_traceback_text(self): 

432 """Return plain text version of debug 500 HTTP error page.""" 

433 with self.text_template_path.open(encoding="utf-8") as fh: 

434 t = DEBUG_ENGINE.from_string(fh.read()) 

435 c = Context(self.get_traceback_data(), autoescape=False, use_l10n=False) 

436 return t.render(c) 

437 

438 def _get_source(self, filename, loader, module_name): 

439 source = None 

440 if hasattr(loader, "get_source"): 

441 try: 

442 source = loader.get_source(module_name) 

443 except ImportError: 

444 pass 

445 if source is not None: 

446 source = source.splitlines() 

447 if source is None: 

448 try: 

449 with open(filename, "rb") as fp: 

450 source = fp.read().splitlines() 

451 except OSError: 

452 pass 

453 return source 

454 

455 def _get_lines_from_file( 

456 self, filename, lineno, context_lines, loader=None, module_name=None 

457 ): 

458 """ 

459 Return context_lines before and after lineno from file. 

460 Return (pre_context_lineno, pre_context, context_line, post_context). 

461 """ 

462 source = self._get_source(filename, loader, module_name) 

463 if source is None: 

464 return None, [], None, [] 

465 

466 # If we just read the source from a file, or if the loader did not 

467 # apply tokenize.detect_encoding to decode the source into a 

468 # string, then we should do that ourselves. 

469 if isinstance(source[0], bytes): 

470 encoding = "ascii" 

471 for line in source[:2]: 

472 # File coding may be specified. Match pattern from PEP-263 

473 # (https://www.python.org/dev/peps/pep-0263/) 

474 match = re.search(rb"coding[:=]\s*([-\w.]+)", line) 

475 if match: 

476 encoding = match[1].decode("ascii") 

477 break 

478 source = [str(sline, encoding, "replace") for sline in source] 

479 

480 lower_bound = max(0, lineno - context_lines) 

481 upper_bound = lineno + context_lines 

482 

483 try: 

484 pre_context = source[lower_bound:lineno] 

485 context_line = source[lineno] 

486 post_context = source[lineno + 1 : upper_bound] 

487 except IndexError: 

488 return None, [], None, [] 

489 return lower_bound, pre_context, context_line, post_context 

490 

491 def _get_explicit_or_implicit_cause(self, exc_value): 

492 explicit = getattr(exc_value, "__cause__", None) 

493 suppress_context = getattr(exc_value, "__suppress_context__", None) 

494 implicit = getattr(exc_value, "__context__", None) 

495 return explicit or (None if suppress_context else implicit) 

496 

497 def get_traceback_frames(self): 

498 # Get the exception and all its causes 

499 exceptions = [] 

500 exc_value = self.exc_value 

501 while exc_value: 

502 exceptions.append(exc_value) 

503 exc_value = self._get_explicit_or_implicit_cause(exc_value) 

504 if exc_value in exceptions: 

505 warnings.warn( 

506 "Cycle in the exception chain detected: exception '%s' " 

507 "encountered again." % exc_value, 

508 ExceptionCycleWarning, 

509 ) 

510 # Avoid infinite loop if there's a cyclic reference (#29393). 

511 break 

512 

513 frames = [] 

514 # No exceptions were supplied to ExceptionReporter 

515 if not exceptions: 

516 return frames 

517 

518 # In case there's just one exception, take the traceback from self.tb 

519 exc_value = exceptions.pop() 

520 tb = self.tb if not exceptions else exc_value.__traceback__ 

521 while True: 

522 frames.extend(self.get_exception_traceback_frames(exc_value, tb)) 

523 try: 

524 exc_value = exceptions.pop() 

525 except IndexError: 

526 break 

527 tb = exc_value.__traceback__ 

528 return frames 

529 

530 def get_exception_traceback_frames(self, exc_value, tb): 

531 exc_cause = self._get_explicit_or_implicit_cause(exc_value) 

532 exc_cause_explicit = getattr(exc_value, "__cause__", True) 

533 if tb is None: 

534 yield { 

535 "exc_cause": exc_cause, 

536 "exc_cause_explicit": exc_cause_explicit, 

537 "tb": None, 

538 "type": "user", 

539 } 

540 while tb is not None: 

541 # Support for __traceback_hide__ which is used by a few libraries 

542 # to hide internal frames. 

543 if tb.tb_frame.f_locals.get("__traceback_hide__"): 

544 tb = tb.tb_next 

545 continue 

546 filename = tb.tb_frame.f_code.co_filename 

547 function = tb.tb_frame.f_code.co_name 

548 lineno = tb.tb_lineno - 1 

549 loader = tb.tb_frame.f_globals.get("__loader__") 

550 module_name = tb.tb_frame.f_globals.get("__name__") or "" 

551 ( 

552 pre_context_lineno, 

553 pre_context, 

554 context_line, 

555 post_context, 

556 ) = self._get_lines_from_file( 

557 filename, 

558 lineno, 

559 7, 

560 loader, 

561 module_name, 

562 ) 

563 if pre_context_lineno is None: 

564 pre_context_lineno = lineno 

565 pre_context = [] 

566 context_line = "<source code not available>" 

567 post_context = [] 

568 

569 colno = tb_area_colno = "" 

570 if PY311: 

571 _, _, start_column, end_column = next( 

572 itertools.islice( 

573 tb.tb_frame.f_code.co_positions(), tb.tb_lasti // 2, None 

574 ) 

575 ) 

576 if start_column and end_column: 

577 underline = "^" * (end_column - start_column) 

578 spaces = " " * (start_column + len(str(lineno + 1)) + 2) 

579 colno = f"\n{spaces}{underline}" 

580 tb_area_spaces = " " * ( 

581 4 

582 + start_column 

583 - (len(context_line) - len(context_line.lstrip())) 

584 ) 

585 tb_area_colno = f"\n{tb_area_spaces}{underline}" 

586 yield { 

587 "exc_cause": exc_cause, 

588 "exc_cause_explicit": exc_cause_explicit, 

589 "tb": tb, 

590 "type": "django" if module_name.startswith("django.") else "user", 

591 "filename": filename, 

592 "function": function, 

593 "lineno": lineno + 1, 

594 "vars": self.filter.get_traceback_frame_variables( 

595 self.request, tb.tb_frame 

596 ), 

597 "id": id(tb), 

598 "pre_context": pre_context, 

599 "context_line": context_line, 

600 "post_context": post_context, 

601 "pre_context_lineno": pre_context_lineno + 1, 

602 "colno": colno, 

603 "tb_area_colno": tb_area_colno, 

604 } 

605 tb = tb.tb_next 

606 

607 

608def technical_404_response(request, exception): 

609 """Create a technical 404 error response. `exception` is the Http404.""" 

610 try: 

611 error_url = exception.args[0]["path"] 

612 except (IndexError, TypeError, KeyError): 

613 error_url = request.path_info[1:] # Trim leading slash 

614 

615 try: 

616 tried = exception.args[0]["tried"] 

617 except (IndexError, TypeError, KeyError): 

618 resolved = True 

619 tried = request.resolver_match.tried if request.resolver_match else None 

620 else: 

621 resolved = False 

622 if not tried or ( # empty URLconf 

623 request.path_info == "/" 

624 and len(tried) == 1 

625 and len(tried[0]) == 1 # default URLconf 

626 and getattr(tried[0][0], "app_name", "") 

627 == getattr(tried[0][0], "namespace", "") 

628 == "admin" 

629 ): 

630 return default_urlconf(request) 

631 

632 urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF) 

633 if isinstance(urlconf, types.ModuleType): 

634 urlconf = urlconf.__name__ 

635 

636 with builtin_template_path("technical_404.html").open(encoding="utf-8") as fh: 

637 t = DEBUG_ENGINE.from_string(fh.read()) 

638 reporter_filter = get_default_exception_reporter_filter() 

639 c = Context( 

640 { 

641 "urlconf": urlconf, 

642 "root_urlconf": settings.ROOT_URLCONF, 

643 "request_path": error_url, 

644 "urlpatterns": tried, 

645 "resolved": resolved, 

646 "reason": str(exception), 

647 "request": request, 

648 "settings": reporter_filter.get_safe_settings(), 

649 "raising_view_name": get_caller(request), 

650 } 

651 ) 

652 return HttpResponseNotFound(t.render(c)) 

653 

654 

655def default_urlconf(request): 

656 """Create an empty URLconf 404 error response.""" 

657 with builtin_template_path("default_urlconf.html").open(encoding="utf-8") as fh: 

658 t = DEBUG_ENGINE.from_string(fh.read()) 

659 c = Context( 

660 { 

661 "version": get_docs_version(), 

662 } 

663 ) 

664 

665 return HttpResponse(t.render(c))