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

1import functools 

2import itertools 

3import re 

4import sys 

5import types 

6import warnings 

7from pathlib import Path 

8 

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 

20 

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) 

29 

30 

31def builtin_template_path(name): 

32 """ 

33 Return a path to a builtin template. 

34 

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 

39 

40 

41class ExceptionCycleWarning(UserWarning): 

42 pass 

43 

44 

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 """ 

52 

53 def __init__(self, callable_setting): 

54 self._wrapped = callable_setting 

55 

56 def __repr__(self): 

57 return repr(self._wrapped) 

58 

59 

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 ) 

74 

75 

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)() 

80 

81 

82def get_exception_reporter_filter(request): 

83 default_filter = get_default_exception_reporter_filter() 

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

85 

86 

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 ) 

94 

95 

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 

104 

105 

106class SafeExceptionReporterFilter: 

107 """ 

108 Use annotations made by the sensitive_post_parameters and 

109 sensitive_variables decorators to filter out sensitive information. 

110 """ 

111 

112 cleansed_substitute = "********************" 

113 hidden_settings = _lazy_re_compile( 

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

115 ) 

116 

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 

129 

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 

140 

141 if callable(cleansed): 

142 cleansed = CallableSettingWrapper(cleansed) 

143 

144 return cleansed 

145 

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 

156 

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()} 

164 

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()} 

172 

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 

181 

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 

195 

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 

222 

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) 

231 

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 

236 

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 

257 

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) 

277 

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 

288 

289 return cleansed.items() 

290 

291 

292class ExceptionReporter: 

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

294 

295 @property 

296 def html_template_path(self): 

297 return builtin_template_path("technical_500.html") 

298 

299 @property 

300 def text_template_path(self): 

301 return builtin_template_path("technical_500.txt") 

302 

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 

310 

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

312 self.template_does_not_exist = False 

313 self.postmortem = None 

314 

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 ) 

325 

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] 

331 

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 

344 

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 

357 

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]" 

367 

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) 

394 

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 

405 

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) 

412 

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) 

419 

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 

436 

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, [] 

447 

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] 

461 

462 lower_bound = max(0, lineno - context_lines) 

463 upper_bound = lineno + context_lines 

464 

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 

472 

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) 

478 

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 

494 

495 frames = [] 

496 # No exceptions were supplied to ExceptionReporter 

497 if not exceptions: 

498 return frames 

499 

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 

511 

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 = [] 

550 

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 

588 

589 

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 

596 

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) 

613 

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

615 if isinstance(urlconf, types.ModuleType): 

616 urlconf = urlconf.__name__ 

617 

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)) 

635 

636 

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 ) 

646 

647 return HttpResponse(t.render(c))