Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/core/validators.py: 57%

300 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1import ipaddress 

2import re 

3import warnings 

4from pathlib import Path 

5from urllib.parse import urlsplit, urlunsplit 

6 

7from django.core.exceptions import ValidationError 

8from django.utils.deconstruct import deconstructible 

9from django.utils.deprecation import RemovedInDjango41Warning 

10from django.utils.encoding import punycode 

11from django.utils.ipv6 import is_valid_ipv6_address 

12from django.utils.regex_helper import _lazy_re_compile 

13from django.utils.translation import gettext_lazy as _ 

14from django.utils.translation import ngettext_lazy 

15 

16# These values, if given to validate(), will trigger the self.required check. 

17EMPTY_VALUES = (None, "", [], (), {}) 

18 

19 

20@deconstructible 

21class RegexValidator: 

22 regex = "" 

23 message = _("Enter a valid value.") 

24 code = "invalid" 

25 inverse_match = False 

26 flags = 0 

27 

28 def __init__( 

29 self, regex=None, message=None, code=None, inverse_match=None, flags=None 

30 ): 

31 if regex is not None: 

32 self.regex = regex 

33 if message is not None: 

34 self.message = message 

35 if code is not None: 

36 self.code = code 

37 if inverse_match is not None: 37 ↛ 38line 37 didn't jump to line 38, because the condition on line 37 was never true

38 self.inverse_match = inverse_match 

39 if flags is not None: 39 ↛ 40line 39 didn't jump to line 40, because the condition on line 39 was never true

40 self.flags = flags 

41 if self.flags and not isinstance(self.regex, str): 41 ↛ 42line 41 didn't jump to line 42, because the condition on line 41 was never true

42 raise TypeError( 

43 "If the flags are set, regex must be a regular expression string." 

44 ) 

45 

46 self.regex = _lazy_re_compile(self.regex, self.flags) 

47 

48 def __call__(self, value): 

49 """ 

50 Validate that the input contains (or does *not* contain, if 

51 inverse_match is True) a match for the regular expression. 

52 """ 

53 regex_matches = self.regex.search(str(value)) 

54 invalid_input = regex_matches if self.inverse_match else not regex_matches 

55 if invalid_input: 55 ↛ 56line 55 didn't jump to line 56, because the condition on line 55 was never true

56 raise ValidationError(self.message, code=self.code, params={"value": value}) 

57 

58 def __eq__(self, other): 

59 return ( 

60 isinstance(other, RegexValidator) 

61 and self.regex.pattern == other.regex.pattern 

62 and self.regex.flags == other.regex.flags 

63 and (self.message == other.message) 

64 and (self.code == other.code) 

65 and (self.inverse_match == other.inverse_match) 

66 ) 

67 

68 

69@deconstructible 

70class URLValidator(RegexValidator): 

71 ul = "\u00a1-\uffff" # Unicode letters range (must not be a raw string). 

72 

73 # IP patterns 

74 ipv4_re = ( 

75 r"(?:0|25[0-5]|2[0-4]\d|1\d?\d?|[1-9]\d?)" 

76 r"(?:\.(?:0|25[0-5]|2[0-4]\d|1\d?\d?|[1-9]\d?)){3}" 

77 ) 

78 ipv6_re = r"\[[0-9a-f:.]+\]" # (simple regex, validated later) 

79 

80 # Host patterns 

81 hostname_re = ( 

82 r"[a-z" + ul + r"0-9](?:[a-z" + ul + r"0-9-]{0,61}[a-z" + ul + r"0-9])?" 

83 ) 

84 # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1 

85 domain_re = r"(?:\.(?!-)[a-z" + ul + r"0-9-]{1,63}(?<!-))*" 

86 tld_re = ( 

87 r"\." # dot 

88 r"(?!-)" # can't start with a dash 

89 r"(?:[a-z" + ul + "-]{2,63}" # domain label 

90 r"|xn--[a-z0-9]{1,59})" # or punycode label 

91 r"(?<!-)" # can't end with a dash 

92 r"\.?" # may have a trailing dot 

93 ) 

