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

297 statements  

« prev     ^ index     » next       coverage.py v7.0.5, created at 2023-01-17 06:13 +0000

1import ipaddress 

2import math 

3import re 

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.encoding import punycode 

10from django.utils.ipv6 import is_valid_ipv6_address 

11from django.utils.regex_helper import _lazy_re_compile 

12from django.utils.translation import gettext_lazy as _ 

13from django.utils.translation import ngettext_lazy 

14 

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

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

17 

18 

19@deconstructible 

20class RegexValidator: 

21 regex = "" 

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

23 code = "invalid" 

24 inverse_match = False 

25 flags = 0 

26 

27 def __init__( 

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

29 ): 

30 if regex is not None: 

31 self.regex = regex 

32 if message is not None: 

33 self.message = message 

34 if code is not None: 

35 self.code = code 

36 if inverse_match is not None: 

37 self.inverse_match = inverse_match 

38 if flags is not None: 

39 self.flags = flags 

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

41 raise TypeError( 

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

43 ) 

44 

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

46 

47 def __call__(self, value): 

48 """ 

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

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

51 """ 

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

53 invalid_input = regex_matches if self.inverse_match else not regex_matches 

54 if invalid_input: 

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

56 

57 def __eq__(self, other): 

58 return ( 

59 isinstance(other, RegexValidator) 

60 and self.regex.pattern == other.regex.pattern 

61 and self.regex.flags == other.regex.flags 

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

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

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

65 ) 

66 

67 

68@deconstructible 

69class URLValidator(RegexValidator): 

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

71 

72 # IP patterns 

73 ipv4_re = ( 

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

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

76 ) 

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

78 

79 # Host patterns 

80 hostname_re = ( 

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

82 ) 

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

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

85 tld_re = ( 

86 r"\." # dot 

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

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

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

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

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

92 ) 

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

94 

95 regex = _lazy_re_compile( 

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

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

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

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

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

101 r"\Z", 

102 re.IGNORECASE, 

103 ) 

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

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

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

107 

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

109 super().__init__(**kwargs) 

110 if schemes is not None: 

111 self.schemes = schemes 

112 

113 def __call__(self, value): 

114 if not isinstance(value, str): 

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

116 if self.unsafe_chars.intersection(value): 

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

118 # Check if the scheme is valid. 

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

120 if scheme not in self.schemes: 

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

122 

123 # Then check full URL 

124 try: 

125 splitted_url = urlsplit(value) 

126 except ValueError: 

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

128 try: 

129 super().__call__(value) 

130 except ValidationError as e: 

131 # Trivial case failed. Try for possible IDN domain 

132 if value: 

133 scheme, netloc, path, query, fragment = splitted_url 

134 try: 

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

136 except UnicodeError: # invalid domain part 

137 raise e 

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

139 super().__call__(url) 

140 else: 

141 raise 

142 else: 

143 # Now verify IPv6 in the netloc part 

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

145 if host_match: 

146 potential_ip = host_match[1] 

147 try: 

148 validate_ipv6_address(potential_ip) 

149 except ValidationError: 

150 raise ValidationError( 

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

152 ) 

153 

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

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

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

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

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

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

160 

161 

162integer_validator = RegexValidator( 

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

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

165 code="invalid", 

166) 

167 

168 

169def validate_integer(value): 

170 return integer_validator(value) 

171 

172 

173@deconstructible 

174class EmailValidator: 

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

176 code = "invalid" 

177 user_regex = _lazy_re_compile( 

178 # dot-atom 

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

180 # quoted-string 

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

182 r'*"\Z)', 

183 re.IGNORECASE, 

184 ) 

185 domain_regex = _lazy_re_compile( 

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

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

188 re.IGNORECASE, 

189 ) 

190 literal_regex = _lazy_re_compile( 

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

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

193 re.IGNORECASE, 

194 ) 

195 domain_allowlist = ["localhost"] 

196 

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

198 if message is not None: 

199 self.message = message 

200 if code is not None: 

201 self.code = code 

202 if allowlist is not None: 

203 self.domain_allowlist = allowlist 

204 

205 def __call__(self, value): 

206 if not value or "@" not in value: 

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

208 

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

210 

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

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

213 

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

215 domain_part 

216 ): 

217 # Try for possible IDN domain-part 

