Skip to content

Commit ee493a6

Browse files
committed
Fail to upload package when description rendering is invalid.
This branch prevents a package upload if the rendered content for the description_content_type is invalid. If no description_content_type is set, 'text/x-rst' is assumed as the type. Fixes pypi#3966
1 parent 8ec505b commit ee493a6

File tree

3 files changed

+81
-3
lines changed

3 files changed

+81
-3
lines changed

tests/unit/forklift/test_legacy.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,62 @@ def test_fails_with_invalid_names(self, pyramid_config, db_request, name):
10031003
"See /the/help/url/ "
10041004
"for more information.").format(name)
10051005

1006+
@pytest.mark.parametrize(
1007+
("description_content_type", "description", "message"), [
1008+
(
1009+
"text/x-rst",
1010+
".. invalid-directive::",
1011+
"400 The description failed to render for 'text/x-rst'. "
1012+
"See /the/help/url/ for more information."
1013+
),
1014+
(
1015+
"",
1016+
".. invalid-directive::",
1017+
"400 The description failed to render for 'text/x-rst'. "
1018+
"See /the/help/url/ for more information."
1019+
),
1020+
],
1021+
)
1022+
def test_fails_invalid_render(self, pyramid_config, db_request,
1023+
description_content_type, description,
1024+
message):
1025+
pyramid_config.testing_securitypolicy(userid=1)
1026+
user = UserFactory.create()
1027+
EmailFactory.create(user=user)
1028+
db_request.user = user
1029+
db_request.remote_addr = "10.10.10.30"
1030+
1031+
db_request.POST = MultiDict({
1032+
"metadata_version": "1.2",
1033+
"name": "example",
1034+
"version": "1.0",
1035+
"filetype": "sdist",
1036+
"md5_digest": "a fake md5 digest",
1037+
"content": pretend.stub(
1038+
filename="example-1.0.tar.gz",
1039+
file=io.BytesIO(b"A fake file."),
1040+
type="application/tar",
1041+
),
1042+
"description_content_type": description_content_type,
1043+
"description": description,
1044+
})
1045+
1046+
db_request.help_url = pretend.call_recorder(
1047+
lambda **kw: "/the/help/url/"
1048+
)
1049+
1050+
with pytest.raises(HTTPBadRequest) as excinfo:
1051+
legacy.file_upload(db_request)
1052+
1053+
resp = excinfo.value
1054+
1055+
assert db_request.help_url.calls == [
1056+
pretend.call(_anchor='project-name')
1057+
]
1058+
1059+
assert resp.status_code == 400
1060+
assert resp.status == message
1061+
10061062
@pytest.mark.parametrize("name", ["xml", "XML", "pickle", "PiCKle",
10071063
"main", "future", "al", "uU", "test",
10081064
"encodings.utf_8_sig",
@@ -1215,6 +1271,7 @@ def test_successful_upload(self, tmpdir, monkeypatch, pyramid_config,
12151271
"filetype": "sdist",
12161272
"pyversion": "source",
12171273
"content": content,
1274+
"description": "an example description",
12181275
})
12191276
db_request.POST.extend([
12201277
("classifiers", "Environment :: Other Environment"),

warehouse/forklift/legacy.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
Project, Release, Dependency, DependencyKind, Role, File, Filename,
4646
JournalEntry, BlacklistedProject,
4747
)
48-
from warehouse.utils import http
48+
from warehouse.utils import http, readme
4949

5050

5151
MAX_FILESIZE = 60 * 1024 * 1024 # 60M
@@ -940,6 +940,26 @@ def file_upload(request):
940940
)
941941
)
942942

943+
# Uploading should prevent broken rendered descriptions.
944+
if form.description.data:
945+
description_content_type = form.description_content_type.data
946+
if not description_content_type:
947+
description_content_type = 'text/x-rst'
948+
rendered = readme.render(
949+
form.description.data, description_content_type,
950+
use_fallback=False)
951+
if rendered is None:
952+
raise _exc_with_message(
953+
HTTPBadRequest,
954+
("The description failed to render "
955+
"for '{description_content_type}'. "
956+
"See {projecthelp} "
957+
"for more information.").format(
958+
description_content_type=description_content_type,
959+
projecthelp=request.help_url(_anchor='project-name'),
960+
),
961+
) from None
962+
943963
try:
944964
canonical_version = packaging.utils.canonicalize_version(
945965
form.version.data

warehouse/utils/readme.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
}
3030

3131

32-
def render(value, content_type=None):
32+
def render(value, content_type=None, use_fallback=True):
3333
if value is None:
3434
return value
3535

@@ -45,7 +45,8 @@ def render(value, content_type=None):
4545
# If the content was not rendered, we'll render as plaintext instead. The
4646
# reason it's necessary to do this instead of just accepting plaintext is
4747
# that readme_renderer will deal with sanitizing the content.
48-
if rendered is None:
48+
# Skip the fallback option when validating that rendered output is ok.
49+
if use_fallback and rendered is None:
4950
rendered = readme_renderer.txt.render(value)
5051

5152
return rendered

0 commit comments

Comments
 (0)