Repository: ckan/ckan

Issue:
URL validator does not support ":" for specifying ports
## CKAN version 2.10.1

## Describe the bug
The URL validator https://github.com/ckan/ckan/blob/2.10/ckan/logic/validators.py#L775 does not support specifying a port.
E.g. ``https://example.com/some-data`` is valid, but ``https://example.com:8080/some-data`` is **not** valid.

### Expected behavior
IMO, Port numbers should be allowed in a URL.

### Additional details
The `pieces.netloc`-part of the check could be adapted such that it splits it into `server_name` and `port` and checks those separately. I would be willing to write a PR if this is an acceptable approach.



Discussion:

Resolving patch:
diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py
--- a/ckan/logic/validators.py
+++ b/ckan/logic/validators.py
@@ -772,25 +772,37 @@ def tag_not_in_vocabulary(key: FlattenKey, tag_dict: FlattenDataDict,
         return
 
 
-def url_validator(key: FlattenKey, data: FlattenDataDict,
-                  errors: FlattenErrorDict, context: Context) -> Any:
-    ''' Checks that the provided value (if it is present) is a valid URL '''
-
+def url_validator(
+    key: FlattenKey,
+    data: FlattenDataDict,
+    errors: FlattenErrorDict,
+    context: Context,
+) -> Any:
+    """Checks that the provided value (if it is present) is a valid URL"""
     url = data.get(key, None)
     if not url:
         return
 
     try:
         pieces = urlparse(url)
-        if all([pieces.scheme, pieces.netloc]) and \
-           set(pieces.netloc) <= set(string.ascii_letters + string.digits + '-.') and \
-           pieces.scheme in ['http', 'https']:
-            return
+        if all([pieces.scheme, pieces.netloc]) and pieces.scheme in [
+            "http",
+            "https",
+        ]:
+            hostname, port = (
+                pieces.netloc.split(":")
+                if ":" in pieces.netloc
+                else (pieces.netloc, None)
+            )
+            if set(hostname) <= set(
+                string.ascii_letters + string.digits + "-."
+            ) and (port is None or port.isdigit()):
+                return
     except ValueError:
         # url is invalid
         pass
 
-    errors[key].append(_('Please provide a valid URL'))
+    errors[key].append(_("Please provide a valid URL"))
 
 
 def user_name_exists(user_name: str, context: Context) -> Any:


Test patch:
diff --git a/ckan/tests/logic/test_validators.py b/ckan/tests/logic/test_validators.py
--- a/ckan/tests/logic/test_validators.py
+++ b/ckan/tests/logic/test_validators.py
@@ -899,3 +899,41 @@ def convert(tag_string):
     assert convert("") == []
     assert convert("trailing comma,") == ["trailing comma"]
     assert convert("trailing comma space, ") == ["trailing comma space"]
+
+
+def test_url_validator():
+    key = ("url",)
+    errors = {(key): []}
+
+    # Test with a valid URL without a port
+    url = {("url",): "https://example.com"}
+    validators.url_validator(key, url, errors, {})
+    assert errors == {('url',): []}
+
+    # Test with a valid URL with a port
+    url = {("url",): "https://example.com:8080"}
+    validators.url_validator(key, url, errors, {})
+    assert errors == {('url',): []}
+
+    # Test with an invalid URL (invalid characters in hostname)
+    url = {("url",): "https://exa$mple.com"}
+    validators.url_validator(key, url, errors, {})
+    assert errors[key] == ['Please provide a valid URL']
+    errors[key].clear()
+
+    # Test with an invalid URL (invalid port)
+    url = {("url",): "https://example.com:80a80"}
+    validators.url_validator(key, url, errors, {})
+    assert errors[key] == ['Please provide a valid URL']
+    errors[key].clear()
+
+    # Test with an invalid URL (no scheme)
+    url = {("url",): "example.com"}
+    validators.url_validator(key, url, errors, {})
+    assert errors[key] == ['Please provide a valid URL']
+    errors[key].clear()
+
+    # Test with an invalid URL (unsupported scheme)
+    url = {("url",): "ftp://example.com"}
+    validators.url_validator(key, url, errors, {})
+    assert errors[key] == ['Please provide a valid URL']