94 host_re = "(" + hostname_re + domain_re + tld_re + "|localhost)" 

95 

96 regex = _lazy_re_compile( 

97 r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately 

98 r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication 

99 r"(?:" + ipv4_re + "|" + ipv6_re + "|" + host_re + ")" 

100 r"(?::\d{1,5})?" # port 

101 r"(?:[/?#][^\s]*)?" # resource path 

102 r"\Z", 

103 re.IGNORECASE, 

104 ) 

105 message = _("Enter a valid URL.") 

106 schemes = ["http", "https", "ftp", "ftps"] 

107 unsafe_chars = frozenset("\t\r\n") 

108 

109 def __init__(self, schemes=None, **kwargs): 

110 super().__init__(**kwargs) 

111 if schemes is not None: 111 ↛ 112line 111 didn't jump to line 112, because the condition on line 111 was never true

112 self.schemes = schemes 

113 

114 def __call__(self, value): 

115 if not isinstance(value, str): 

116 raise ValidationError(self.message, code=self.code, params={"value": value}) 

117 if self.unsafe_chars.intersection(value): 

118 raise ValidationError(self.message, code=self.code, params={"value": value}) 

119 # Check if the scheme is valid. 

120 scheme = value.split("://")[0].lower() 

121 if scheme not in self.schemes: 

122 raise ValidationError(self.message, code=self.code, params={"value": value}) 

123 

124 # Then check full URL 

125 try: 

126 super().__call__(value) 

127 except ValidationError as e: 

128 # Trivial case failed. Try for possible IDN domain 

129 if value: 

130 try: 

131 scheme, netloc, path, query, fragment = urlsplit(value) 

132 except ValueError: # for example, "Invalid IPv6 URL" 

133 raise ValidationError( 

134 self.message, code=self.code, params={"value": value} 

135 ) 

136 try: 

137 netloc = punycode(netloc) # IDN -> ACE 

138 except UnicodeError: # invalid domain part 

139 raise e 

140 url = urlunsplit((scheme, netloc, path, query, fragment)) 

141 super().__call__(url) 

142 else: 

143 raise 

144 else: 

145 # Now verify IPv6 in the netloc part 

146 host_match = re.search(r"^\[(.+)\](?::\d{1,5})?$", urlsplit(value).netloc) 

147 if host_match: 

148 potential_ip = host_match[1] 

149 try: 

150 validate_ipv6_address(potential_ip) 

151 except ValidationError: 

152 raise ValidationError( 

153 self.message, code=self.code, params={"value": value} 

154 ) 

155 

156 # The maximum length of a full host name is 253 characters per RFC 1034 

157 # section 3.1. It's defined to be 255 bytes or less, but this includes 

158 # one byte for the length of the name and one byte for the trailing dot 

159 # that's used to indicate absolute names in DNS. 

160 if len(urlsplit(value).hostname) > 253: 

161 raise ValidationError(self.message, code=self.code, params={"value": value}) 

162 

163 

164integer_validator = RegexValidator( 

165 _lazy_re_compile(r"^-?\d+\Z"), 

166 message=_("Enter a valid integer."), 

167 code="invalid", 

168) 

169 

170 

171def validate_integer(value): 

172 return integer_validator(value) 

173 

174 

175@deconstructible 

176class EmailValidator: 

177 message = _("Enter a valid email address.") 

178 code = "invalid" 

179 user_regex = _lazy_re_compile( 

180 # dot-atom 

181 r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z" 

182 # quoted-string 

183 r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])' 

184 r'*"\Z)', 

185 re.IGNORECASE, 

186 ) 

187 domain_regex = _lazy_re_compile( 

188 # max length for domain name labels is 63 characters per RFC 1034 

189 r"((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\Z", 

190 re.IGNORECASE, 

191 ) 

192 literal_regex = _lazy_re_compile( 

193 # literal form, ipv4 or ipv6 address (SMTP 4.1.3) 

194 r"\[([A-F0-9:.]+)\]\Z", 

195 re.IGNORECASE, 

196 ) 