218 try: 

219 domain_part = punycode(domain_part) 

220 except UnicodeError: 

221 pass 

222 else: 

223 if self.validate_domain_part(domain_part): 

224 return 

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

226 

227 def validate_domain_part(self, domain_part): 

228 if self.domain_regex.match(domain_part): 

229 return True 

230 

231 literal_match = self.literal_regex.match(domain_part) 

232 if literal_match: 

233 ip_address = literal_match[1] 

234 try: 

235 validate_ipv46_address(ip_address) 

236 return True 

237 except ValidationError: 

238 pass 

239 return False 

240 

241 def __eq__(self, other): 

242 return ( 

243 isinstance(other, EmailValidator) 

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

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

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

247 ) 

248 

249 

250validate_email = EmailValidator() 

251 

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

253validate_slug = RegexValidator( 

254 slug_re, 

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

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

257 "invalid", 

258) 

259 

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

261validate_unicode_slug = RegexValidator( 

262 slug_unicode_re, 

263 _( 

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

265 "hyphens." 

266 ), 

267 "invalid", 

268) 

269 

270 

271def validate_ipv4_address(value): 

272 try: 

273 ipaddress.IPv4Address(value) 

274 except ValueError: 

275 raise ValidationError( 

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

277 ) 

278 else: 

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

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

281 # TODO: Remove when dropping support for PY39. 

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

283 raise ValidationError( 

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

285 code="invalid", 

286 params={"value": value}, 

287 ) 

288 

289 

290def validate_ipv6_address(value): 

291 if not is_valid_ipv6_address(value): 

292 raise ValidationError( 

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

294 ) 

295 

296 

297def validate_ipv46_address(value): 

298 try: 

299 validate_ipv4_address(value) 

300 except ValidationError: 

301 try: 

302 validate_ipv6_address(value) 

303 except ValidationError: 

304 raise ValidationError( 

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

306 code="invalid", 

307 params={"value": value}, 

308 ) 

309 

310 

311ip_address_validator_map = { 

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

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

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

315} 

316 

317 

318def ip_address_validators(protocol, unpack_ipv4): 

319 """ 

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

321 the GenericIPAddressField. 

322 """ 

323 if protocol != "both" and unpack_ipv4: 

324 raise ValueError( 

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

326 ) 

327 try: 

328 return ip_address_validator_map[protocol.lower()] 

329 except KeyError: 

330 raise ValueError( 

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

332 % (protocol, list(ip_address_validator_map)) 

333 ) 

334 

335 

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

337 regexp = _lazy_re_compile( 

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

339 % { 

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

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

342 } 

343 ) 

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

345 

346 

347validate_comma_separated_integer_list = int_list_validator( 

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

349) 

350 

351 

352@deconstructible 

353class BaseValidator: 

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

355 code = "limit_value" 

356 

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

358 self.limit_value = limit_value 

359 if message: 

360 self.message = message 

361 

362 def __call__(self, value): 

363 cleaned = self.clean(value) 

364 limit_value = ( 

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

366 ) 

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

368 if self.compare(cleaned, limit_value): 

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

370 

371 def __eq__(self, other): 

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

373 return NotImplemented 

374 return ( 

375 self.limit_value == other.limit_value 

376 and self.message == other.message 

377 and self.code == other.code 

378 ) 

379 

380 def compare(self, a, b): 

381 return a is not b 

382 

383 def clean(self, x): 

384 return x 

385 

386 

387@deconstructible 

388class MaxValueValidator(BaseValidator): 

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

390 code = "max_value" 

391 

392 def compare(self, a, b): 

393 return a > b 

394 

395 

396@deconstructible 

397class MinValueValidator(BaseValidator): 

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

399 code = "min_value" 

400 

401 def compare(self, a, b): 

402 return a < b 

403 

404 

405@deconstructible 

406class StepValueValidator(BaseValidator): 

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

408 code = "step_size" 

409 

410 def compare(self, a, b): 

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

412 

413 

414@deconstructible 

415class MinLengthValidator(BaseValidator): 

416 message = ngettext_lazy( 

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

418 "%(show_value)d).", 

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

420 "%(show_value)d).", 

421 "limit_value", 

422 ) 

423 code = "min_length" 

424 

425 def compare(self, a, b): 

426 return a < b 

427 

