Skip to content

Commit 87339ec

Browse files
authored
DOP-3064: Validate relative URLs in card directives (#408)
* some initial work to add validation in the correct code path * pull path logic into util function * simple test for relative url util * make linter happy * leverage existing function, add parser-level testing * use existing add_doc_target_ext function * enforce invariant for structure of relative paths * make linter happy * remove unnecessary string cast * add test for malformed relative path * compile regex at global scope to avoid repeated compilation/unnecessary reliance on cache
1 parent fee89c6 commit 87339ec

File tree

4 files changed

+77
-1
lines changed

4 files changed

+77
-1
lines changed

snooty/diagnostics.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,19 @@ def did_you_mean(self) -> List[str]:
506506
return [f"`{self.parts[0]} <{self.parts[1]}>`__"]
507507

508508

509+
class MalformedRelativePath(Diagnostic):
510+
severity = Diagnostic.Level.error
511+
512+
def __init__(
513+
self,
514+
relative_path: str,
515+
start: Union[int, Tuple[int, int]],
516+
end: Union[None, int, Tuple[int, int]] = None,
517+
) -> None:
518+
super().__init__(f"Malformed relative path {relative_path}", start, end)
519+
self.relative_path = relative_path
520+
521+
509522
class MissingTab(Diagnostic):
510523
severity = Diagnostic.Level.error
511524

snooty/parser.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
InvalidTableStructure,
5656
InvalidURL,
5757
MalformedGlossary,
58+
MalformedRelativePath,
5859
MissingChild,
5960
RemovedLiteralBlockSyntax,
6061
TabMustBeDirective,
@@ -73,6 +74,7 @@
7374
from .util import RST_EXTENSIONS
7475

7576
NO_CHILDREN = (n.SubstitutionReference,)
77+
MULTIPLE_FORWARD_SLASHES = re.compile(r"([\/])\1")
7678
logger = logging.getLogger(__name__)
7779

7880

@@ -923,9 +925,12 @@ def _locate_text(text: str) -> int:
923925

924926
elif key in {"mongodb:card"}:
925927
image_argument = options.get("icon")
928+
url_argument = options.get("url")
926929

927930
if image_argument:
928931
self.validate_and_add_asset(doc, image_argument, line)
932+
if url_argument and not url_argument.startswith("http"):
933+
self.validate_relative_url(url_argument, line)
929934

930935
return doc
931936

@@ -940,6 +945,21 @@ def validate_and_add_asset(
940945
CannotOpenFile(Path(image_argument), err.strerror, line)
941946
)
942947

948+
def validate_relative_url(self, url_argument: str, line: int) -> None:
949+
"""Validate relative URL points to page within current docs site.
950+
URLs can be of the form /foo, foo, /foo/"""
951+
target_path = util.add_doc_target_ext(
952+
url_argument, self.docpath, self.project_config.source_path
953+
)
954+
955+
if not target_path.is_file():
956+
err_message = (
957+
f"{os.strerror(errno.ENOENT)} for relative path {url_argument}"
958+
)
959+
self.diagnostics.append(CannotOpenFile(target_path, err_message, line))
960+
elif MULTIPLE_FORWARD_SLASHES.search(url_argument) is not None:
961+
self.diagnostics.append(MalformedRelativePath(url_argument, line))
962+
943963
def validate_doc_role(self, node: docutils.nodes.Node) -> None:
944964
"""Validate target for doc role"""
945965
resolved_target_path = util.add_doc_target_ext(

snooty/test_parser.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
InvalidURL,
1616
MakeCorrectionMixin,
1717
MalformedGlossary,
18+
MalformedRelativePath,
1819
TabMustBeDirective,
1920
UnexpectedIndentation,
2021
UnknownTabID,
@@ -119,6 +120,48 @@ def test_chapter() -> None:
119120
assert isinstance(diagnostics[0], CannotOpenFile)
120121

121122

123+
def test_card() -> None:
124+
"""Test card directive"""
125+
path = ROOT_PATH.joinpath(Path("test.rst"))
126+
project_config = ProjectConfig(ROOT_PATH, "", source="./")
127+
parser = rstparser.Parser(project_config, JSONVisitor)
128+
129+
SOURCE_DIR = "/test_project/source/"
130+
VALID_URLS = ["index", "index/"]
131+
INVALID_URL = [("foo", CannotOpenFile), ("index//", MalformedRelativePath)]
132+
CARD_CONTENT = """
133+
.. card-group::
134+
:columns: 3
135+
:layout: carousel
136+
137+
.. card::
138+
:headline: Develop Applications
139+
:cta: Test Develop Applications Relative URL Test
140+
:url: %s
141+
142+
This card was built with a relative URL.
143+
"""
144+
145+
for url in VALID_URLS:
146+
page, diagnostics = parse_rst(
147+
parser,
148+
path,
149+
CARD_CONTENT % (SOURCE_DIR + url),
150+
)
151+
152+
page.finish(diagnostics)
153+
assert len(diagnostics) == 0
154+
155+
for url, error_type in INVALID_URL:
156+
page, diagnostics = parse_rst(
157+
parser,
158+
path,
159+
CARD_CONTENT % (SOURCE_DIR + url),
160+
)
161+
assert len(diagnostics) == 1
162+
assert isinstance(diagnostics[0], error_type)
163+
164+
122165
def test_tabs() -> None:
123166
tabs_path = ROOT_PATH.joinpath(Path("test_tabs.rst"))
124167
project_config = ProjectConfig(ROOT_PATH, "", source="./")

snooty/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def add_doc_target_ext(target: str, docpath: PurePath, project_root: Path) -> Pa
169169
new_suffix = target_path.suffix + ".txt"
170170
target_path = target_path.with_suffix(new_suffix)
171171

172-
fileid, resolved_target_path = reroot_path(target_path, docpath, project_root)
172+
_, resolved_target_path = reroot_path(target_path, docpath, project_root)
173173
return resolved_target_path
174174

175175

0 commit comments

Comments
 (0)