197 domain_allowlist = ["localhost"] 

198 

199 @property 

200 def domain_whitelist(self): 

201 warnings.warn( 

202 "The domain_whitelist attribute is deprecated in favor of " 

203 "domain_allowlist.", 

204 RemovedInDjango41Warning, 

205 stacklevel=2, 

206 ) 

207 return self.domain_allowlist 

208 

209 @domain_whitelist.setter 

210 def domain_whitelist(self, allowlist): 

211 warnings.warn( 

212 "The domain_whitelist attribute is deprecated in favor of " 

213 "domain_allowlist.", 

214 RemovedInDjango41Warning, 

215 stacklevel=2, 

216 ) 

217 self.domain_allowlist = allowlist 

218 

219 def __init__(self, message=None, code=None, allowlist=None, *, whitelist=None): 

220 if whitelist is not None: 220 ↛ 221line 220 didn't jump to line 221, because the condition on line 220 was never true

221 allowlist = whitelist 

222 warnings.warn( 

223 "The whitelist argument is deprecated in favor of allowlist.", 

224 RemovedInDjango41Warning, 

225 stacklevel=2, 

226 ) 

227 if message is not None: 

228 self.message = message 

229 if code is not None: 229 ↛ 230line 229 didn't jump to line 230, because the condition on line 229 was never true

230 self.code = code 

231 if allowlist is not None: 231 ↛ 232line 231 didn't jump to line 232, because the condition on line 231 was never true

232 self.domain_allowlist = allowlist 

233 

234 def __call__(self, value): 

235 if not value or "@" not in value: 235 ↛ 236line 235 didn't jump to line 236, because the condition on line 235 was never true

236 raise ValidationError(self.message, code=self.code, params={"value": value}) 

237 

238 user_part, domain_part = value.rsplit("@", 1) 

239 

240 if not self.user_regex.match(user_part): 240 ↛ 241line 240 didn't jump to line 241, because the condition on line 240 was never true

241 raise ValidationError(self.message, code=self.code, params={"value": value}) 

242 

243 if domain_part not in self.domain_allowlist and not self.validate_domain_part( 243 ↛ 247line 243 didn't jump to line 247, because the condition on line 243 was never true

244 domain_part 

245 ): 

246 # Try for possible IDN domain-part 

247 try: 

248 domain_part = punycode(domain_part) 

249 except UnicodeError: 

250 pass 

251 else: 

252 if self.validate_domain_part(domain_part): 

253 return 

254 raise ValidationError(self.message, code=self.code, params={"value": value}) 

255 

256 def validate_domain_part(self, domain_part): 

257 if self.domain_regex.match(domain_part): 257 ↛ 260line 257 didn't jump to line 260, because the condition on line 257 was never false

258 return True 

259 

260 literal_match = self.literal_regex.match(domain_part) 

261 if literal_match: 

262 ip_address = literal_match[1] 

263 try: 

264 validate_ipv46_address(ip_address) 

265 return True 

266 except ValidationError: 

267 pass 

268 return False 

269 

270 def __eq__(self, other): 

271 return ( 

272 isinstance(other, EmailValidator) 

273 and (self.domain_allowlist == other.domain_allowlist) 

274 and (self.message == other.message) 

275 and (self.code == other.code) 

276 ) 

277 

278 

279validate_email = EmailValidator() 

280 

281slug_re = _lazy_re_compile(r"^[-a-zA-Z0-9_]+\Z") 

282validate_slug = RegexValidator( 

283 slug_re, 

284 # Translators: "letters" means latin letters: a-z and A-Z. 

285 _("Enter a valid “slug” consisting of letters, numbers, underscores or hyphens."), 

286 "invalid", 

287) 

288 

289slug_unicode_re = _lazy_re_compile(r"^[-\w]+\Z") 

290validate_unicode_slug = RegexValidator( 

291 slug_unicode_re, 

292 _( 

293 "Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " 

294 "hyphens." 

295 ), 

296 "invalid", 

297) 

