Skip to content

Commit ca32b13

Browse files
committed
Catch ValueError when parsing URLs, show a better error/warning
1 parent 252e8dc commit ca32b13

File tree

6 files changed

+49
-9
lines changed

6 files changed

+49
-9
lines changed

properdocs/config/config_options.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from urllib.parse import urlsplit, urlunsplit
1818

1919
import markdown
20-
import pathspec
2120
import pathspec.gitignore
2221

2322
from properdocs import plugins, theme, utils
@@ -497,8 +496,8 @@ def run_validation(self, value: object) -> str:
497496
return value
498497
try:
499498
parsed_url = urlsplit(value)
500-
except (AttributeError, TypeError):
501-
raise ValidationError("Unable to parse the URL.")
499+
except Exception:
500+
raise ValidationError("The URL is invalid")
502501

503502
if parsed_url.scheme and parsed_url.netloc:
504503
if self.is_dir and not parsed_url.path.endswith('/'):

properdocs/structure/nav.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,16 @@ def get_navigation(files: Files, config: ProperDocsConfig) -> Navigation:
164164

165165
links = _get_by_type(items, Link)
166166
for link in links:
167-
scheme, netloc, path, query, fragment = urlsplit(link.url)
168-
if scheme or netloc:
169-
log.debug(f"An external link to '{link.url}' is included in the 'nav' configuration.")
167+
invalid: str | None = None
168+
try:
169+
scheme, netloc, path, query, fragment = urlsplit(link.url)
170+
except Exception as e:
171+
invalid = 'invalid'
172+
else:
173+
if scheme or netloc:
174+
invalid = 'external'
175+
if invalid:
176+
log.debug(f"An {invalid} link to '{link.url}' is included in the 'nav' configuration.")
170177
elif (
171178
link.url.startswith('/')
172179
and config.validation.nav.absolute_links

properdocs/structure/pages.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,15 @@ def _possible_target_uris(
417417
tried.add(guess)
418418

419419
def path_to_url(self, url: str) -> str:
420-
scheme, netloc, path, query, anchor = urlsplit(url)
420+
try:
421+
scheme, netloc, path, query, anchor = urlsplit(url)
422+
except ValueError: # Invalid URL, e.g. invalid IPv6.
423+
log.log(
424+
self.config.validation.links.unrecognized_links,
425+
f"Doc file '{self.file.src_uri}' contains an invalid link '{url}', "
426+
f"it was left as is.",
427+
)
428+
return url
421429

422430
absolute_link = None
423431
warning_level, warning = 0, ''

properdocs/tests/config/config_options_tests.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,14 +424,24 @@ class Schema(Config):
424424
conf = self.get_config(Schema, {'option': None})
425425
self.assertEqual(conf.option, None)
426426

427+
@unittest.skipUnless(sys.version_info >= (3, 11), "new error kind in Python 3.11")
427428
def test_invalid_url(self) -> None:
428429
class Schema(Config):
429430
option = c.URL()
430431

431432
with self.expect_error(option="Required configuration not provided."):
432433
self.get_config(Schema, {'option': None})
433434

434-
for url in "properdocs.org", "//properdocs.org/test", "http:/properdocs.org/", "/hello/":
435+
for url in ["http://[a]/"]:
436+
with self.subTest(url=url):
437+
with self.expect_error(option="The URL is invalid"):
438+
self.get_config(Schema, {'option': url})
439+
440+
def test_invalid_url_no_http(self) -> None:
441+
class Schema(Config):
442+
option = c.URL()
443+
444+
for url in ["properdocs.org", "//properdocs.org/test", "http:/properdocs.org/", "/hello/"]:
435445
with self.subTest(url=url):
436446
with self.expect_error(
437447
option="The URL isn't valid, it should include the http:// (scheme)"

properdocs/tests/structure/page_tests.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,19 @@ def test_absolute_win_local_path(self):
12211221
'<a href="\\image.png">absolute local path</a>',
12221222
)
12231223

1224+
@unittest.skipUnless(sys.version_info >= (3, 11), "new error kind in Python 3.11")
1225+
def test_invalid_link_ipv6(self):
1226+
for use_directory_urls in True, False:
1227+
self.assertEqual(
1228+
self.get_rendered_result(
1229+
use_directory_urls=use_directory_urls,
1230+
content='[invalid link](http://[a]/)',
1231+
files=['index.md'],
1232+
logs="INFO:Doc file 'index.md' contains an invalid link 'http://[a]/', it was left as is.",
1233+
),
1234+
'<a href="http://[a]">absolute local path</a>',
1235+
)
1236+
12241237
def test_email_link(self):
12251238
self.assertEqual(
12261239
self.get_rendered_result(content='<mail@example.com>', files=['index.md']),

properdocs/utils/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,11 @@ def _get_norm_url(path: str) -> tuple[str, int]:
221221
f"That will be unsupported in a future release. Please change it to '/'."
222222
)
223223
path = path.replace('\\', '/')
224+
try:
225+
parsed = urlsplit(path)
226+
except ValueError:
227+
return path, -1
224228
# Allow links to be fully qualified URLs
225-
parsed = urlsplit(path)
226229
if parsed.scheme or parsed.netloc or path.startswith(('/', '#')):
227230
return path, -1
228231

0 commit comments

Comments
 (0)