Merge lp:~stevenk/launchpad/handle-invalid-unicode-in-query-string-redux into lp:launchpad

Proposed by Steve Kowalik
Status: Merged
Approved by: Steve Kowalik
Approved revision: no longer in the source branch.
Merged at revision: 16482
Proposed branch: lp:~stevenk/launchpad/handle-invalid-unicode-in-query-string-redux
Merge into: lp:launchpad
Diff against target: 566 lines (+114/-58)
18 files modified
lib/lp/app/browser/tales.py (+1/-1)
lib/lp/bugs/interfaces/bug.py (+2/-2)
lib/lp/bugs/model/bug.py (+8/-2)
lib/lp/registry/interfaces/productrelease.py (+4/-6)
lib/lp/registry/model/productrelease.py (+22/-17)
lib/lp/registry/stories/webservice/xx-project-registry.txt (+17/-7)
lib/lp/registry/subscribers.py (+2/-4)
lib/lp/registry/tests/test_subscribers.py (+1/-1)
lib/lp/services/webapp/escaping.py (+1/-1)
lib/lp/services/webapp/menu.py (+1/-1)
lib/lp/services/webapp/pgsession.py (+1/-1)
lib/lp/services/webapp/publisher.py (+20/-2)
lib/lp/services/webapp/servers.py (+12/-5)
lib/lp/services/webapp/tests/test_menu.py (+2/-2)
lib/lp/services/webapp/tests/test_servers.py (+15/-3)
lib/lp/services/webapp/tests/test_user_requested_oops.py (+2/-1)
lib/lp/services/webapp/tests/test_view_model.py (+2/-1)
lib/lp/testing/tests/test_publication.py (+1/-1)
To merge this branch: bzr merge lp:~stevenk/launchpad/handle-invalid-unicode-in-query-string-redux
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+146773@code.launchpad.net

Commit message

If we fail to decode the query string parameters, forcibly decode it to utf-8 with replacements, again. We also work around a Zope bug by grabbing the request and pulling the un-decoded file contents out for IProductReleaseFile.add_file and IBug.addAttachment.

Description of the change

This branch resurrects the changes from r1646[12] which were reverted due to discovering that lazr.restfulclient does not set filename on file uploads, so Zope will encode the contents, resulting in garbage being stored.

We work around this by grabbing the current request in the two exported methods (IProductRelease.add_file, and IBug.addAttachment) and reaching into the body and pulling out the un-encoded file contents and using that instead.

I have also ripped out the export of get_current_browser_request in lp.services.webapp.publisher, and fixed all imports that were expecting it there to pull from lazr.restful.utils, as well as some whitespace cleanups.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

