diff --git a/properdocs/config/config_options.py b/properdocs/config/config_options.py index d16faea7..7a9a82a6 100644 --- a/properdocs/config/config_options.py +++ b/properdocs/config/config_options.py @@ -17,7 +17,6 @@ from urllib.parse import urlsplit, urlunsplit import markdown -import pathspec import pathspec.gitignore from properdocs import plugins, theme, utils @@ -497,8 +496,8 @@ def run_validation(self, value: object) -> str: return value try: parsed_url = urlsplit(value) - except (AttributeError, TypeError): - raise ValidationError("Unable to parse the URL.") + except Exception: + raise ValidationError("The URL is invalid") if parsed_url.scheme and parsed_url.netloc: if self.is_dir and not parsed_url.path.endswith('/'): diff --git a/properdocs/structure/nav.py b/properdocs/structure/nav.py index d6bd8d1b..c885e646 100644 --- a/properdocs/structure/nav.py +++ b/properdocs/structure/nav.py @@ -164,9 +164,16 @@ def get_navigation(files: Files, config: ProperDocsConfig) -> Navigation: links = _get_by_type(items, Link) for link in links: - scheme, netloc, path, query, fragment = urlsplit(link.url) - if scheme or netloc: - log.debug(f"An external link to '{link.url}' is included in the 'nav' configuration.") + invalid: str | None = None + try: + scheme, netloc, path, query, fragment = urlsplit(link.url) + except Exception as e: + invalid = 'invalid' + else: + if scheme or netloc: + invalid = 'external' + if invalid: + log.debug(f"An {invalid} link to '{link.url}' is included in the 'nav' configuration.") elif ( link.url.startswith('/') and config.validation.nav.absolute_links diff --git a/properdocs/structure/pages.py b/properdocs/structure/pages.py index 8e141a0c..ca15282e 100644 --- a/properdocs/structure/pages.py +++ b/properdocs/structure/pages.py @@ -417,7 +417,15 @@ def _possible_target_uris( tried.add(guess) def path_to_url(self, url: str) -> str: - scheme, netloc, path, query, anchor = urlsplit(url) + try: + scheme, netloc, path, query, anchor = urlsplit(url) + except ValueError: # Invalid URL, e.g. invalid IPv6. + log.log( + self.config.validation.links.unrecognized_links, + f"Doc file '{self.file.src_uri}' contains an invalid link '{url}', " + f"it was left as is.", + ) + return url absolute_link = None warning_level, warning = 0, '' diff --git a/properdocs/tests/config/config_options_tests.py b/properdocs/tests/config/config_options_tests.py index be13eb1a..258d3c0d 100644 --- a/properdocs/tests/config/config_options_tests.py +++ b/properdocs/tests/config/config_options_tests.py @@ -424,6 +424,7 @@ class Schema(Config): conf = self.get_config(Schema, {'option': None}) self.assertEqual(conf.option, None) + @unittest.skipUnless(sys.version_info >= (3, 11), "new error kind in Python 3.11") def test_invalid_url(self) -> None: class Schema(Config): option = c.URL() @@ -431,7 +432,16 @@ class Schema(Config): with self.expect_error(option="Required configuration not provided."): self.get_config(Schema, {'option': None}) - for url in "properdocs.org", "//properdocs.org/test", "http:/properdocs.org/", "/hello/": + for url in ["http://[a]/"]: + with self.subTest(url=url): + with self.expect_error(option="The URL is invalid"): + self.get_config(Schema, {'option': url}) + + def test_invalid_url_no_http(self) -> None: + class Schema(Config): + option = c.URL() + + for url in ["properdocs.org", "//properdocs.org/test", "http:/properdocs.org/", "/hello/"]: with self.subTest(url=url): with self.expect_error( option="The URL isn't valid, it should include the http:// (scheme)" diff --git a/properdocs/tests/structure/page_tests.py b/properdocs/tests/structure/page_tests.py index 4d6810e9..851ad0b0 100644 --- a/properdocs/tests/structure/page_tests.py +++ b/properdocs/tests/structure/page_tests.py @@ -1221,6 +1221,19 @@ def test_absolute_win_local_path(self): 'absolute local path', ) + @unittest.skipUnless(sys.version_info >= (3, 11), "new error kind in Python 3.11") + def test_invalid_link_ipv6(self): + for use_directory_urls in True, False: + self.assertEqual( + self.get_rendered_result( + use_directory_urls=use_directory_urls, + content='[invalid link](http://[a]/)', + files=['index.md'], + logs="INFO:Doc file 'index.md' contains an invalid link 'http://[a]/', it was left as is.", + ), + 'invalid link', + ) + def test_email_link(self): self.assertEqual( self.get_rendered_result(content='', files=['index.md']), diff --git a/properdocs/utils/__init__.py b/properdocs/utils/__init__.py index 6bf97ffb..2cc6813c 100644 --- a/properdocs/utils/__init__.py +++ b/properdocs/utils/__init__.py @@ -221,8 +221,11 @@ def _get_norm_url(path: str) -> tuple[str, int]: f"That will be unsupported in a future release. Please change it to '/'." ) path = path.replace('\\', '/') + try: + parsed = urlsplit(path) + except ValueError: + return path, -1 # Allow links to be fully qualified URLs - parsed = urlsplit(path) if parsed.scheme or parsed.netloc or path.startswith(('/', '#')): return path, -1