298 

299 

300def validate_ipv4_address(value): 

301 try: 

302 ipaddress.IPv4Address(value) 

303 except ValueError: 

304 raise ValidationError( 

305 _("Enter a valid IPv4 address."), code="invalid", params={"value": value} 

306 ) 

307 else: 

308 # Leading zeros are forbidden to avoid ambiguity with the octal 

309 # notation. This restriction is included in Python 3.9.5+. 

310 # TODO: Remove when dropping support for PY39. 

311 if any(octet != "0" and octet[0] == "0" for octet in value.split(".")): 

312 raise ValidationError( 

313 _("Enter a valid IPv4 address."), 

314 code="invalid", 

315 params={"value": value}, 

316 ) 

317 

318 

319def validate_ipv6_address(value): 

320 if not is_valid_ipv6_address(value): 

321 raise ValidationError( 

322 _("Enter a valid IPv6 address."), code="invalid", params={"value": value} 

323 ) 

324 

325 

326def validate_ipv46_address(value): 

327 try: 

328 validate_ipv4_address(value) 

329 except ValidationError: 

330 try: 

331 validate_ipv6_address(value) 

332 except ValidationError: 

333 raise ValidationError( 

334 _("Enter a valid IPv4 or IPv6 address."), 

335 code="invalid", 

336 params={"value": value}, 

337 ) 

338 

339 

340ip_address_validator_map = { 

341 "both": ([validate_ipv46_address], _("Enter a valid IPv4 or IPv6 address.")), 

342 "ipv4": ([validate_ipv4_address], _("Enter a valid IPv4 address.")), 

343 "ipv6": ([validate_ipv6_address], _("Enter a valid IPv6 address.")), 

344} 

345 

346 

347def ip_address_validators(protocol, unpack_ipv4): 

348 """ 

349 Depending on the given parameters, return the appropriate validators for 

350 the GenericIPAddressField. 

351 """ 

352 if protocol != "both" and unpack_ipv4: 

353 raise ValueError( 

354 "You can only use `unpack_ipv4` if `protocol` is set to 'both'" 

355 ) 

356 try: 

357 return ip_address_validator_map[protocol.lower()] 

358 except KeyError: 

359 raise ValueError( 

360 "The protocol '%s' is unknown. Supported: %s" 

361 % (protocol, list(ip_address_validator_map)) 

362 ) 

363 

364 

365def int_list_validator(sep=",", message=None, code="invalid", allow_negative=False): 

366 regexp = _lazy_re_compile( 

367 r"^%(neg)s\d+(?:%(sep)s%(neg)s\d+)*\Z" 

368 % { 

369 "neg": "(-)?" if allow_negative else "", 

370 "sep": re.escape(sep), 

371 } 

372 ) 

373 return RegexValidator(regexp, message=message, code=code) 

374 

375 

376validate_comma_separated_integer_list = int_list_validator( 

377 message=_("Enter only digits separated by commas."), 

378) 

379 

380 

381@deconstructible 

382class BaseValidator: 

383 message = _("Ensure this value is %(limit_value)s (it is %(show_value)s).") 

384 code = "limit_value" 

385 

386 def __init__(self, limit_value, message=None): 

387 self.limit_value = limit_value 

388 if message: 

389 self.message = message 

390 

391 def __call__(self, value): 

392 cleaned = self.clean(value) 

393 limit_value = ( 

394 self.limit_value() if callable(self.limit_value) else self.limit_value 

395 ) 

396 params = {"limit_value": limit_value, "show_value": cleaned, "value": value} 

397 if self.compare(cleaned, limit_value): 397 ↛ 398line 397 didn't jump to line 398, because the condition on line 397 was never true

398 raise ValidationError(self.message, code=self.code, params=params) 

399 

400 def __eq__(self, other): 

401 if not isinstance(other, self.__class__): 

402 return NotImplemented 

403 return ( 

404 self.limit_value == other.limit_value 

405 and self.message == other.message 

406 and self.code == other.code 

407 ) 