You'll want to also apply the hack to signature_content, but otherwise fine.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/app/browser/tales.py'
--- lib/lp/app/browser/tales.py 2013-01-30 05:31:20 +0000
+++ lib/lp/app/browser/tales.py 2013-02-07 01:27:23 +0000
@@ -19,6 +19,7 @@
19import urllib19import urllib
2020
21from lazr.enum import enumerated_type_registry21from lazr.enum import enumerated_type_registry
22from lazr.restful.utils import get_current_browser_request
22from lazr.uri import URI23from lazr.uri import URI
23import pytz24import pytz
24from z3c.ptcompat import ViewPageTemplateFile25from z3c.ptcompat import ViewPageTemplateFile
@@ -94,7 +95,6 @@
94 get_facet,95 get_facet,
95 )96 )
96from lp.services.webapp.publisher import (97from lp.services.webapp.publisher import (
97 get_current_browser_request,
98 LaunchpadView,98 LaunchpadView,
99 nearest,99 nearest,
100 )100 )
101101
=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py 2013-01-07 02:40:55 +0000
+++ lib/lp/bugs/interfaces/bug.py 2013-02-07 01:27:23 +0000
@@ -685,14 +685,14 @@
685class IBugEdit(Interface):685class IBugEdit(Interface):
686 """IBug attributes that require launchpad.Edit permission."""686 """IBug attributes that require launchpad.Edit permission."""
687687
688 @call_with(owner=REQUEST_USER)688 @call_with(owner=REQUEST_USER, from_api=True)
689 @operation_parameters(689 @operation_parameters(
690 data=Bytes(constraint=attachment_size_constraint),690 data=Bytes(constraint=attachment_size_constraint),
691 comment=Text(), filename=TextLine(), is_patch=Bool(),691 comment=Text(), filename=TextLine(), is_patch=Bool(),
692 content_type=TextLine(), description=Text())692 content_type=TextLine(), description=Text())
693 @export_factory_operation(IBugAttachment, [])693 @export_factory_operation(IBugAttachment, [])
694 def addAttachment(owner, data, comment, filename, is_patch=False,694 def addAttachment(owner, data, comment, filename, is_patch=False,
695 content_type=None, description=None):695 content_type=None, description=None, from_api=False):
696 """Attach a file to this bug.696 """Attach a file to this bug.
697697
698 :owner: An IPerson.698 :owner: An IPerson.
699699
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2012-12-20 14:55:13 +0000
+++ lib/lp/bugs/model/bug.py 2013-02-07 01:27:23 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Launchpad bug-related database table classes."""4"""Launchpad bug-related database table classes."""
@@ -226,6 +226,7 @@
226 )226 )
227from lp.services.webapp.authorization import check_permission227from lp.services.webapp.authorization import check_permission
228from lp.services.webapp.interfaces import ILaunchBag228from lp.services.webapp.interfaces import ILaunchBag
229from lp.services.webapp.publisher import get_raw_form_value_from_current_request
229230
230231
231def snapshot_bug_params(bug_params):232def snapshot_bug_params(bug_params):
@@ -1250,8 +1251,13 @@
1250 bug_watch.destroySelf()1251 bug_watch.destroySelf()
12511252
1252 def addAttachment(self, owner, data, comment, filename, is_patch=False,1253 def addAttachment(self, owner, data, comment, filename, is_patch=False,
1253 content_type=None, description=None):1254 content_type=None, description=None, from_api=False):
1254 """See `IBug`."""1255 """See `IBug`."""
1256 # XXX: StevenK 2013-02-06 bug=1116954: We should not need to refetch
1257 # the file content from the request, since the passed in one has been
1258 # wrongly encoded.
1259 if from_api:
1260 data = get_raw_form_value_from_current_request('data')
1255 if isinstance(data, str):1261 if isinstance(data, str):
1256 filecontent = data1262 filecontent = data
1257 else:1263 else:
12581264
=== modified file 'lib/lp/registry/interfaces/productrelease.py'
--- lib/lp/registry/interfaces/productrelease.py 2013-01-25 05:25:34 +0000
+++ lib/lp/registry/interfaces/productrelease.py 2013-02-07 01:27:23 +0000
@@ -224,7 +224,7 @@
224class IProductReleaseEditRestricted(Interface):224class IProductReleaseEditRestricted(Interface):
225 """`IProductRelease` properties which require `launchpad.Edit`."""225 """`IProductRelease` properties which require `launchpad.Edit`."""
226226
227 @call_with(uploader=REQUEST_USER)227 @call_with(uploader=REQUEST_USER, from_api=True)
228 @operation_parameters(228 @operation_parameters(
229 filename=TextLine(),229 filename=TextLine(),
230 signature_filename=TextLine(),230 signature_filename=TextLine(),
@@ -232,16 +232,14 @@
232 file_content=Bytes(constraint=productrelease_file_size_constraint),232 file_content=Bytes(constraint=productrelease_file_size_constraint),
233 signature_content=Bytes(233 signature_content=Bytes(
234 constraint=productrelease_signature_size_constraint),234 constraint=productrelease_signature_size_constraint),
235 file_type=copy_field(IProductReleaseFile['filetype'], required=False)235 file_type=copy_field(IProductReleaseFile['filetype'], required=False))
236 )236 @export_factory_operation(IProductReleaseFile, ['description'])
237 @export_factory_operation(
238 IProductReleaseFile, ['description'])
239 @export_operation_as('add_file')237 @export_operation_as('add_file')
240 def addReleaseFile(filename, file_content, content_type,238 def addReleaseFile(filename, file_content, content_type,
241 uploader, signature_filename=None,239 uploader, signature_filename=None,
242 signature_content=None,240 signature_content=None,
243 file_type=UpstreamFileType.CODETARBALL,241 file_type=UpstreamFileType.CODETARBALL,
244 description=None):242 description=None, from_api=False):
245 """Add file to the library and link to this `IProductRelease`.243 """Add file to the library and link to this `IProductRelease`.
246244
247 The signature file will also be added if available.245 The signature file will also be added if available.
248246
=== modified file 'lib/lp/registry/model/productrelease.py'
--- lib/lp/registry/model/productrelease.py 2013-01-25 05:25:34 +0000
+++ lib/lp/registry/model/productrelease.py 2013-02-07 01:27:23 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -56,6 +56,7 @@
56 )56 )
57from lp.services.librarian.interfaces import ILibraryFileAliasSet57from lp.services.librarian.interfaces import ILibraryFileAliasSet
58from lp.services.propertycache import cachedproperty58from lp.services.propertycache import cachedproperty
59from lp.services.webapp.publisher import get_raw_form_value_from_current_request
5960
6061
61class ProductRelease(SQLBase):62class ProductRelease(SQLBase):
@@ -148,7 +149,7 @@
148 uploader, signature_filename=None,149 uploader, signature_filename=None,
149 signature_content=None,150 signature_content=None,
150 file_type=UpstreamFileType.CODETARBALL,151 file_type=UpstreamFileType.CODETARBALL,
151 description=None):152 description=None, from_api=False):
152 """See `IProductRelease`."""153 """See `IProductRelease`."""
153 if not self.can_have_release_files:154 if not self.can_have_release_files:
154 raise ProprietaryProduct(155 raise ProprietaryProduct(
@@ -157,31 +158,35 @@
157 raise InvalidFilename158 raise InvalidFilename
158 # Create the alias for the file.159 # Create the alias for the file.
159 filename = self.normalizeFilename(filename)160 filename = self.normalizeFilename(filename)
161 # XXX: StevenK 2013-02-06 bug=1116954: We should not need to refetch
162 # the file content from the request, since the passed in one has been
163 # wrongly encoded.
164 if from_api:
165 file_content = get_raw_form_value_from_current_request(
166 'file_content')
160 file_obj, file_size = self._getFileObjectAndSize(file_content)167 file_obj, file_size = self._getFileObjectAndSize(file_content)
161168
162 alias = getUtility(ILibraryFileAliasSet).create(169 alias = getUtility(ILibraryFileAliasSet).create(
163 name=filename,170 name=filename, size=file_size, file=file_obj,
164 size=file_size,
165 file=file_obj,
166 contentType=content_type)171 contentType=content_type)
167 if signature_filename is not None and signature_content is not None:172 if signature_filename is not None and signature_content is not None:
173 # XXX: StevenK 2013-02-06 bug=1116954: We should not need to
174 # refetch the file content from the request, since the passed in
175 # one has been wrongly encoded.
176 if from_api:
177 signature_content = get_raw_form_value_from_current_request(
178 'signature_content')
168 signature_obj, signature_size = self._getFileObjectAndSize(179 signature_obj, signature_size = self._getFileObjectAndSize(
169 signature_content)180 signature_content)
170 signature_filename = self.normalizeFilename(181 signature_filename = self.normalizeFilename(signature_filename)
171 signature_filename)
172 signature_alias = getUtility(ILibraryFileAliasSet).create(182 signature_alias = getUtility(ILibraryFileAliasSet).create(
173 name=signature_filename,183 name=signature_filename, size=signature_size,
174 size=signature_size,184 file=signature_obj, contentType='application/pgp-signature')
175 file=signature_obj,
176 contentType='application/pgp-signature')
177 else:185 else:
178 signature_alias = None186 signature_alias = None
179 return ProductReleaseFile(productrelease=self,187 return ProductReleaseFile(
180 libraryfile=alias,188 productrelease=self, libraryfile=alias, signature=signature_alias,
181 signature=signature_alias,189 filetype=file_type, description=description, uploader=uploader)
182 filetype=file_type,
183 description=description,
184 uploader=uploader)
185190
186 def getFileAliasByName(self, name):191 def getFileAliasByName(self, name):
187 """See `IProductRelease`."""192 """See `IProductRelease`."""
188193
=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2012-12-10 13:43:47 +0000
+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2013-02-07 01:27:23 +0000
@@ -1020,8 +1020,8 @@
10201020
1021 >>> pr_url = '/firefox/1.0/1.0.0'1021 >>> pr_url = '/firefox/1.0/1.0.0'
1022 >>> ff_100 = webservice.get(pr_url).jsonBody()1022 >>> ff_100 = webservice.get(pr_url).jsonBody()
1023 >>> file_content="first attachment file content"1023 >>> file_content="first attachment file content \xff"
1024 >>> sig_file_content="hash hash hash"1024 >>> sig_file_content="hash hash hash \xff"
1025 >>> response = webservice.named_post(ff_100['self_link'], 'add_file',1025 >>> response = webservice.named_post(ff_100['self_link'], 'add_file',
1026 ... filename='filename.txt',1026 ... filename='filename.txt',
1027 ... file_content=file_content,1027 ... file_content=file_content,
@@ -1043,6 +1043,20 @@
1043 >>> print_self_link_of_entries(ff_100_files)1043 >>> print_self_link_of_entries(ff_100_files)
1044 http://.../firefox/1.0/1.0.0/+file/filename.txt1044 http://.../firefox/1.0/1.0.0/+file/filename.txt
10451045
1046And it has been uploaded correctly.
1047
1048 >>> from zope.component import getUtility
1049 >>> from lp.registry.interfaces.product import IProductSet
1050 >>> from lp.testing import login, logout
1051 >>> login('bac@canonical.com')
1052 >>> concrete_one_zero = getUtility(IProductSet)['firefox'].getRelease(
1053 ... '1.0.0')
1054 >>> concrete_one_zero.files[0].libraryfile.read() == file_content
1055 True
1056 >>> concrete_one_zero.files[0].signature.read() == sig_file_content
1057 True
1058 >>> logout()
1059
1046The file type and description are optional. If no signature is1060The file type and description are optional. If no signature is
1047available then it must be explicitly set to None.1061available then it must be explicitly set to None.
10481062
@@ -1106,12 +1120,8 @@
1106If a project has a commercial-use subscription then it can be retrieved1120If a project has a commercial-use subscription then it can be retrieved
1107through the API.1121through the API.
11081122
1109 >>> from zope.component import getUtility
1110 >>> from lp.registry.interfaces.product import IProductSet
1111 >>> from lp.testing import login, logout
1112 >>> login('bac@canonical.com')1123 >>> login('bac@canonical.com')
1113 >>> product_set = getUtility(IProductSet)1124 >>> mmm = getUtility(IProductSet)['mega-money-maker']
1114 >>> mmm = product_set.getByName('mega-money-maker')
1115 >>> print mmm.commercial_subscription1125 >>> print mmm.commercial_subscription
1116 None1126 None
11171127
11181128
=== modified file 'lib/lp/registry/subscribers.py'
--- lib/lp/registry/subscribers.py 2012-11-26 08:40:20 +0000
+++ lib/lp/registry/subscribers.py 2013-02-07 01:27:23 +0000
@@ -11,6 +11,7 @@
11from datetime import datetime11from datetime import datetime
12import textwrap12import textwrap
1313
14from lazr.restful.utils import get_current_browser_request
14import pytz15import pytz
15from zope.security.proxy import removeSecurityProxy16from zope.security.proxy import removeSecurityProxy
1617
@@ -23,10 +24,7 @@
23 simple_sendmail,24 simple_sendmail,
24 )25 )
25from lp.services.webapp.escaping import structured26from lp.services.webapp.escaping import structured
26from lp.services.webapp.publisher import (27from lp.services.webapp.publisher import canonical_url
27 canonical_url,
28 get_current_browser_request,
29 )
3028
3129
32def product_licenses_modified(product, event):30def product_licenses_modified(product, event):
3331
=== modified file 'lib/lp/registry/tests/test_subscribers.py'
--- lib/lp/registry/tests/test_subscribers.py 2012-12-10 13:43:47 +0000
+++ lib/lp/registry/tests/test_subscribers.py 2013-02-07 01:27:23 +0000
@@ -7,6 +7,7 @@
77
8from datetime import datetime8from datetime import datetime
99
10from lazr.restful.utils import get_current_browser_request
10import pytz11import pytz
11from zope.component import getUtility12from zope.component import getUtility
12from zope.security.proxy import removeSecurityProxy13from zope.security.proxy import removeSecurityProxy
@@ -23,7 +24,6 @@
23 product_licenses_modified,24 product_licenses_modified,
24 )25 )
25from lp.services.webapp.escaping import html_escape26from lp.services.webapp.escaping import html_escape
26from lp.services.webapp.publisher import get_current_browser_request
27from lp.testing import (27from lp.testing import (
28 login_person,28 login_person,
29 logout,29 logout,
3030
=== modified file 'lib/lp/services/webapp/escaping.py'
--- lib/lp/services/webapp/escaping.py 2012-12-10 22:25:44 +0000
+++ lib/lp/services/webapp/escaping.py 2013-02-07 01:27:23 +0000
@@ -8,6 +8,7 @@
8 'structured',8 'structured',
9 ]9 ]
1010
11from lazr.restful.utils import get_current_browser_request
11from zope.i18n import (12from zope.i18n import (
12 Message,13 Message,
13 translate,14 translate,
@@ -15,7 +16,6 @@
15from zope.interface import implements16from zope.interface import implements
1617
17from lp.services.webapp.interfaces import IStructuredString18from lp.services.webapp.interfaces import IStructuredString
18from lp.services.webapp.publisher import get_current_browser_request
1919
2020
21HTML_REPLACEMENTS = (21HTML_REPLACEMENTS = (
2222
=== modified file 'lib/lp/services/webapp/menu.py'
--- lib/lp/services/webapp/menu.py 2012-12-12 04:59:52 +0000
+++ lib/lp/services/webapp/menu.py 2013-02-07 01:27:23 +0000
@@ -21,6 +21,7 @@
21import types21import types
2222
23from lazr.delegates import delegates23from lazr.delegates import delegates
24from lazr.restful.utils import get_current_browser_request
24from lazr.uri import (25from lazr.uri import (
25 InvalidURIError,26 InvalidURIError,
26 URI,27 URI,
@@ -45,7 +46,6 @@
45 )46 )
46from lp.services.webapp.publisher import (47from lp.services.webapp.publisher import (
47 canonical_url,48 canonical_url,
48 get_current_browser_request,
49 LaunchpadView,49 LaunchpadView,
50 UserAttributeCache,50 UserAttributeCache,
51 )51 )
5252
=== modified file 'lib/lp/services/webapp/pgsession.py'
--- lib/lp/services/webapp/pgsession.py 2012-01-01 03:00:09 +0000
+++ lib/lp/services/webapp/pgsession.py 2013-02-07 01:27:23 +0000
@@ -9,6 +9,7 @@
9import time9import time
10from UserDict import DictMixin10from UserDict import DictMixin
1111
12from lazr.restful.utils import get_current_browser_request
12from storm.zope.interfaces import IZStorm13from storm.zope.interfaces import IZStorm
13from zope.app.security.interfaces import IUnauthenticatedPrincipal14from zope.app.security.interfaces import IUnauthenticatedPrincipal
14from zope.component import getUtility15from zope.component import getUtility
@@ -21,7 +22,6 @@
21 )22 )
2223
23from lp.services.helpers import ensure_unicode24from lp.services.helpers import ensure_unicode
24from lp.services.webapp.publisher import get_current_browser_request
2525
2626
27SECONDS = 127SECONDS = 1
2828
=== modified file 'lib/lp/services/webapp/publisher.py'
--- lib/lp/services/webapp/publisher.py 2012-11-26 18:44:34 +0000
+++ lib/lp/services/webapp/publisher.py 2013-02-07 01:27:23 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Publisher of objects as web pages.4"""Publisher of objects as web pages.
@@ -8,13 +8,13 @@
8__metaclass__ = type8__metaclass__ = type
9__all__ = [9__all__ = [
10 'DataDownloadView',10 'DataDownloadView',
11 'get_raw_form_value_from_current_request',
11 'LaunchpadContainer',12 'LaunchpadContainer',
12 'LaunchpadView',13 'LaunchpadView',
13 'LaunchpadXMLRPCView',14 'LaunchpadXMLRPCView',
14 'canonical_name',15 'canonical_name',
15 'canonical_url',16 'canonical_url',
16 'canonical_url_iterator',17 'canonical_url_iterator',
17 'get_current_browser_request',
18 'nearest',18 'nearest',
19 'Navigation',19 'Navigation',
20 'rootObject',20 'rootObject',
@@ -26,6 +26,7 @@
26 'UserAttributeCache',26 'UserAttributeCache',
27 ]27 ]
2828
29from cgi import FieldStorage
29import httplib30import httplib
30import re31import re
3132
@@ -800,6 +801,23 @@
800 return None801 return None
801802
802803
804def get_raw_form_value_from_current_request(field_name):
805 # XXX: StevenK 2013-02-06 bug=1116954: We should not need to refetch
806 # the file content from the request, since the passed in one has been
807 # wrongly encoded.
808 # Circular imports.
809 from lp.services.webapp.servers import WebServiceClientRequest
810 request = get_current_browser_request()
811 assert isinstance(request, WebServiceClientRequest)
812 # Zope wrongly encodes any form element that doesn't look like a file,
813 # so re-fetch the file content if it has been encoded.
814 if request and request.form.has_key(field_name) and isinstance(
815 request.form[field_name], unicode):
816 request._environ['wsgi.input'].seek(0)
817 fs = FieldStorage(fp=request._body_instream, environ=request._environ)
818 return fs[field_name].value
819
820
803class RootObject:821class RootObject:
804 implements(ILaunchpadApplication, ILaunchpadRoot)822 implements(ILaunchpadApplication, ILaunchpadRoot)
805823
806824
=== modified file 'lib/lp/services/webapp/servers.py'
--- lib/lp/services/webapp/servers.py 2013-02-03 01:49:37 +0000
+++ lib/lp/services/webapp/servers.py 2013-02-07 01:27:23 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Definition of the internet servers that Launchpad uses."""4"""Definition of the internet servers that Launchpad uses."""
@@ -18,6 +18,7 @@
18 WebServicePublicationMixin,18 WebServicePublicationMixin,
19 WebServiceRequestTraversal,19 WebServiceRequestTraversal,
20 )20 )
21from lazr.restful.utils import get_current_browser_request
21from lazr.uri import URI22from lazr.uri import URI
22import transaction23import transaction
23from transaction.interfaces import ISynchronizer24from transaction.interfaces import ISynchronizer
@@ -100,10 +101,7 @@
100 )101 )
101from lp.services.webapp.opstats import OpStats102from lp.services.webapp.opstats import OpStats
102from lp.services.webapp.publication import LaunchpadBrowserPublication103from lp.services.webapp.publication import LaunchpadBrowserPublication
103from lp.services.webapp.publisher import (104from lp.services.webapp.publisher import RedirectionView
104 get_current_browser_request,
105 RedirectionView,
106 )
107from lp.services.webapp.vhosts import allvhosts105from lp.services.webapp.vhosts import allvhosts
108from lp.services.webservice.interfaces import IWebServiceApplication106from lp.services.webservice.interfaces import IWebServiceApplication
109from lp.testopenid.interfaces.server import ITestOpenIDApplication107from lp.testopenid.interfaces.server import ITestOpenIDApplication
@@ -657,6 +655,15 @@
657 """As per zope.publisher.browser.BrowserRequest._createResponse"""655 """As per zope.publisher.browser.BrowserRequest._createResponse"""
658 return LaunchpadBrowserResponse()656 return LaunchpadBrowserResponse()
659657
658 def _decode(self, text):
659 text = super(LaunchpadBrowserRequest, self)._decode(text)
660 if isinstance(text, str):
661 # BrowserRequest._decode failed to do so with the user-specified
662 # charsets, so decode as UTF-8 with replacements, since we always
663 # want unicode.
664 text = unicode(text, 'utf-8', 'replace')
665 return text
666
660 @cachedproperty667 @cachedproperty
661 def form_ng(self):668 def form_ng(self):
662 """See ILaunchpadBrowserApplicationRequest."""669 """See ILaunchpadBrowserApplicationRequest."""
663670
=== modified file 'lib/lp/services/webapp/tests/test_menu.py'
--- lib/lp/services/webapp/tests/test_menu.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/webapp/tests/test_menu.py 2013-02-07 01:27:23 +0000
@@ -3,6 +3,7 @@
33
4__metaclass__ = type4__metaclass__ = type
55
6from lazr.restful.utils import get_current_browser_request
6from zope.security.management import newInteraction7from zope.security.management import newInteraction
78
8from lp.services.webapp.menu import (9from lp.services.webapp.menu import (
@@ -10,7 +11,6 @@
10 MENU_ANNOTATION_KEY,11 MENU_ANNOTATION_KEY,
11 MenuBase,12 MenuBase,
12 )13 )
13from lp.services.webapp.publisher import get_current_browser_request
14from lp.testing import (14from lp.testing import (
15 ANONYMOUS,15 ANONYMOUS,
16 login,16 login,
@@ -77,7 +77,7 @@
77 login(ANONYMOUS)77 login(ANONYMOUS)
78 context = object()78 context = object()
79 menu = TestMenu(context)79 menu = TestMenu(context)
80 link = menu._get_link('test_link')80 menu._get_link('test_link')
81 cache = get_current_browser_request().annotations.get(81 cache = get_current_browser_request().annotations.get(
82 MENU_ANNOTATION_KEY)82 MENU_ANNOTATION_KEY)
83 self.assertEquals(len(cache.keys()), 1)83 self.assertEquals(len(cache.keys()), 1)
8484
=== modified file 'lib/lp/services/webapp/tests/test_servers.py'
--- lib/lp/services/webapp/tests/test_servers.py 2013-02-03 01:49:37 +0000
+++ lib/lp/services/webapp/tests/test_servers.py 2013-02-07 01:27:23 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -50,6 +50,7 @@
50 EventRecorder,50 EventRecorder,
51 TestCase,51 TestCase,
52 )52 )
53from lp.testing.layers import FunctionalLayer
5354
5455
55class SetInWSGIEnvironmentTestCase(TestCase):56class SetInWSGIEnvironmentTestCase(TestCase):
@@ -230,8 +231,7 @@
230231
231 for method in denied_methods:232 for method in denied_methods:
232 env = self.wsgi_env(self.non_api_path, method)233 env = self.wsgi_env(self.non_api_path, method)
233 self.assert_(self.factory.canHandle(env),234 self.assert_(self.factory.canHandle(env), "Sanity check")
234 "Sanity check")
235 # Returns a tuple of (request_factory, publication_factory).235 # Returns a tuple of (request_factory, publication_factory).
236 rfactory, pfactory = self.factory.checkRequest(env)236 rfactory, pfactory = self.factory.checkRequest(env)
237 self.assert_(rfactory is not None,237 self.assert_(rfactory is not None,
@@ -352,6 +352,8 @@
352class TestBasicLaunchpadRequest(TestCase):352class TestBasicLaunchpadRequest(TestCase):
353 """Tests for the base request class"""353 """Tests for the base request class"""
354354
355 layer = FunctionalLayer
356
355 def test_baserequest_response_should_vary(self):357 def test_baserequest_response_should_vary(self):
356 """Test that our base response has a proper vary header."""358 """Test that our base response has a proper vary header."""
357 request = LaunchpadBrowserRequest(StringIO.StringIO(''), {})359 request = LaunchpadBrowserRequest(StringIO.StringIO(''), {})
@@ -387,6 +389,16 @@
387 request = LaunchpadBrowserRequest(StringIO.StringIO(''), env)389 request = LaunchpadBrowserRequest(StringIO.StringIO(''), env)
388 self.assertEquals(u'fnord/trunk\ufffd', request.getHeader('PATH_INFO'))390 self.assertEquals(u'fnord/trunk\ufffd', request.getHeader('PATH_INFO'))
389391
392 def test_request_with_invalid_query_string_recovers(self):
393 # When the query string has invalid utf-8, it is decoded with
394 # replacement.
395 env = {'QUERY_STRING': 'field.title=subproc\xe9s '}
396 request = LaunchpadBrowserRequest(StringIO.StringIO(''), env)
397 # XXX: Python 2.6 and 2.7 handle unicode replacement differently.
398 self.assertIn(
399 request.query_string_params['field.title'],
400 ([u'subproc\ufffd'], [u'subproc\ufffds ']))
401
390402
391class TestFeedsBrowserRequest(TestCase):403class TestFeedsBrowserRequest(TestCase):
392 """Tests for `FeedsBrowserRequest`."""404 """Tests for `FeedsBrowserRequest`."""
393405
=== modified file 'lib/lp/services/webapp/tests/test_user_requested_oops.py'
--- lib/lp/services/webapp/tests/test_user_requested_oops.py 2012-03-06 23:39:08 +0000
+++ lib/lp/services/webapp/tests/test_user_requested_oops.py 2013-02-07 01:27:23 +0000
@@ -1,4 +1,5 @@
1# Copyright 2009 Canonical Ltd. All rights reserved.1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
23
3"""Tests for the user requested oops using ++oops++ traversal."""4"""Tests for the user requested oops using ++oops++ traversal."""
45
56
=== modified file 'lib/lp/services/webapp/tests/test_view_model.py'
--- lib/lp/services/webapp/tests/test_view_model.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/webapp/tests/test_view_model.py 2013-02-07 01:27:23 +0000
@@ -1,4 +1,5 @@
1# Copyright 2011 Canonical Ltd. All rights reserved.1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
23
3"""Tests for the user requested oops using ++oops++ traversal."""4"""Tests for the user requested oops using ++oops++ traversal."""
45
56
=== modified file 'lib/lp/testing/tests/test_publication.py'
--- lib/lp/testing/tests/test_publication.py 2012-04-12 06:06:49 +0000
+++ lib/lp/testing/tests/test_publication.py 2013-02-07 01:27:23 +0000
@@ -6,6 +6,7 @@
6__metaclass__ = type6__metaclass__ = type
77
8from lazr.restful import EntryResource8from lazr.restful import EntryResource
9from lazr.restful.utils import get_current_browser_request
9from zope.app.pagetemplate.simpleviewclass import simple10from zope.app.pagetemplate.simpleviewclass import simple
10from zope.component import (11from zope.component import (
11 getSiteManager,12 getSiteManager,
@@ -24,7 +25,6 @@
24 ILaunchpadRoot,25 ILaunchpadRoot,
25 )26 )
26from lp.services.webapp.publisher import (27from lp.services.webapp.publisher import (
27 get_current_browser_request,
28 Navigation,28 Navigation,
29 stepthrough,29 stepthrough,
30 )30 )