Repository: wtforms/wtforms
Pull request #524 (2019-11-05 16:33:42)

URLs with query params should pass URL validator
Fixes https://github.com/wtforms/wtforms/issues/523

Previously URL validator didn't accept URLs with query parameters if there wasn't also a path specified. This was, because in that case the old regex would include query-params to the `host`, and attempt to call `HostnameValidation` with the incorrect `host`.

Old regex:
```python
regex = re.compile(r"^[a-z]+://(?P<host>[^/:]+)(?P<port>:[0-9]+)?(?P<path>\/.*)?$")
matches = regex.match('https://foo.io?no=path')
print(matches.group('host'))
# -> foo.io?no=path
```

New regex:
```python
regex = (
    r"^[a-z]+://"
    r"(?P<host>[^\/\?:]+)"
    r"(?P<port>:[0-9]+)?"
    r"(?P<path>\/.*?)?"
    r"(?P<query>\?.*)?$"
)
matches = regex.match('https://foo.io?no=path')
print(matches.group('host'))
# -> foo.io
print(matches.group('query'))
# -> ?no=path
```

Added a bunch of tests to verify this behavior.


whb07: looks good in general just missing the tweaks for the typo and an extra test case for the tld test set

Patch:
diff --git a/src/wtforms/validators.py b/src/wtforms/validators.py
index 58d12b6..b77c14b 100644
--- a/src/wtforms/validators.py
+++ b/src/wtforms/validators.py
@@ -495,7 +495,13 @@ class URL(Regexp):
     """
 
     def __init__(self, require_tld=True, message=None):
-        regex = r"^[a-z]+://(?P<host>[^/:]+)(?P<port>:[0-9]+)?(?P<path>\/.*)?$"
+        regex = (
+            r"^[a-z]+://"
+            r"(?P<host>[^\/\?:]+)"
+            r"(?P<port>:[0-9]+)?"
+            r"(?P<path>\/.*?)?"
+            r"(?P<query>\?.*)?$"
+        )
         super(URL, self).__init__(regex, re.IGNORECASE, message)
         self.validate_hostname = HostnameValidation(
             require_tld=require_tld, allow_ip=True


Test patch:
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 8ef8324..b7e9a4e 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -382,6 +382,9 @@ def test_equal_to_raises(
         u"http://foobar.dk/",
         u"http://foo-bar.dk/",
         u"http://foo_bar.dk/",
+        u"http://foobar.dk?query=param",
+        u"http://foobar.dk/path?query=param",
+        u"http://foobar.dk/path?query=param&foo=faa",
         u"http://foobar.museum/foobar",
         u"http://192.168.0.1/foobar",
         u"http://192.168.0.1:9000/fake",
@@ -400,10 +403,20 @@ def test_valid_url_passes(url_val, dummy_form, dummy_field):
     validator(dummy_form, dummy_field)
 
 
-@pytest.mark.parametrize("url_val", [u"http://localhost/foobar", u"http://foobar"])
+@pytest.mark.parametrize(
+    "url_val",
+    [
+        u"http://localhost/foobar",
+        u"http://foobar",
+        u"http://foobar?query=param&foo=faa",
+        u"http://foobar:5000?query=param&foo=faa",
+        u"http://foobar/path?query=param&foo=faa",
+        u"http://foobar:1234/path?query=param&foo=faa",
+    ],
+)
 def test_valid_url_notld_passes(url_val, dummy_form, dummy_field):
     """
-    Require TLD option se to false, correct URL should pass without raising
+    Require TLD option set to false, correct URL should pass without raising
     """
     validator = url(require_tld=False)
     dummy_field.data = url_val