Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/django/core/validators.py: 68%

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

320 statements  

1import ipaddress 

2import math 

3import re 

4from pathlib import Path 

5from urllib.parse import urlsplit 

6 

7from django.core.exceptions import ValidationError 

8from django.utils.deconstruct import deconstructible 

9from django.utils.ipv6 import is_valid_ipv6_address 

10from django.utils.regex_helper import _lazy_re_compile 

11from django.utils.translation import gettext_lazy as _ 

12from django.utils.translation import ngettext_lazy 

13 

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

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

16 

17 

18@deconstructible 

19class RegexValidator: 

20 regex = "" 

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

22 code = "invalid" 

23 inverse_match = False 

24 flags = 0 

25 

26 def __init__( 

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

28 ): 

29 if regex is not None: 

30 self.regex = regex 

31 if message is not None: 

32 self.message = message 

33 if code is not None: 

34 self.code = code 

35 if inverse_match is not None: 

36 self.inverse_match = inverse_match 

37 if flags is not None: 

38 self.flags = flags 

39 if self.flags and not isinstance(self.regex, str): 

40 raise TypeError( 

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

42 ) 

43 

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

45 

46 def __call__(self, value): 

47 """ 

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

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

50 """ 

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

52 invalid_input = regex_matches if self.inverse_match else not regex_matches 

53 if invalid_input: 

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

55 

56 def __eq__(self, other): 

57 return ( 

58 isinstance(other, RegexValidator) 

59 and self.regex.pattern == other.regex.pattern 

60 and self.regex.flags == other.regex.flags 

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

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

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

64 ) 

65 

66 

67@deconstructible 

68class DomainNameValidator(RegexValidator): 

69 message = _("Enter a valid domain name.") 

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

71 # Host patterns. 

72 hostname_re = ( 

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

74 ) 

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

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

77 # Top-level domain. 

78 tld_no_fqdn_re = ( 

79 r"\." # dot 

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

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

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

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

84 ) 

85 tld_re = tld_no_fqdn_re + r"\.?" 

86 ascii_only_hostname_re = r"[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?" 

87 ascii_only_domain_re = r"(?:\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-))*" 

88 ascii_only_tld_re = ( 

89 r"\." # dot 

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

91 r"(?:[a-zA-Z0-9-]{2,63})" # domain label 

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

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

94 ) 

95 

96 max_length = 255 

97 

98 def __init__(self, **kwargs): 

99 self.accept_idna = kwargs.pop("accept_idna", True) 

100 

101 if self.accept_idna: 

102 self.regex = _lazy_re_compile( 

103 r"^" + self.hostname_re + self.domain_re + self.tld_re + r"$", 

104 re.IGNORECASE, 

105 ) 

106 else: 

107 self.regex = _lazy_re_compile( 

108 r"^" 

109 + self.ascii_only_hostname_re 

110 + self.ascii_only_domain_re 

111 + self.ascii_only_tld_re 

112 + r"$", 

113 re.IGNORECASE, 

114 ) 

115 super().__init__(**kwargs) 

116 

117 def __call__(self, value): 

118 if not isinstance(value, str) or len(value) > self.max_length: 

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

120 if not self.accept_idna and not value.isascii(): 

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

122 super().__call__(value) 

123 

124 

125validate_domain_name = DomainNameValidator() 

126 

127 

128@deconstructible 

129class URLValidator(RegexValidator): 

130 # IP patterns 

131 ipv4_re = ( 

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

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

134 ) 

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

136 

137 hostname_re = DomainNameValidator.hostname_re 

138 domain_re = DomainNameValidator.domain_re 

139 tld_re = DomainNameValidator.tld_re 

140 

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

142 

143 regex = _lazy_re_compile( 

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

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

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

147 r"(?::[0-9]{1,5})?" # port 

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

149 r"\Z", 

150 re.IGNORECASE, 

151 ) 

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

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

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

155 max_length = 2048 

156 

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

158 super().__init__(**kwargs) 

159 if schemes is not None: 

160 self.schemes = schemes 

161 

162 def __call__(self, value): 

163 if not isinstance(value, str) or len(value) > self.max_length: 

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

