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
1=== modified file 'lib/lp/app/browser/tales.py'
2--- lib/lp/app/browser/tales.py 2013-01-30 05:31:20 +0000
3+++ lib/lp/app/browser/tales.py 2013-02-07 01:27:23 +0000
4@@ -19,6 +19,7 @@
5 import urllib
6
7 from lazr.enum import enumerated_type_registry
8+from lazr.restful.utils import get_current_browser_request
9 from lazr.uri import URI
10 import pytz
11 from z3c.ptcompat import ViewPageTemplateFile
12@@ -94,7 +95,6 @@
13 get_facet,
14 )
15 from lp.services.webapp.publisher import (
16- get_current_browser_request,
17 LaunchpadView,
18 nearest,
19 )
20
21=== modified file 'lib/lp/bugs/interfaces/bug.py'
22--- lib/lp/bugs/interfaces/bug.py 2013-01-07 02:40:55 +0000
23+++ lib/lp/bugs/interfaces/bug.py 2013-02-07 01:27:23 +0000
24@@ -685,14 +685,14 @@
25 class IBugEdit(Interface):
26 """IBug attributes that require launchpad.Edit permission."""
27
28- @call_with(owner=REQUEST_USER)
29+ @call_with(owner=REQUEST_USER, from_api=True)
30 @operation_parameters(
31 data=Bytes(constraint=attachment_size_constraint),
32 comment=Text(), filename=TextLine(), is_patch=Bool(),
33 content_type=TextLine(), description=Text())
34 @export_factory_operation(IBugAttachment, [])
35 def addAttachment(owner, data, comment, filename, is_patch=False,
36- content_type=None, description=None):
37+ content_type=None, description=None, from_api=False):
38 """Attach a file to this bug.
39
40 :owner: An IPerson.
41
42=== modified file 'lib/lp/bugs/model/bug.py'
43--- lib/lp/bugs/model/bug.py 2012-12-20 14:55:13 +0000
44+++ lib/lp/bugs/model/bug.py 2013-02-07 01:27:23 +0000
45@@ -1,4 +1,4 @@
46-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
47+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
48 # GNU Affero General Public License version 3 (see the file LICENSE).
49
50 """Launchpad bug-related database table classes."""
51@@ -226,6 +226,7 @@
52 )
53 from lp.services.webapp.authorization import check_permission
54 from lp.services.webapp.interfaces import ILaunchBag
55+from lp.services.webapp.publisher import get_raw_form_value_from_current_request
56
57
58 def snapshot_bug_params(bug_params):
59@@ -1250,8 +1251,13 @@
60 bug_watch.destroySelf()
61
62 def addAttachment(self, owner, data, comment, filename, is_patch=False,
63- content_type=None, description=None):
64+ content_type=None, description=None, from_api=False):
65 """See `IBug`."""
66+ # XXX: StevenK 2013-02-06 bug=1116954: We should not need to refetch
67+ # the file content from the request, since the passed in one has been
68+ # wrongly encoded.
69+ if from_api:
70+ data = get_raw_form_value_from_current_request('data')
71 if isinstance(data, str):
72 filecontent = data
73 else:
74
75=== modified file 'lib/lp/registry/interfaces/productrelease.py'
76--- lib/lp/registry/interfaces/productrelease.py 2013-01-25 05:25:34 +0000
77+++ lib/lp/registry/interfaces/productrelease.py 2013-02-07 01:27:23 +0000
78@@ -224,7 +224,7 @@
79 class IProductReleaseEditRestricted(Interface):
80 """`IProductRelease` properties which require `launchpad.Edit`."""
81
82- @call_with(uploader=REQUEST_USER)
83+ @call_with(uploader=REQUEST_USER, from_api=True)
84 @operation_parameters(
85 filename=TextLine(),
86 signature_filename=TextLine(),
87@@ -232,16 +232,14 @@
88 file_content=Bytes(constraint=productrelease_file_size_constraint),
89 signature_content=Bytes(
90 constraint=productrelease_signature_size_constraint),
91- file_type=copy_field(IProductReleaseFile['filetype'], required=False)
92- )
93- @export_factory_operation(
94- IProductReleaseFile, ['description'])
95+ file_type=copy_field(IProductReleaseFile['filetype'], required=False))
96+ @export_factory_operation(IProductReleaseFile, ['description'])
97 @export_operation_as('add_file')
98 def addReleaseFile(filename, file_content, content_type,
99 uploader, signature_filename=None,
100 signature_content=None,
101 file_type=UpstreamFileType.CODETARBALL,
102- description=None):
103+ description=None, from_api=False):
104 """Add file to the library and link to this `IProductRelease`.
105
106 The signature file will also be added if available.
107
108=== modified file 'lib/lp/registry/model/productrelease.py'
109--- lib/lp/registry/model/productrelease.py 2013-01-25 05:25:34 +0000
110+++ lib/lp/registry/model/productrelease.py 2013-02-07 01:27:23 +0000
111@@ -1,4 +1,4 @@
112-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
113+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
114 # GNU Affero General Public License version 3 (see the file LICENSE).
115
116 __metaclass__ = type
117@@ -56,6 +56,7 @@
118 )
119 from lp.services.librarian.interfaces import ILibraryFileAliasSet
120 from lp.services.propertycache import cachedproperty
121+from lp.services.webapp.publisher import get_raw_form_value_from_current_request
122
123
124 class ProductRelease(SQLBase):
125@@ -148,7 +149,7 @@
126 uploader, signature_filename=None,
127 signature_content=None,
128 file_type=UpstreamFileType.CODETARBALL,
129- description=None):
130+ description=None, from_api=False):
131 """See `IProductRelease`."""
132 if not self.can_have_release_files:
133 raise ProprietaryProduct(
134@@ -157,31 +158,35 @@
135 raise InvalidFilename
136 # Create the alias for the file.
137 filename = self.normalizeFilename(filename)
138+ # XXX: StevenK 2013-02-06 bug=1116954: We should not need to refetch
139+ # the file content from the request, since the passed in one has been
140+ # wrongly encoded.
141+ if from_api:
142+ file_content = get_raw_form_value_from_current_request(
143+ 'file_content')
144 file_obj, file_size = self._getFileObjectAndSize(file_content)
145
146 alias = getUtility(ILibraryFileAliasSet).create(
147- name=filename,
148- size=file_size,
149- file=file_obj,
150+ name=filename, size=file_size, file=file_obj,
151 contentType=content_type)
152 if signature_filename is not None and signature_content is not None:
153+ # XXX: StevenK 2013-02-06 bug=1116954: We should not need to
154+ # refetch the file content from the request, since the passed in
155+ # one has been wrongly encoded.
156+ if from_api:
157+ signature_content = get_raw_form_value_from_current_request(
158+ 'signature_content')
159 signature_obj, signature_size = self._getFileObjectAndSize(
160 signature_content)
161- signature_filename = self.normalizeFilename(
162- signature_filename)
163+ signature_filename = self.normalizeFilename(signature_filename)
164 signature_alias = getUtility(ILibraryFileAliasSet).create(
165- name=signature_filename,
166- size=signature_size,
167- file=signature_obj,
168- contentType='application/pgp-signature')
169+ name=signature_filename, size=signature_size,
170+ file=signature_obj, contentType='application/pgp-signature')
171 else:
172 signature_alias = None
173- return ProductReleaseFile(productrelease=self,
174- libraryfile=alias,
175- signature=signature_alias,
176- filetype=file_type,
177- description=description,
178- uploader=uploader)
179+ return ProductReleaseFile(
180+ productrelease=self, libraryfile=alias, signature=signature_alias,
181+ filetype=file_type, description=description, uploader=uploader)
182
183 def getFileAliasByName(self, name):
184 """See `IProductRelease`."""
185
186=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
187--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2012-12-10 13:43:47 +0000
188+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2013-02-07 01:27:23 +0000
189@@ -1020,8 +1020,8 @@
190
191 >>> pr_url = '/firefox/1.0/1.0.0'
192 >>> ff_100 = webservice.get(pr_url).jsonBody()
193- >>> file_content="first attachment file content"
194- >>> sig_file_content="hash hash hash"
195+ >>> file_content="first attachment file content \xff"
196+ >>> sig_file_content="hash hash hash \xff"
197 >>> response = webservice.named_post(ff_100['self_link'], 'add_file',
198 ... filename='filename.txt',
199 ... file_content=file_content,
200@@ -1043,6 +1043,20 @@
201 >>> print_self_link_of_entries(ff_100_files)
202 http://.../firefox/1.0/1.0.0/+file/filename.txt
203
204+And it has been uploaded correctly.
205+
206+ >>> from zope.component import getUtility
207+ >>> from lp.registry.interfaces.product import IProductSet
208+ >>> from lp.testing import login, logout
209+ >>> login('bac@canonical.com')
210+ >>> concrete_one_zero = getUtility(IProductSet)['firefox'].getRelease(
211+ ... '1.0.0')
212+ >>> concrete_one_zero.files[0].libraryfile.read() == file_content
213+ True
214+ >>> concrete_one_zero.files[0].signature.read() == sig_file_content
215+ True
216+ >>> logout()
217+
218 The file type and description are optional. If no signature is
219 available then it must be explicitly set to None.
220
221@@ -1106,12 +1120,8 @@
222 If a project has a commercial-use subscription then it can be retrieved
223 through the API.
224
225- >>> from zope.component import getUtility
226- >>> from lp.registry.interfaces.product import IProductSet
227- >>> from lp.testing import login, logout
228 >>> login('bac@canonical.com')
229- >>> product_set = getUtility(IProductSet)
230- >>> mmm = product_set.getByName('mega-money-maker')
231+ >>> mmm = getUtility(IProductSet)['mega-money-maker']
232 >>> print mmm.commercial_subscription
233 None
234
235
236=== modified file 'lib/lp/registry/subscribers.py'
237--- lib/lp/registry/subscribers.py 2012-11-26 08:40:20 +0000
238+++ lib/lp/registry/subscribers.py 2013-02-07 01:27:23 +0000
239@@ -11,6 +11,7 @@
240 from datetime import datetime
241 import textwrap
242
243+from lazr.restful.utils import get_current_browser_request
244 import pytz
245 from zope.security.proxy import removeSecurityProxy
246
247@@ -23,10 +24,7 @@
248 simple_sendmail,
249 )
250 from lp.services.webapp.escaping import structured
251-from lp.services.webapp.publisher import (
252- canonical_url,
253- get_current_browser_request,
254- )
255+from lp.services.webapp.publisher import canonical_url
256
257
258 def product_licenses_modified(product, event):
259
260=== modified file 'lib/lp/registry/tests/test_subscribers.py'
261--- lib/lp/registry/tests/test_subscribers.py 2012-12-10 13:43:47 +0000
262+++ lib/lp/registry/tests/test_subscribers.py 2013-02-07 01:27:23 +0000
263@@ -7,6 +7,7 @@
264
265 from datetime import datetime
266
267+from lazr.restful.utils import get_current_browser_request
268 import pytz
269 from zope.component import getUtility
270 from zope.security.proxy import removeSecurityProxy
271@@ -23,7 +24,6 @@
272 product_licenses_modified,
273 )
274 from lp.services.webapp.escaping import html_escape
275-from lp.services.webapp.publisher import get_current_browser_request
276 from lp.testing import (
277 login_person,
278 logout,
279
280=== modified file 'lib/lp/services/webapp/escaping.py'
281--- lib/lp/services/webapp/escaping.py 2012-12-10 22:25:44 +0000
282+++ lib/lp/services/webapp/escaping.py 2013-02-07 01:27:23 +0000
283@@ -8,6 +8,7 @@
284 'structured',
285 ]
286
287+from lazr.restful.utils import get_current_browser_request
288 from zope.i18n import (
289 Message,
290 translate,
291@@ -15,7 +16,6 @@
292 from zope.interface import implements
293
294 from lp.services.webapp.interfaces import IStructuredString
295-from lp.services.webapp.publisher import get_current_browser_request
296
297
298 HTML_REPLACEMENTS = (
299
300=== modified file 'lib/lp/services/webapp/menu.py'
301--- lib/lp/services/webapp/menu.py 2012-12-12 04:59:52 +0000
302+++ lib/lp/services/webapp/menu.py 2013-02-07 01:27:23 +0000
303@@ -21,6 +21,7 @@
304 import types
305
306 from lazr.delegates import delegates
307+from lazr.restful.utils import get_current_browser_request
308 from lazr.uri import (
309 InvalidURIError,
310 URI,
311@@ -45,7 +46,6 @@
312 )
313 from lp.services.webapp.publisher import (
314 canonical_url,
315- get_current_browser_request,
316 LaunchpadView,
317 UserAttributeCache,
318 )
319
320=== modified file 'lib/lp/services/webapp/pgsession.py'
321--- lib/lp/services/webapp/pgsession.py 2012-01-01 03:00:09 +0000
322+++ lib/lp/services/webapp/pgsession.py 2013-02-07 01:27:23 +0000
323@@ -9,6 +9,7 @@
324 import time
325 from UserDict import DictMixin
326
327+from lazr.restful.utils import get_current_browser_request
328 from storm.zope.interfaces import IZStorm
329 from zope.app.security.interfaces import IUnauthenticatedPrincipal
330 from zope.component import getUtility
331@@ -21,7 +22,6 @@
332 )
333
334 from lp.services.helpers import ensure_unicode
335-from lp.services.webapp.publisher import get_current_browser_request
336
337
338 SECONDS = 1
339
340=== modified file 'lib/lp/services/webapp/publisher.py'
341--- lib/lp/services/webapp/publisher.py 2012-11-26 18:44:34 +0000
342+++ lib/lp/services/webapp/publisher.py 2013-02-07 01:27:23 +0000
343@@ -1,4 +1,4 @@
344-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
345+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
346 # GNU Affero General Public License version 3 (see the file LICENSE).
347
348 """Publisher of objects as web pages.
349@@ -8,13 +8,13 @@
350 __metaclass__ = type
351 __all__ = [
352 'DataDownloadView',
353+ 'get_raw_form_value_from_current_request',
354 'LaunchpadContainer',
355 'LaunchpadView',
356 'LaunchpadXMLRPCView',
357 'canonical_name',
358 'canonical_url',
359 'canonical_url_iterator',
360- 'get_current_browser_request',
361 'nearest',
362 'Navigation',
363 'rootObject',
364@@ -26,6 +26,7 @@
365 'UserAttributeCache',
366 ]
367
368+from cgi import FieldStorage
369 import httplib
370 import re
371
372@@ -800,6 +801,23 @@
373 return None
374
375
376+def get_raw_form_value_from_current_request(field_name):
377+ # XXX: StevenK 2013-02-06 bug=1116954: We should not need to refetch
378+ # the file content from the request, since the passed in one has been
379+ # wrongly encoded.
380+ # Circular imports.
381+ from lp.services.webapp.servers import WebServiceClientRequest
382+ request = get_current_browser_request()
383+ assert isinstance(request, WebServiceClientRequest)
384+ # Zope wrongly encodes any form element that doesn't look like a file,
385+ # so re-fetch the file content if it has been encoded.
386+ if request and request.form.has_key(field_name) and isinstance(
387+ request.form[field_name], unicode):
388+ request._environ['wsgi.input'].seek(0)
389+ fs = FieldStorage(fp=request._body_instream, environ=request._environ)
390+ return fs[field_name].value
391+
392+
393 class RootObject:
394 implements(ILaunchpadApplication, ILaunchpadRoot)
395
396
397=== modified file 'lib/lp/services/webapp/servers.py'
398--- lib/lp/services/webapp/servers.py 2013-02-03 01:49:37 +0000
399+++ lib/lp/services/webapp/servers.py 2013-02-07 01:27:23 +0000
400@@ -1,4 +1,4 @@
401-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
402+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
403 # GNU Affero General Public License version 3 (see the file LICENSE).
404
405 """Definition of the internet servers that Launchpad uses."""
406@@ -18,6 +18,7 @@
407 WebServicePublicationMixin,
408 WebServiceRequestTraversal,
409 )
410+from lazr.restful.utils import get_current_browser_request
411 from lazr.uri import URI
412 import transaction
413 from transaction.interfaces import ISynchronizer
414@@ -100,10 +101,7 @@
415 )
416 from lp.services.webapp.opstats import OpStats
417 from lp.services.webapp.publication import LaunchpadBrowserPublication
418-from lp.services.webapp.publisher import (
419- get_current_browser_request,
420- RedirectionView,
421- )
422+from lp.services.webapp.publisher import RedirectionView
423 from lp.services.webapp.vhosts import allvhosts
424 from lp.services.webservice.interfaces import IWebServiceApplication
425 from lp.testopenid.interfaces.server import ITestOpenIDApplication
426@@ -657,6 +655,15 @@
427 """As per zope.publisher.browser.BrowserRequest._createResponse"""
428 return LaunchpadBrowserResponse()
429
430+ def _decode(self, text):
431+ text = super(LaunchpadBrowserRequest, self)._decode(text)
432+ if isinstance(text, str):
433+ # BrowserRequest._decode failed to do so with the user-specified
434+ # charsets, so decode as UTF-8 with replacements, since we always
435+ # want unicode.
436+ text = unicode(text, 'utf-8', 'replace')
437+ return text
438+
439 @cachedproperty
440 def form_ng(self):
441 """See ILaunchpadBrowserApplicationRequest."""
442
443=== modified file 'lib/lp/services/webapp/tests/test_menu.py'
444--- lib/lp/services/webapp/tests/test_menu.py 2012-01-01 02:58:52 +0000
445+++ lib/lp/services/webapp/tests/test_menu.py 2013-02-07 01:27:23 +0000
446@@ -3,6 +3,7 @@
447
448 __metaclass__ = type
449
450+from lazr.restful.utils import get_current_browser_request
451 from zope.security.management import newInteraction
452
453 from lp.services.webapp.menu import (
454@@ -10,7 +11,6 @@
455 MENU_ANNOTATION_KEY,
456 MenuBase,
457 )
458-from lp.services.webapp.publisher import get_current_browser_request
459 from lp.testing import (
460 ANONYMOUS,
461 login,
462@@ -77,7 +77,7 @@
463 login(ANONYMOUS)
464 context = object()
465 menu = TestMenu(context)
466- link = menu._get_link('test_link')
467+ menu._get_link('test_link')
468 cache = get_current_browser_request().annotations.get(
469 MENU_ANNOTATION_KEY)
470 self.assertEquals(len(cache.keys()), 1)
471
472=== modified file 'lib/lp/services/webapp/tests/test_servers.py'
473--- lib/lp/services/webapp/tests/test_servers.py 2013-02-03 01:49:37 +0000
474+++ lib/lp/services/webapp/tests/test_servers.py 2013-02-07 01:27:23 +0000
475@@ -1,4 +1,4 @@
476-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
477+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
478 # GNU Affero General Public License version 3 (see the file LICENSE).
479
480 __metaclass__ = type
481@@ -50,6 +50,7 @@
482 EventRecorder,
483 TestCase,
484 )
485+from lp.testing.layers import FunctionalLayer
486
487
488 class SetInWSGIEnvironmentTestCase(TestCase):
489@@ -230,8 +231,7 @@
490
491 for method in denied_methods:
492 env = self.wsgi_env(self.non_api_path, method)
493- self.assert_(self.factory.canHandle(env),
494- "Sanity check")
495+ self.assert_(self.factory.canHandle(env), "Sanity check")
496 # Returns a tuple of (request_factory, publication_factory).
497 rfactory, pfactory = self.factory.checkRequest(env)
498 self.assert_(rfactory is not None,
499@@ -352,6 +352,8 @@
500 class TestBasicLaunchpadRequest(TestCase):
501 """Tests for the base request class"""
502
503+ layer = FunctionalLayer
504+
505 def test_baserequest_response_should_vary(self):
506 """Test that our base response has a proper vary header."""
507 request = LaunchpadBrowserRequest(StringIO.StringIO(''), {})
508@@ -387,6 +389,16 @@
509 request = LaunchpadBrowserRequest(StringIO.StringIO(''), env)
510 self.assertEquals(u'fnord/trunk\ufffd', request.getHeader('PATH_INFO'))
511
512+ def test_request_with_invalid_query_string_recovers(self):
513+ # When the query string has invalid utf-8, it is decoded with
514+ # replacement.
515+ env = {'QUERY_STRING': 'field.title=subproc\xe9s '}
516+ request = LaunchpadBrowserRequest(StringIO.StringIO(''), env)
517+ # XXX: Python 2.6 and 2.7 handle unicode replacement differently.
518+ self.assertIn(
519+ request.query_string_params['field.title'],
520+ ([u'subproc\ufffd'], [u'subproc\ufffds ']))
521+
522
523 class TestFeedsBrowserRequest(TestCase):
524 """Tests for `FeedsBrowserRequest`."""
525
526=== modified file 'lib/lp/services/webapp/tests/test_user_requested_oops.py'
527--- lib/lp/services/webapp/tests/test_user_requested_oops.py 2012-03-06 23:39:08 +0000
528+++ lib/lp/services/webapp/tests/test_user_requested_oops.py 2013-02-07 01:27:23 +0000
529@@ -1,4 +1,5 @@
530-# Copyright 2009 Canonical Ltd. All rights reserved.
531+# Copyright 2009 Canonical Ltd. This software is licensed under the
532+# GNU Affero General Public License version 3 (see the file LICENSE).
533
534 """Tests for the user requested oops using ++oops++ traversal."""
535
536
537=== modified file 'lib/lp/services/webapp/tests/test_view_model.py'
538--- lib/lp/services/webapp/tests/test_view_model.py 2012-01-01 02:58:52 +0000
539+++ lib/lp/services/webapp/tests/test_view_model.py 2013-02-07 01:27:23 +0000
540@@ -1,4 +1,5 @@
541-# Copyright 2011 Canonical Ltd. All rights reserved.
542+# Copyright 2011 Canonical Ltd. This software is licensed under the
543+# GNU Affero General Public License version 3 (see the file LICENSE).
544
545 """Tests for the user requested oops using ++oops++ traversal."""
546
547
548=== modified file 'lib/lp/testing/tests/test_publication.py'
549--- lib/lp/testing/tests/test_publication.py 2012-04-12 06:06:49 +0000
550+++ lib/lp/testing/tests/test_publication.py 2013-02-07 01:27:23 +0000
551@@ -6,6 +6,7 @@
552 __metaclass__ = type
553
554 from lazr.restful import EntryResource
555+from lazr.restful.utils import get_current_browser_request
556 from zope.app.pagetemplate.simpleviewclass import simple
557 from zope.component import (
558 getSiteManager,
559@@ -24,7 +25,6 @@
560 ILaunchpadRoot,
561 )
562 from lp.services.webapp.publisher import (
563- get_current_browser_request,
564 Navigation,
565 stepthrough,
566 )