408 

409 def compare(self, a, b): 

410 return a is not b 

411 

412 def clean(self, x): 

413 return x 

414 

415 

416@deconstructible 

417class MaxValueValidator(BaseValidator): 

418 message = _("Ensure this value is less than or equal to %(limit_value)s.") 

419 code = "max_value" 

420 

421 def compare(self, a, b): 

422 return a > b 

423 

424 

425@deconstructible 

426class MinValueValidator(BaseValidator): 

427 message = _("Ensure this value is greater than or equal to %(limit_value)s.") 

428 code = "min_value" 

429 

430 def compare(self, a, b): 

431 return a < b 

432 

433 

434@deconstructible 

435class MinLengthValidator(BaseValidator): 

436 message = ngettext_lazy( 

437 "Ensure this value has at least %(limit_value)d character (it has " 

438 "%(show_value)d).", 

439 "Ensure this value has at least %(limit_value)d characters (it has " 

440 "%(show_value)d).", 

441 "limit_value", 

442 ) 

443 code = "min_length" 

444 

445 def compare(self, a, b): 

446 return a < b 

447 

448 def clean(self, x): 

449 return len(x) 

450 

451 

452@deconstructible 

453class MaxLengthValidator(BaseValidator): 

454 message = ngettext_lazy( 

455 "Ensure this value has at most %(limit_value)d character (it has " 

456 "%(show_value)d).", 

457 "Ensure this value has at most %(limit_value)d characters (it has " 

458 "%(show_value)d).", 

459 "limit_value", 

460 ) 

461 code = "max_length" 

462 

463 def compare(self, a, b): 

464 return a > b 

465 

466 def clean(self, x): 

467 return len(x) 

468 

469 

470@deconstructible 

471class DecimalValidator: 

472 """ 

473 Validate that the input does not exceed the maximum number of digits 

474 expected, otherwise raise ValidationError. 

475 """ 

476 

477 messages = { 

478 "invalid": _("Enter a number."), 

479 "max_digits": ngettext_lazy( 

480 "Ensure that there are no more than %(max)s digit in total.", 

481 "Ensure that there are no more than %(max)s digits in total.", 

482 "max", 

483 ), 

484 "max_decimal_places": ngettext_lazy( 

485 "Ensure that there are no more than %(max)s decimal place.", 

486 "Ensure that there are no more than %(max)s decimal places.", 

487 "max", 

488 ), 

489 "max_whole_digits": ngettext_lazy( 

490 "Ensure that there are no more than %(max)s digit before the decimal " 

491 "point.", 

492 "Ensure that there are no more than %(max)s digits before the decimal " 

493 "point.", 

494 "max", 

495 ), 

496 } 

497 

498 def __init__(self, max_digits, decimal_places): 

499 self.max_digits = max_digits 

500 self.decimal_places = decimal_places 

501 

502 def __call__(self, value): 

503 digit_tuple, exponent = value.as_tuple()[1:] 

504 if exponent in {"F", "n", "N"}: 

505 raise ValidationError( 

506 self.messages["invalid"], code="invalid", params={"value": value} 

507 ) 

508 if exponent >= 0: 

509 # A positive exponent adds that many trailing zeros. 

510 digits = len(digit_tuple) + exponent 

511 decimals = 0 

512 else: 

513 # If the absolute value of the negative exponent is larger than the 

514 # number of digits, then it's the same as the number of digits, 

515 # because it'll consume all of the digits in digit_tuple and then 

516 # add abs(exponent) - len(digit_tuple) leading zeros after the 

517 # decimal point. 

518 if abs(exponent) > len(digit_tuple): 

519 digits = decimals = abs(exponent) 

520 else: 

521 digits = len(digit_tuple) 

522 decimals = abs(exponent) 

523 whole_digits = digits - decimals 

524 

525 if self.max_digits is not None and digits > self.max_digits: 