165 if self.unsafe_chars.intersection(value): 

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

167 # Check if the scheme is valid. 

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

169 if scheme not in self.schemes: 

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

171 

172 # Then check full URL 

173 try: 

174 splitted_url = urlsplit(value) 

175 except ValueError: 

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

177 super().__call__(value) 

178 # Now verify IPv6 in the netloc part 

179 host_match = re.search(r"^\[(.+)\](?::[0-9]{1,5})?$", splitted_url.netloc) 

180 if host_match: 

181 potential_ip = host_match[1] 

182 try: 

183 validate_ipv6_address(potential_ip) 

184 except ValidationError: 

185 raise ValidationError( 

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

187 ) 

188 

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

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

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

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

193 if splitted_url.hostname is None or len(splitted_url.hostname) > 253: 

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

195 

196 

197integer_validator = RegexValidator( 

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

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

200 code="invalid", 

201) 

202 

203 

204def validate_integer(value): 

205 return integer_validator(value) 

206 

207 

208@deconstructible 

209class EmailValidator: 

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

211 code = "invalid" 

212 hostname_re = DomainNameValidator.hostname_re 

213 domain_re = DomainNameValidator.domain_re 

214 tld_no_fqdn_re = DomainNameValidator.tld_no_fqdn_re 

215 

216 user_regex = _lazy_re_compile( 

217 # dot-atom 

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

219 # quoted-string 

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

221 r'*"\Z)', 

222 re.IGNORECASE, 

223 ) 

224 domain_regex = _lazy_re_compile( 

225 r"^" + hostname_re + domain_re + tld_no_fqdn_re + r"\Z", 

226 re.IGNORECASE, 

227 ) 

228 literal_regex = _lazy_re_compile( 

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

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

231 re.IGNORECASE, 

232 ) 

233 domain_allowlist = ["localhost"] 

234 

235 def __init__(self, message=None, code=None, allowlist=None): 

236 if message is not None: 

237 self.message = message 

238 if code is not None: 

239 self.code = code 

240 if allowlist is not None: 

241 self.domain_allowlist = allowlist 

242 

243 def __call__(self, value): 

244 # The maximum length of an email is 320 characters per RFC 3696 

245 # section 3. 

246 if not value or "@" not in value or len(value) > 320: 

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

248 

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

250 

251 if not self.user_regex.match(user_part): 

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

253 

254 if domain_part not in self.domain_allowlist and not self.validate_domain_part( 

255 domain_part 

256 ): 

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

258 

259 def validate_domain_part(self, domain_part): 

260 if self.domain_regex.match(domain_part): 

261 return True 

262 

263 literal_match = self.literal_regex.match(domain_part) 

264 if literal_match: 

265 ip_address = literal_match[1] 

266 try: 

267 validate_ipv46_address(ip_address) 

268 return True 

269 except ValidationError: 

270 pass 

271 return False 

272 

273 def __eq__(self, other): 

274 return ( 

275 isinstance(other, EmailValidator) 

276 and (set(self.domain_allowlist) == set(other.domain_allowlist)) 

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

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

279 ) 

280 

281 

282validate_email = EmailValidator() 

283 

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

285validate_slug = RegexValidator( 

286 slug_re, 

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

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

289 "invalid", 

290) 

291 

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

293validate_unicode_slug = RegexValidator( 

294 slug_unicode_re, 

295 _( 

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

297 "hyphens." 

298 ), 

299 "invalid", 

300) 

301 

302 

303def validate_ipv4_address(value): 

304 try: 

305 ipaddress.IPv4Address(value) 

306 except ValueError: 

307 raise ValidationError( 

308 _("Enter a valid %(protocol)s address."), 

309 code="invalid", 

310 params={"protocol": _("IPv4"), "value": value}, 

311 ) 

312 

313 

314def validate_ipv6_address(value): 

315 if not is_valid_ipv6_address(value): 

316 raise ValidationError( 

317 _("Enter a valid %(protocol)s address."), 

318 code="invalid", 

319 params={"protocol": _("IPv6"), "value": value}, 

320 ) 

321 

322 

323def validate_ipv46_address(value): 

324 try: 

325 validate_ipv4_address(value) 

