diff --git a/trac/tests/functional/testcases.py b/trac/tests/functional/testcases.py index 4c8414d844..2a34919042 100755 --- a/trac/tests/functional/testcases.py +++ b/trac/tests/functional/testcases.py @@ -304,6 +304,7 @@ def runTest(self): # are used and the req.perm has no permissions. tc.notfind(internal_error) tc.notfind("You don't have the required permissions") + tc.find('>logged in as joé') self._tester.logout() # finally restore expected 'admin' login self._tester.login('admin') diff --git a/trac/web/api.py b/trac/web/api.py index c944b005c7..80cf2ede85 100644 --- a/trac/web/api.py +++ b/trac/web/api.py @@ -470,6 +470,33 @@ def arg_list_to_args(arg_list): return args +if hasattr(str, 'isascii'): + _isascii = lambda value: value.isascii() +else: + _is_non_ascii_re = re.compile(r'[^\x00-\x7f]') + _isascii = lambda value: not _is_non_ascii_re.search(value) + + +def wsgi_string_decode(value): + """Convert from a WSGI "bytes-as-unicode" string to an unicode string. + """ + if not isinstance(value, str): + raise TypeError('Must a str instance rather than %s' % type(value)) + if not _isascii(value): + value = value.encode('iso-8859-1').decode('utf-8') + return value + + +def wsgi_string_encode(value): + """Convert from an unicode string to a WSGI "bytes-as-unicode" string. + """ + if not isinstance(value, str): + raise TypeError('Must a str instance rather than %s' % type(value)) + if not _isascii(value): + value = value.encode('utf-8').decode('iso-8859-1') + return value + + def _raise_if_null_bytes(value): if value and '\x00' in value: raise HTTPBadRequest(_("Invalid request arguments.")) @@ -666,7 +693,7 @@ def __getattr__(self, name): def __repr__(self): uri = self.environ.get('PATH_INFO', '') - qs = self.query_string + qs = self.environ.get('QUERY_STRING', '') if qs: uri += '?' + qs return '<%s "%s %r">' % (self.__class__.__name__, self.method, uri) @@ -698,21 +725,16 @@ def method(self): def path_info(self): """Path inside the application""" path_info = self.environ.get('PATH_INFO', '') - if isinstance(path_info, str): - # According to PEP 3333, the value is decoded by iso-8859-1 - # encoding when it is a unicode string. However, we need - # decoded unicode string by utf-8 encoding. - path_info = path_info.encode('iso-8859-1') try: - return str(path_info, 'utf-8') - except UnicodeDecodeError: + return wsgi_string_decode(path_info) + except UnicodeError: raise HTTPNotFound(_("Invalid URL encoding (was %(path_info)r)", path_info=path_info)) @property def query_string(self): """Query part of the request""" - return self.environ.get('QUERY_STRING', '') + return wsgi_string_decode(self.environ.get('QUERY_STRING', '')) @property def remote_addr(self): @@ -727,7 +749,7 @@ def remote_user(self): """ user = self.environ.get('REMOTE_USER') if user is not None: - return to_unicode(user) + return wsgi_string_decode(user) @property def request_path(self): @@ -745,7 +767,7 @@ def scheme(self): @property def base_path(self): """The root path of the application""" - return self.environ.get('SCRIPT_NAME', '') + return wsgi_string_decode(self.environ.get('SCRIPT_NAME', '')) @property def server_name(self): diff --git a/trac/web/main.py b/trac/web/main.py index dc5bccb8d7..b8c6b9cc62 100644 --- a/trac/web/main.py +++ b/trac/web/main.py @@ -53,7 +53,8 @@ HTTPInternalServerError, HTTPNotFound, IAuthenticator, \ IRequestFilter, IRequestHandler, Request, \ RequestDone, TracNotImplementedError, \ - is_valid_default_handler, parse_header + is_valid_default_handler, parse_header, \ + wsgi_string_decode, wsgi_string_encode from trac.web.chrome import Chrome, ITemplateProvider, add_notice, \ add_stylesheet, add_warning from trac.web.href import Href @@ -535,14 +536,14 @@ def dispatch_request(environ, start_response): # the remaining path in the `PATH_INFO` variable. script_name = environ.get('SCRIPT_NAME', '') try: - if isinstance(script_name, str): - script_name = script_name.encode('iso-8859-1') # PEP 3333 - script_name = str(script_name, 'utf-8') + script_name = wsgi_string_decode(script_name) + env_name = wsgi_string_decode(env_name) except UnicodeDecodeError: errmsg = 'Invalid URL encoding (was %r)' % script_name else: # (as Href expects unicode parameters) - environ['SCRIPT_NAME'] = Href(script_name)(env_name) + script_name = wsgi_string_encode(Href(script_name)(env_name)) + environ['SCRIPT_NAME'] = script_name environ['PATH_INFO'] = '/' + '/'.join(path_info) if env_parent_dir: diff --git a/trac/web/standalone.py b/trac/web/standalone.py index 5929dc038f..44775e2f71 100755 --- a/trac/web/standalone.py +++ b/trac/web/standalone.py @@ -32,6 +32,7 @@ from trac import __version__ as VERSION from trac.util import autoreload, daemon from trac.util.text import printerr +from trac.web.api import wsgi_string_encode from trac.web.auth import BasicAuthentication, DigestAuthentication from trac.web.main import dispatch_request from trac.web.wsgi import WSGIServer, WSGIRequestHandler @@ -59,7 +60,7 @@ def __call__(self, environ, start_response): remote_user = auth.do_auth(environ, start_response) if not remote_user: return [] - environ['REMOTE_USER'] = remote_user + environ['REMOTE_USER'] = wsgi_string_encode(remote_user) return self.application(environ, start_response) diff --git a/trac/web/tests/api.py b/trac/web/tests/api.py index 8ce5a002b7..a43e1a9a7e 100644 --- a/trac/web/tests/api.py +++ b/trac/web/tests/api.py @@ -702,6 +702,48 @@ def test_check_modified_if_none_match(self): req.send(b'') self.assertEqual(etag, req.headers_sent['ETag']) + def test_path_info(self): + + def test(expected, value): + environ = _make_environ(PATH_INFO=value) + self.assertEqual(expected, _make_req(environ).path_info) + + test('', '') + test('/wiki/WikiStart', '/wiki/WikiStart') + test('/wiki/TæstPäge', '/wiki/T\xc3\xa6stP\xc3\xa4ge') + + def test_query_string(self): + + def test(expected, value): + environ = _make_environ(QUERY_STRING=value) + self.assertEqual(expected, _make_req(environ).query_string) + + test('', '') + test('status=defect&milestone=milestone1', + 'status=defect&milestone=milestone1') + test('status=defećt&milestóne=milestone1', + 'status=defe\xc4\x87t&milest\xc3\xb3ne=milestone1') + + def test_base_path(self): + + def test(expected, value): + environ = _make_environ(SCRIPT_NAME=value) + self.assertEqual(expected, _make_req(environ).base_path) + + test('', '') + test('/1.6-stable', '/1.6-stable') + test('/Prøjeçt-42', '/Pr\xc3\xb8je\xc3\xa7t-42') + + def test_remote_user(self): + + def test(expected, value): + environ = _make_environ(REMOTE_USER=value) + self.assertEqual(expected, _make_req(environ).remote_user) + + test('', '') + test('joe', 'joe') + test('jöhn', 'j\xc3\xb6hn') + class RequestSendFileTestCase(unittest.TestCase):