Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/validators/url.py: 96%
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
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
1"""URL."""
3# standard
4from functools import lru_cache
5import re
6from typing import Callable, Optional
7from urllib.parse import parse_qs, unquote, urlsplit
9# local
10from .hostname import hostname
11from .utils import validator
14@lru_cache
15def _username_regex():
16 return re.compile(
17 # extended latin
18 r"(^[\u0100-\u017F\u0180-\u024F]"
19 # dot-atom
20 + r"|[-!#$%&'*+/=?^_`{}|~0-9a-z]+(\.[-!#$%&'*+/=?^_`{}|~0-9a-z]+)*$"
21 # non-quoted-string
22 + r"|^([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\011.])*$)",
23 re.IGNORECASE,
24 )
27@lru_cache
28def _path_regex():
29 return re.compile(
30 # allowed symbols
31 r"^[\/a-z0-9\-\.\_\~\!\$\&\'\(\)\*\+\,\;\=\:\@\%"
32 # symbols / pictographs
33 + r"\U0001F300-\U0001F5FF"
34 # emoticons / emoji
35 + r"\U0001F600-\U0001F64F"
36 # multilingual unicode ranges
37 + r"\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+$",
38 re.IGNORECASE,
39 )
42def _validate_scheme(value: str):
43 """Validate scheme."""
44 # More schemes will be considered later.
45 return (
46 value
47 # fmt: off
48 in {
49 "file",
50 "ftp",
51 "ftps",
52 "git",
53 "http",
54 "https",
55 "irc",
56 "rtmp",
57 "rtmps",
58 "rtsp",
59 "sftp",
60 "ssh",
61 "telnet",
62 }
63 # fmt: on
64 if value
65 else False
66 )
69def _confirm_ipv6_skip(value: str, skip_ipv6_addr: bool):
70 """Confirm skip IPv6 check."""
71 return skip_ipv6_addr or value.count(":") < 2 or not value.startswith("[")
74def _validate_auth_segment(value: str):
75 """Validate authentication segment."""
76 if not value:
77 return True
78 if (colon_count := value.count(":")) > 1:
79 # everything before @ is then considered as a username
80 # this is a bad practice, but syntactically valid URL
81 return _username_regex().match(unquote(value))
82 if colon_count < 1:
83 return _username_regex().match(value)
84 username, password = value.rsplit(":", 1)
85 return _username_regex().match(username) and all(
86 char_to_avoid not in password for char_to_avoid in ("/", "?", "#", "@")
87 )
90def _validate_netloc(
91 value: str,
92 skip_ipv6_addr: bool,
93 skip_ipv4_addr: bool,
94 may_have_port: bool,
95 simple_host: bool,
96 consider_tld: bool,
97 private: Optional[bool],
98 rfc_1034: bool,
99 rfc_2782: bool,
100):
101 """Validate netloc."""
102 if not value or value.count("@") > 1:
103 return False
104 if value.count("@") < 1:
105 return hostname(
106 (
107 value
108 if _confirm_ipv6_skip(value, skip_ipv6_addr) or "]:" in value
109 else value.lstrip("[").replace("]", "", 1)
110 ),
111 skip_ipv6_addr=_confirm_ipv6_skip(value, skip_ipv6_addr),
112 skip_ipv4_addr=skip_ipv4_addr,
113 may_have_port=may_have_port,
114 maybe_simple=simple_host,
115 consider_tld=consider_tld,
116 private=private,
117 rfc_1034=rfc_1034,
118 rfc_2782=rfc_2782,
119 )
120 basic_auth, host = value.rsplit("@", 1)
121 return hostname(
122 (
123 host
124 if _confirm_ipv6_skip(host, skip_ipv6_addr) or "]:" in value
125 else host.lstrip("[").replace("]", "", 1)
126 ),
127 skip_ipv6_addr=_confirm_ipv6_skip(host, skip_ipv6_addr),
128 skip_ipv4_addr=skip_ipv4_addr,
129 may_have_port=may_have_port,
130 maybe_simple=simple_host,
131 consider_tld=consider_tld,
132 private=private,
133 rfc_1034=rfc_1034,
134 rfc_2782=rfc_2782,
135 ) and _validate_auth_segment(basic_auth)
138def _validate_optionals(path: str, query: str, fragment: str, strict_query: bool):
139 """Validate path query and fragments."""
140 optional_segments = True
141 if path:
142 optional_segments &= bool(_path_regex().match(path))
143 try:
144 if (
145 query
146 # ref: https://github.com/python/cpython/issues/117109
147 and parse_qs(query, strict_parsing=strict_query, separator="&")
148 and parse_qs(query, strict_parsing=strict_query, separator=";")
149 ):
150 optional_segments &= True
151 except TypeError:
152 # for Python < v3.9.2 (official v3.10)
153 if query and parse_qs(query, strict_parsing=strict_query):
154 optional_segments &= True
155 if fragment:
156 # See RFC3986 Section 3.5 Fragment for allowed characters
157 # Adding "#", see https://github.com/python-validators/validators/issues/403
158 optional_segments &= bool(
159 re.fullmatch(r"[0-9a-z?/:@\-._~%!$&'()*+,;=#]*", fragment, re.IGNORECASE)
160 )
161 return optional_segments
164@validator
165def url(
166 value: str,
167 /,
168 *,
169 skip_ipv6_addr: bool = False,
170 skip_ipv4_addr: bool = False,
171 may_have_port: bool = True,
172 simple_host: bool = False,
173 strict_query: bool = True,
174 consider_tld: bool = False,
175 private: Optional[bool] = None, # only for ip-addresses
176 rfc_1034: bool = False,
177 rfc_2782: bool = False,
178 validate_scheme: Callable[[str], bool] = _validate_scheme,
179):
180 r"""Return whether or not given value is a valid URL.
182 This validator was originally inspired from [URL validator of dperini][1].
183 The following diagram is from [urlly][2]::
186 foo://admin:hunter1@example.com:8042/over/there?name=ferret#nose
187 \_/ \___/ \_____/ \_________/ \__/\_________/ \_________/ \__/
188 | | | | | | | |
189 scheme username password hostname port path query fragment
191 [1]: https://gist.github.com/dperini/729294
192 [2]: https://github.com/treeform/urlly
194 Examples:
195 >>> url('http://duck.com')
196 True
197 >>> url('ftp://foobar.dk')
198 True
199 >>> url('http://10.0.0.1')
200 True
201 >>> url('http://example.com/">user@example.com')
202 ValidationError(func=url, args={'value': 'http://example.com/">user@example.com'})
204 Args:
205 value:
206 URL string to validate.
207 skip_ipv6_addr:
208 When URL string cannot contain an IPv6 address.
209 skip_ipv4_addr:
210 When URL string cannot contain an IPv4 address.
211 may_have_port:
212 URL string may contain port number.
213 simple_host:
214 URL string maybe only hyphens and alpha-numerals.
215 strict_query:
216 Fail validation on query string parsing error.
217 consider_tld:
218 Restrict domain to TLDs allowed by IANA.
219 private:
220 Embedded IP address is public if `False`, private/local if `True`.
221 rfc_1034:
222 Allow trailing dot in domain/host name.
223 Ref: [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034).
224 rfc_2782:
225 Domain/Host name is of type service record.
226 Ref: [RFC 2782](https://www.rfc-editor.org/rfc/rfc2782).
227 validate_scheme:
228 Function that validates URL scheme.
230 Returns:
231 (Literal[True]): If `value` is a valid url.
232 (ValidationError): If `value` is an invalid url.
233 """
234 if not value or re.search(r"[\s\x00-\x1f\x7f]", value):
235 # url must not contain any white
236 # spaces, they must be encoded
237 return False
239 try:
240 scheme, netloc, path, query, fragment = urlsplit(value)
241 except ValueError:
242 return False
244 # RFC 8089: a "file" URL's authority is commonly empty or "localhost",
245 # neither of which is a hostname `_validate_netloc` would accept.
246 is_local_file = scheme == "file" and netloc in ("", "localhost")
248 return (
249 validate_scheme(scheme)
250 and (
251 is_local_file
252 or _validate_netloc(
253 netloc,
254 skip_ipv6_addr,
255 skip_ipv4_addr,
256 may_have_port,
257 simple_host,
258 consider_tld,
259 private,
260 rfc_1034,
261 rfc_2782,
262 )
263 )
264 and _validate_optionals(path, query, fragment, strict_query)
265 )