326 except ValidationError: 

327 try: 

328 validate_ipv6_address(value) 

329 except ValidationError: 

330 raise ValidationError( 

331 _("Enter a valid %(protocol)s address."), 

332 code="invalid", 

333 params={"protocol": _("IPv4 or IPv6"), "value": value}, 

334 ) 

335 

336 

337ip_address_validator_map = { 

338 "both": [validate_ipv46_address], 

339 "ipv4": [validate_ipv4_address], 

340 "ipv6": [validate_ipv6_address], 

341} 

342 

343 

344def ip_address_validators(protocol, unpack_ipv4): 

345 """ 

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

347 the GenericIPAddressField. 

348 """ 

349 if protocol != "both" and unpack_ipv4: 

350 raise ValueError( 

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

352 ) 

353 try: 

354 return ip_address_validator_map[protocol.lower()] 

355 except KeyError: 

356 raise ValueError( 

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

358 % (protocol, list(ip_address_validator_map)) 

359 ) 

360 

361 

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

363 regexp = _lazy_re_compile( 

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

365 % { 

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

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

368 } 

369 ) 

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

371 

372 

373validate_comma_separated_integer_list = int_list_validator( 

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

375) 

376 

377 

378@deconstructible 

379class BaseValidator: 

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

381 code = "limit_value" 

382 

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

384 self.limit_value = limit_value 

385 if message: 

386 self.message = message 

387 

388 def __call__(self, value): 

389 cleaned = self.clean(value) 

390 limit_value = ( 

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

392 ) 

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

394 if self.compare(cleaned, limit_value): 

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

396 

397 def __eq__(self, other): 

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

399 return NotImplemented 

400 return ( 

401 self.limit_value == other.limit_value 

402 and self.message == other.message 

403 and self.code == other.code 

404 ) 

405 

406 def compare(self, a, b): 

407 return a is not b 

408 

409 def clean(self, x): 

410 return x 

411 

412 

413@deconstructible 

414class MaxValueValidator(BaseValidator): 

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

416 code = "max_value" 

417 

418 def compare(self, a, b): 

419 return a > b 

420 

421 

422@deconstructible 

423class MinValueValidator(BaseValidator): 

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

425 code = "min_value" 

426 

427 def compare(self, a, b): 

428 return a < b 

429 

430 

431@deconstructible 

432class StepValueValidator(BaseValidator): 

433 message = _("Ensure this value is a multiple of step size %(limit_value)s.") 

434 code = "step_size" 

435 

436 def __init__(self, limit_value, message=None, offset=None): 

437 super().__init__(limit_value, message) 

438 if offset is not None: 

439 self.message = _( 

440 "Ensure this value is a multiple of step size %(limit_value)s, " 

441 "starting from %(offset)s, e.g. %(offset)s, %(valid_value1)s, " 

442 "%(valid_value2)s, and so on." 

443 ) 

444 self.offset = offset 

445 

446 def __call__(self, value): 

447 if self.offset is None: 

448 super().__call__(value) 

449 else: 

450 cleaned = self.clean(value) 

451 limit_value = ( 

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

453 ) 

454 if self.compare(cleaned, limit_value): 

455 offset = cleaned.__class__(self.offset) 

456 params = { 

457 "limit_value": limit_value, 

458 "offset": offset, 

459 "valid_value1": offset + limit_value, 

460 "valid_value2": offset + 2 * limit_value, 

461 } 

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

463 

464 def compare(self, a, b): 

465 offset = 0 if self.offset is None else self.offset 

466 return not math.isclose(math.remainder(a - offset, b), 0, abs_tol=1e-9) 

467 

468 

469@deconstructible 

470class MinLengthValidator(BaseValidator): 

471 message = ngettext_lazy( 

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

473 "%(show_value)d).", 

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

475 "%(show_value)d).", 

476 "limit_value", 

477 ) 

478 code = "min_length" 

479 

480 def compare(self, a, b): 

481 return a < b 

482 

483 def clean(self, x): 

484 return len(x) 

485 

486 

487@deconstructible 

488class MaxLengthValidator(BaseValidator): 