526 raise ValidationError( 

527 self.messages["max_digits"], 

528 code="max_digits", 

529 params={"max": self.max_digits, "value": value}, 

530 ) 

531 if self.decimal_places is not None and decimals > self.decimal_places: 

532 raise ValidationError( 

533 self.messages["max_decimal_places"], 

534 code="max_decimal_places", 

535 params={"max": self.decimal_places, "value": value}, 

536 ) 

537 if ( 

538 self.max_digits is not None 

539 and self.decimal_places is not None 

540 and whole_digits > (self.max_digits - self.decimal_places) 

541 ): 

542 raise ValidationError( 

543 self.messages["max_whole_digits"], 

544 code="max_whole_digits", 

545 params={"max": (self.max_digits - self.decimal_places), "value": value}, 

546 ) 

547 

548 def __eq__(self, other): 

549 return ( 

550 isinstance(other, self.__class__) 

551 and self.max_digits == other.max_digits 

552 and self.decimal_places == other.decimal_places 

553 ) 

554 

555 

556@deconstructible 

557class FileExtensionValidator: 

558 message = _( 

559 "File extension “%(extension)s” is not allowed. " 

560 "Allowed extensions are: %(allowed_extensions)s." 

561 ) 

562 code = "invalid_extension" 

563 

564 def __init__(self, allowed_extensions=None, message=None, code=None): 

565 if allowed_extensions is not None: 565 ↛ 569line 565 didn't jump to line 569, because the condition on line 565 was never false

566 allowed_extensions = [ 

567 allowed_extension.lower() for allowed_extension in allowed_extensions 

568 ] 

569 self.allowed_extensions = allowed_extensions 

570 if message is not None: 570 ↛ 571line 570 didn't jump to line 571, because the condition on line 570 was never true

571 self.message = message 

572 if code is not None: 572 ↛ 573line 572 didn't jump to line 573, because the condition on line 572 was never true

573 self.code = code 

574 

575 def __call__(self, value): 

576 extension = Path(value.name).suffix[1:].lower() 

577 if ( 577 ↛ 581line 577 didn't jump to line 581

578 self.allowed_extensions is not None 

579 and extension not in self.allowed_extensions 

580 ): 

581 raise ValidationError( 

582 self.message, 

583 code=self.code, 

584 params={ 

585 "extension": extension, 

586 "allowed_extensions": ", ".join(self.allowed_extensions), 

587 "value": value, 

588 }, 

589 ) 

590 

591 def __eq__(self, other): 

592 return ( 

593 isinstance(other, self.__class__) 

594 and self.allowed_extensions == other.allowed_extensions 

595 and self.message == other.message 

596 and self.code == other.code 

597 ) 

598 

599 

600def get_available_image_extensions(): 

601 try: 

602 from PIL import Image 

603 except ImportError: 

604 return [] 

605 else: 

606 Image.init() 

607 return [ext.lower()[1:] for ext in Image.EXTENSION] 

608 

609 

610def validate_image_file_extension(value): 

611 return FileExtensionValidator(allowed_extensions=get_available_image_extensions())( 

612 value 

613 ) 

614 

615 

616@deconstructible 

617class ProhibitNullCharactersValidator: 

618 """Validate that the string doesn't contain the null character.""" 

619 

620 message = _("Null characters are not allowed.") 

621 code = "null_characters_not_allowed" 

622 

623 def __init__(self, message=None, code=None): 

624 if message is not None: 624 ↛ 625line 624 didn't jump to line 625, because the condition on line 624 was never true

625 self.message = message 

626 if code is not None: 626 ↛ 627line 626 didn't jump to line 627, because the condition on line 626 was never true

627 self.code = code 

628 

629 def __call__(self, value): 

630 if "\x00" in str(value): 630 ↛ 631line 630 didn't jump to line 631, because the condition on line 630 was never true

631 raise ValidationError(self.message, code=self.code, params={"value": value}) 

632 

633 def __eq__(self, other): 

634 return ( 

635 isinstance(other, self.__class__) 

636 and self.message == other.message 

637 and self.code == other.code 

638 )