428 def clean(self, x): 

429 return len(x) 

430 

431 

432@deconstructible 

433class MaxLengthValidator(BaseValidator): 

434 message = ngettext_lazy( 

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

436 "%(show_value)d).", 

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

438 "%(show_value)d).", 

439 "limit_value", 

440 ) 

441 code = "max_length" 

442 

443 def compare(self, a, b): 

444 return a > b 

445 

446 def clean(self, x): 

447 return len(x) 

448 

449 

450@deconstructible 

451class DecimalValidator: 

452 """ 

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

454 expected, otherwise raise ValidationError. 

455 """ 

456 

457 messages = { 

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

459 "max_digits": ngettext_lazy( 

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

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

462 "max", 

463 ), 

464 "max_decimal_places": ngettext_lazy( 

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

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

467 "max", 

468 ), 

469 "max_whole_digits": ngettext_lazy( 

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

471 "point.", 

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

473 "point.", 

474 "max", 

475 ), 

476 } 

477 

478 def __init__(self, max_digits, decimal_places): 

479 self.max_digits = max_digits 

480 self.decimal_places = decimal_places 

481 

482 def __call__(self, value): 

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

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

485 raise ValidationError( 

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

487 ) 

488 if exponent >= 0: 

489 digits = len(digit_tuple) 

490 if digit_tuple != (0,): 

491 # A positive exponent adds that many trailing zeros. 

492 digits += exponent 

493 decimals = 0 

494 else: 

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

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

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

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

499 # decimal point. 

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

501 digits = decimals = abs(exponent) 

502 else: 

503 digits = len(digit_tuple) 

504 decimals = abs(exponent) 

505 whole_digits = digits - decimals 

506 

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

508 raise ValidationError( 

509 self.messages["max_digits"], 

510 code="max_digits", 

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

512 ) 

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

514 raise ValidationError( 

515 self.messages["max_decimal_places"], 

516 code="max_decimal_places", 

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

518 ) 

519 if ( 

520 self.max_digits is not None 

521 and self.decimal_places is not None 

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

523 ): 

524 raise ValidationError( 

525 self.messages["max_whole_digits"], 

526 code="max_whole_digits", 

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

528 ) 

529 

530 def __eq__(self, other): 

531 return ( 

532 isinstance(other, self.__class__) 

533 and self.max_digits == other.max_digits 

534 and self.decimal_places == other.decimal_places 

535 ) 

536 

537 

538@deconstructible 

539class FileExtensionValidator: 

540 message = _( 

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

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

543 ) 

544 code = "invalid_extension" 

545 

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

547 if allowed_extensions is not None: 

548 allowed_extensions = [ 

549 allowed_extension.lower() for allowed_extension in allowed_extensions 

550 ] 

551 self.allowed_extensions = allowed_extensions 

552 if message is not None: 

553 self.message = message 

554 if code is not None: 

555 self.code = code 

556 

557 def __call__(self, value): 

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

559 if ( 

560 self.allowed_extensions is not None 

561 and extension not in self.allowed_extensions 

562 ): 

563 raise ValidationError( 

564 self.message, 

565 code=self.code, 

566 params={ 

567 "extension": extension, 

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

569 "value": value, 

570 }, 

571 ) 

572 

573 def __eq__(self, other): 

574 return ( 

575 isinstance(other, self.__class__) 

576 and self.allowed_extensions == other.allowed_extensions 

577 and self.message == other.message 

578 and self.code == other.code 

579 ) 

580 

581 

582def get_available_image_extensions(): 

583 try: 

584 from PIL import Image 

585 except ImportError: 

586 return [] 

587 else: 

588 Image.init() 

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

590 

591 

592def validate_image_file_extension(value): 

593 return FileExtensionValidator(allowed_extensions=get_available_image_extensions())( 

594 value 

595 ) 

596 

597 

598@deconstructible 

599class ProhibitNullCharactersValidator: 

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

601 

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

603 code = "null_characters_not_allowed" 

604 

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

606 if message is not None: 

607 self.message = message 

608 if code is not None: 

609 self.code = code 

610 

611 def __call__(self, value): 

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

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

614 

615 def __eq__(self, other): 

616 return ( 

617 isinstance(other, self.__class__) 

618 and self.message == other.message 

619 and self.code == other.code 

620 )