489 message = ngettext_lazy( 

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

491 "%(show_value)d).", 

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

493 "%(show_value)d).", 

494 "limit_value", 

495 ) 

496 code = "max_length" 

497 

498 def compare(self, a, b): 

499 return a > b 

500 

501 def clean(self, x): 

502 return len(x) 

503 

504 

505@deconstructible 

506class DecimalValidator: 

507 """ 

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

509 expected, otherwise raise ValidationError. 

510 """ 

511 

512 messages = { 

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

514 "max_digits": ngettext_lazy( 

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

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

517 "max", 

518 ), 

519 "max_decimal_places": ngettext_lazy( 

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

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

522 "max", 

523 ), 

524 "max_whole_digits": ngettext_lazy( 

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

526 "point.", 

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

528 "point.", 

529 "max", 

530 ), 

531 } 

532 

533 def __init__(self, max_digits, decimal_places): 

534 self.max_digits = max_digits 

535 self.decimal_places = decimal_places 

536 

537 def __call__(self, value): 

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

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

540 raise ValidationError( 

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

542 ) 

543 if exponent >= 0: 

544 digits = len(digit_tuple) 

545 if digit_tuple != (0,): 

546 # A positive exponent adds that many trailing zeros. 

547 digits += exponent 

548 decimals = 0 

549 else: 

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

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

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

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

554 # decimal point. 

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

556 digits = decimals = abs(exponent) 

557 else: 

558 digits = len(digit_tuple) 

559 decimals = abs(exponent) 

560 whole_digits = digits - decimals 

561 

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

563 raise ValidationError( 

564 self.messages["max_digits"], 

565 code="max_digits", 

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

567 ) 

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

569 raise ValidationError( 

570 self.messages["max_decimal_places"], 

571 code="max_decimal_places", 

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

573 ) 

574 if ( 

575 self.max_digits is not None 

576 and self.decimal_places is not None 

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

578 ): 

579 raise ValidationError( 

580 self.messages["max_whole_digits"], 

581 code="max_whole_digits", 

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

583 ) 

584 

585 def __eq__(self, other): 

586 return ( 

587 isinstance(other, self.__class__) 

588 and self.max_digits == other.max_digits 

589 and self.decimal_places == other.decimal_places 

590 ) 

591 

592 

593@deconstructible 

594class FileExtensionValidator: 

595 message = _( 

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

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

598 ) 

599 code = "invalid_extension" 

600 

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

602 if allowed_extensions is not None: 

603 allowed_extensions = [ 

604 allowed_extension.lower() for allowed_extension in allowed_extensions 

605 ] 

606 self.allowed_extensions = allowed_extensions 

607 if message is not None: 

608 self.message = message 

609 if code is not None: 

610 self.code = code 

611 

612 def __call__(self, value): 

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

614 if ( 

615 self.allowed_extensions is not None 

616 and extension not in self.allowed_extensions 

617 ): 

618 raise ValidationError( 

619 self.message, 

620 code=self.code, 

621 params={ 

622 "extension": extension, 

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

624 "value": value, 

625 }, 

626 ) 

627 

628 def __eq__(self, other): 

629 return ( 

630 isinstance(other, self.__class__) 

631 and set(self.allowed_extensions or []) 

632 == set(other.allowed_extensions or []) 

633 and self.message == other.message 

634 and self.code == other.code 

635 ) 

636 

637 

638def get_available_image_extensions(): 

639 try: 

640 from PIL import Image 

641 except ImportError: 

642 return [] 

643 else: 

644 Image.init() 

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

646 

647 

648def validate_image_file_extension(value): 

649 return FileExtensionValidator(allowed_extensions=get_available_image_extensions())( 

650 value 

651 ) 

652 

653 

654@deconstructible 

655class ProhibitNullCharactersValidator: 

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

657 

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

659 code = "null_characters_not_allowed" 

660 

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

662 if message is not None: 

663 self.message = message 

664 if code is not None: 

665 self.code = code 

666 

667 def __call__(self, value): 

668 if "\x00" in str(value): 

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

670 

671 def __eq__(self, other): 

672 return ( 

673 isinstance(other, self.__class__) 

674 and self.message == other.message 

675 and self.code == other.code 

676 )