--- /dev/null
+---
+fixes:
+ - |
+ Message IDs containing slashes will now have these slashes percent-encoded.
+ Previously, attempts to access submissions whose Message IDs contained
+ slashes would result in a HTTP 404 on some Django versions. If you wish to
+ access such a submission, you must now percent-encode the slashes first.
+ For example, to access a patch, cover letter or comment with the following
+ message ID:
+
+ bug-28101-10460-NydYNmfPGz@http.sourceware.org/bugzilla/
+
+ You should now use:
+
+ bug-28101-10460-NydYNmfPGz@http.sourceware.org%2Dbugzilla%2D
+
+ Both the web UI and REST API have been updated to generate URLs in this
+ format so this should only be noticable to users manually generating such
+ URLs.
@property
def url_msgid(self):
- """A trimmed messageid, suitable for inclusion in URLs"""
+ """A trimmed Message ID, suitable for inclusion in URLs"""
if settings.DEBUG:
assert self.msgid[0] == '<' and self.msgid[-1] == '>'
return self.msgid.strip('<>')
+ @property
+ def encoded_msgid(self):
+ """Like 'url_msgid' but with slashes percentage encoded."""
+ # We don't want to encode all characters (i.e. use urllib.parse.quote)
+ # because that would result in us encoding the '@' present in all
+ # message IDs. Instead we only percent-encode any slashes present [1].
+ # These are not common so this is very much expected to be an edge
+ # case.
+ #
+ # [1] https://datatracker.ietf.org/doc/html/rfc3986.html#section-2
+ return self.url_msgid.replace('/', '%2F')
+
def save(self, *args, **kwargs):
# Modifying a submission via admin interface changes '\n' newlines in
# message content to '\r\n'. We need to fix them to avoid problems,
'cover-detail',
kwargs={
'project_id': self.project.linkname,
- 'msgid': self.url_msgid,
+ 'msgid': self.encoded_msgid,
},
)
'cover-mbox',
kwargs={
'project_id': self.project.linkname,
- 'msgid': self.url_msgid,
+ 'msgid': self.encoded_msgid,
},
)
'patch-detail',
kwargs={
'project_id': self.project.linkname,
- 'msgid': self.url_msgid,
+ 'msgid': self.encoded_msgid,
},
)
'patch-mbox',
kwargs={
'project_id': self.project.linkname,
- 'msgid': self.url_msgid,
+ 'msgid': self.encoded_msgid,
},
)
class PatchComment(EmailMixin, models.Model):
- # parent
patch = models.ForeignKey(
Patch,
{{ submission.id }}
</button>
{% if submission.diff %}
- <a href="{% url 'patch-raw' project_id=project.linkname msgid=submission.url_msgid %}"
+ <a href="{% url 'patch-raw' project_id=project.linkname msgid=submission.encoded_msgid %}"
class="btn btn-default" role="button" title="Download patch diff">
diff
</a>
- <a href="{% url 'patch-mbox' project_id=project.linkname msgid=submission.url_msgid %}"
+ <a href="{% url 'patch-mbox' project_id=project.linkname msgid=submission.encoded_msgid %}"
class="btn btn-default" role="button" title="Download patch mbox">
mbox
</a>
{% else %}
- <a href="{% url 'cover-mbox' project_id=project.linkname msgid=submission.url_msgid %}"
+ <a href="{% url 'cover-mbox' project_id=project.linkname msgid=submission.encoded_msgid %}"
class="btn btn-default" role="button" title="Download cover mbox">
mbox
</a>
</td>
{% endif %}
<td>
- <a href="{% url 'patch-detail' project_id=project.linkname msgid=patch.url_msgid %}">
+ <a href="{% url 'patch-detail' project_id=project.linkname msgid=patch.encoded_msgid %}">
{{ patch.name|default:"[no subject]"|truncatechars:100 }}
</a>
</td>
{% if cover == submission %}
{{ cover.name|default:"[no subject]"|truncatechars:100 }}
{% else %}
- <a href="{% url 'cover-detail' project_id=project.linkname msgid=cover.url_msgid %}">
+ <a href="{% url 'cover-detail' project_id=project.linkname msgid=cover.encoded_msgid %}">
{{ cover.name|default:"[no subject]"|truncatechars:100 }}
</a>
{% endif %}
{% if sibling == submission %}
{{ sibling.name|default:"[no subject]"|truncatechars:100 }}
{% else %}
- <a href="{% url 'patch-detail' project_id=project.linkname msgid=sibling.url_msgid %}">
+ <a href="{% url 'patch-detail' project_id=project.linkname msgid=sibling.encoded_msgid %}">
{{ sibling.name|default:"[no subject]"|truncatechars:100 }}
</a>
{% endif %}
{% for sibling in related_same_project %}
<li>
{% if sibling.id != submission.id %}
- <a href="{% url 'patch-detail' project_id=project.linkname msgid=sibling.url_msgid %}">
+ <a href="{% url 'patch-detail' project_id=project.linkname msgid=sibling.encoded_msgid %}">
{{ sibling.name|default:"[no subject]"|truncatechars:100 }}
</a>
{% endif %}
<div id="related-outside" class="submission-list" style="display:none;">
{% for sibling in related_outside %}
<li>
- <a href="{% url 'patch-detail' project_id=sibling.project.linkname msgid=sibling.url_msgid %}">
+ <a href="{% url 'patch-detail' project_id=sibling.project.linkname msgid=sibling.encoded_msgid %}">
{{ sibling.name|default:"[no subject]"|truncatechars:100 }}
</a> (in {{ sibling.project }})
</li>
"""Filter covers by msgid."""
cover = create_cover()
- resp = self.client.get(self.api_url(), {'msgid': cover.url_msgid})
+ resp = self.client.get(self.api_url(), {'msgid': cover.encoded_msgid})
self.assertEqual([cover.id], [x['id'] for x in resp.data])
# empty response if nothing matches
"""Filter patches by msgid."""
patch = self._create_patch()
- resp = self.client.get(self.api_url(), {'msgid': patch.url_msgid})
+ resp = self.client.get(self.api_url(), {'msgid': patch.encoded_msgid})
self.assertEqual([patch.id], [x['id'] for x in resp.data])
# empty response if nothing matches
'patch-detail',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
),
params,
'patch-detail',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
),
params,
'patch-detail',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
),
params,
'patch-detail',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
),
params,
'patch-detail',
kwargs={
'project_id': cover.project.linkname,
- 'msgid': cover.url_msgid,
+ 'msgid': cover.encoded_msgid,
},
)
redirect_url = reverse(
'cover-detail',
kwargs={
'project_id': cover.project.linkname,
- 'msgid': cover.url_msgid,
+ 'msgid': cover.encoded_msgid,
},
)
'cover-detail',
kwargs={
'project_id': cover.project.linkname,
- 'msgid': cover.url_msgid,
+ 'msgid': cover.encoded_msgid,
},
)
'cover-mbox',
kwargs={
'project_id': cover.project.linkname,
- 'msgid': cover.url_msgid,
+ 'msgid': cover.encoded_msgid,
},
)
'cover-detail',
kwargs={
'project_id': cover.project.linkname,
- 'msgid': cover.url_msgid,
+ 'msgid': cover.encoded_msgid,
},
),
comment_id,
'cover-detail',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
)
redirect_url = reverse(
'patch-detail',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
)
'patch-detail',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
),
comment_id,
'patch-detail',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
)
'patch-mbox',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
)
'patch-raw',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
)
'patch-detail',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
)
response = self.client.get(requested_url)
'patch-detail',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
)
response = self.client.get(requested_url)
response = self.client.get(
reverse(
'patch-detail',
- args=[self.patch.project.linkname, self.patch.url_msgid],
+ args=[self.patch.project.linkname, self.patch.encoded_msgid],
)
)
self.assertContains(response, self.patch.name)
response = self.client.get(
reverse(
'patch-mbox',
- args=[self.patch.project.linkname, self.patch.url_msgid],
+ args=[self.patch.project.linkname, self.patch.encoded_msgid],
)
)
self.assertEqual(response.status_code, 200)
response = self.client.get(
reverse(
'patch-raw',
- args=[self.patch.project.linkname, self.patch.url_msgid],
+ args=[self.patch.project.linkname, self.patch.encoded_msgid],
)
)
self.assertEqual(response.status_code, 200)
name='project-detail',
),
# patch views
- # NOTE(dja): Per the RFC, msgids can contain slashes. There doesn't seem
- # to be an easy way to tell Django to urlencode the slash when generating
- # URLs, so instead we must use a permissive regex (.+ rather than [^/]+).
- # This also means we need to put the raw and mbox URLs first, otherwise the
- # patch-detail regex will just greedily grab those parts into a massive and
- # wrong msgid.
- #
- # This does mean that message-ids that end in '/raw/' or '/mbox/' will not
- # work, but it is RECOMMENDED by the RFC that the right hand side of the @
- # contains a domain, so I think breaking on messages that have "domains"
- # ending in /raw/ or /mbox/ is good enough.
+ # NOTE(stephenfin): Per the RFC, msgids can contain slashes. Users are
+ # required to percent-encode any slashes present to generate valid URLs.
+ # The API does this automatically.
path(
- 'project/<project_id>/patch/<path:msgid>/raw/',
+ 'project/<project_id>/patch/<str:msgid>/raw/',
patch_views.patch_raw,
name='patch-raw',
),
path(
- 'project/<project_id>/patch/<path:msgid>/mbox/',
+ 'project/<project_id>/patch/<str:msgid>/mbox/',
patch_views.patch_mbox,
name='patch-mbox',
),
path(
- 'project/<project_id>/patch/<path:msgid>/',
+ 'project/<project_id>/patch/<str:msgid>/',
patch_views.patch_detail,
name='patch-detail',
),
'patch-detail',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
)
else: # cover
'cover-detail',
kwargs={
'project_id': cover.project.linkname,
- 'msgid': cover.url_msgid,
+ 'msgid': cover.encoded_msgid,
},
)
'cover-detail',
kwargs={
'project_id': cover.project.linkname,
- 'msgid': cover.url_msgid,
+ 'msgid': cover.encoded_msgid,
},
)
'cover-mbox',
kwargs={
'project_id': cover.project.linkname,
- 'msgid': cover.url_msgid,
+ 'msgid': cover.encoded_msgid,
},
)
def patch_detail(request, project_id, msgid):
project = get_object_or_404(Project, linkname=project_id)
- db_msgid = '<%s>' % msgid
+ db_msgid = f"<{msgid.replace('%2F', '/')}>"
# redirect to cover letters where necessary
try:
'patch-detail',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
)
'patch-mbox',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
)
'patch-raw',
kwargs={
'project_id': patch.project.linkname,
- 'msgid': patch.url_msgid,
+ 'msgid': patch.encoded_msgid,
},
)