Skip to content

Commit

Permalink
Work around a bug in Python. ~ is an unreserved character and should not
Browse files Browse the repository at this point in the history
be quoted in URLs.
  • Loading branch information
faassen committed Apr 11, 2016
1 parent a7618d6 commit 9a36f25
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 2 deletions.
6 changes: 6 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ CHANGES

- Document ``scan`` function in API docs.

- Work around an issue in Python where ~ (tilde) is quoted by
urllib.quote & urllib.encode, even though it should not be according
to the RFC, as ~ is considered unreserved.

https://www.ietf.org/rfc/rfc3986.txt

0.13 (2016-04-06)
=================

Expand Down
28 changes: 26 additions & 2 deletions morepath/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,14 @@ def class_link(self, model, variables=None, name='', app=SAME_APP):
def _encode_link(self, path, name, parameters):
parts = []
if path:
parts.append(quote(path.encode('utf-8')))
parts.append(fixed_quote(path.encode('utf-8')))
if name:
parts.append(name)
result = self.link_prefix() + '/' + '/'.join(parts)
if parameters:
parameters = dict((key, [v.encode('utf-8') for v in value])
for (key, value) in parameters.items())
result += '?' + urlencode(parameters, True)
result += '?' + fixed_urlencode(parameters, True)
return result

def resolve_path(self, path, app=SAME_APP):
Expand Down Expand Up @@ -377,3 +377,27 @@ def mounted_link(path, parameters, app):
app = app.parent
result.reverse()
return '/'.join(result).strip('/'), parameters


def fixed_quote(s, safe='/'):
"""urllib.quote fixed for ~
Workaround for Python bug:
https://bugs.python.org/issue16285
tilde should not be encoded according to RFC3986
"""
return quote(s, safe=safe + '~')


def fixed_urlencode(s, doseq=0):
"""urllib.urlencode fixed for ~
Workaround for Python bug:
https://bugs.python.org/issue16285
tilde should not be encoded according to RFC3986
"""
return urlencode(s, doseq).replace('%7E', '~')
67 changes: 67 additions & 0 deletions morepath/tests/test_path_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -1669,6 +1669,42 @@ def link(self, request):
assert response.body == b'http://localhost/sim%C3%ABple'


def test_quoting_link_generation_tilde():
# tilde is an unreserved character according to
# https://www.ietf.org/rfc/rfc3986.txt but urllib.quote
# quotes it anyway. We test whether our workaround using
# the safe parameter works

class App(morepath.App):
pass

class Model(object):
def __init__(self):
pass

@App.path(model=Model, path='sim~ple')
def get_model():
return Model()

@App.view(model=Model)
def default(self, request):
return "View"

@App.view(model=Model, name='link')
def link(self, request):
return request.link(self)

dectate.commit(App)

c = Client(App())

response = c.get('/sim~ple')
assert response.body == b'View'

response = c.get('/sim~ple/link')
assert response.body == b'http://localhost/sim~ple'


def test_parameter_quoting():
class App(morepath.App):
pass
Expand Down Expand Up @@ -1700,6 +1736,37 @@ def link(self, request):
assert response.body == b'http://localhost/?s=sim%C3%ABple'


def test_parameter_quoting_tilde():
class App(morepath.App):
pass

class Model(object):
def __init__(self, s):
self.s = s

@App.path(model=Model, path='')
def get_model(s):
return Model(s)

@App.view(model=Model)
def default(self, request):
return u"View: %s" % self.s

@App.view(model=Model, name='link')
def link(self, request):
return request.link(self)

dectate.commit(App)

c = Client(App())

response = c.get('/?s=sim~ple')
assert response.body == u"View: sim~ple".encode('utf-8')

response = c.get('/link?s=sim~ple')
assert response.body == b'http://localhost/?s=sim~ple'


def test_class_link_without_variables():
class App(morepath.App):
pass
Expand Down

0 comments on commit 9a36f25

Please sign in to comment.