Skip to content

Commit b7c36ac

Browse files
committed
URL validator: accept user/password, use urllib.urlparse
I was hitting an issue in a build tool that was not letting me specify a URL to clone a git tree with a personal access token (e.g. [1]) in a wtform URL field. I started looking at expanding the original regex, but there are tricks like multiple "@"'s in passwords that are hard to get right. I think that for this purpose, urllib.urlparse (urlparse/urlsplit doesn't seem to matter here) will just "do the right thing". The test-cases are expanded with some coverage of username/passwords. [1] https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#clone-repository-using-personal-access-token
1 parent 8211fc8 commit b7c36ac

File tree

3 files changed

+29
-12
lines changed

3 files changed

+29
-12
lines changed

CHANGES.rst

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
.. currentmodule:: wtforms
22

3+
Version 3.X.X
4+
-------------
5+
6+
Unreleased
7+
8+
- :class:`~validators.URL` validator is based on `urllib.urlparse` and
9+
allows `username:password@` values.
10+
311
Version 3.1.2
412
-------------
513

src/wtforms/validators.py

+16-12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import math
33
import re
44
import uuid
5+
from urllib.parse import urlparse
56

67
__all__ = (
78
"DataRequired",
@@ -505,9 +506,9 @@ def __call__(self, form, field):
505506
super().__call__(form, field, message)
506507

507508

508-
class URL(Regexp):
509+
class URL:
509510
"""
510-
Simple regexp based url validation. Much like the email validator, you
511+
Simple url validation. Much like the email validator, you
511512
probably want to validate the url later by other means if the url must
512513
resolve.
513514
@@ -522,14 +523,7 @@ class URL(Regexp):
522523
"""
523524

524525
def __init__(self, require_tld=True, allow_ip=True, message=None):
525-
regex = (
526-
r"^[a-z]+://"
527-
r"(?P<host>[^\/\?:]+)"
528-
r"(?P<port>:[0-9]+)?"
529-
r"(?P<path>\/.*?)?"
530-
r"(?P<query>\?.*)?$"
531-
)
532-
super().__init__(regex, re.IGNORECASE, message)
526+
self.message = message
533527
self.validate_hostname = HostnameValidation(
534528
require_tld=require_tld, allow_ip=allow_ip
535529
)
@@ -539,8 +533,18 @@ def __call__(self, form, field):
539533
if message is None:
540534
message = field.gettext("Invalid URL.")
541535

542-
match = super().__call__(form, field, message)
543-
if not self.validate_hostname(match.group("host")):
536+
try:
537+
r = urlparse(field.data)
538+
except ValueError as exc:
539+
raise ValidationError(message) from exc
540+
541+
if not r.scheme:
542+
raise ValidationError(message)
543+
544+
if not r.hostname:
545+
raise ValidationError(message)
546+
547+
if not self.validate_hostname(r.hostname):
544548
raise ValidationError(message)
545549

546550

tests/validators/test_url.py

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
"\u0625\u062e\u062a\u0628\u0627\u0631/foo.com", # Arabic
2323
"http://उदाहरण.परीक्षा/", # Hindi
2424
"http://실례.테스트", # Hangul
25+
"http://username:[email protected]",
26+
27+
"http://usern@me:p@[email protected]",
28+
"http://username:[email protected]:1234/path?query=parm",
2529
],
2630
)
2731
def test_valid_url_passes(url_val, dummy_form, dummy_field):
@@ -42,6 +46,7 @@ def test_valid_url_passes(url_val, dummy_form, dummy_field):
4246
"http://foobar:5000?query=param&foo=faa",
4347
"http://foobar/path?query=param&foo=faa",
4448
"http://foobar:1234/path?query=param&foo=faa",
49+
"http://user:password@foobar:1234/path?query=param&foo=faa",
4550
],
4651
)
4752
def test_valid_url_notld_passes(url_val, dummy_form, dummy_field):

0 commit comments

Comments
 (0)