From d643b3bd93d5abd21bb2a1c0f0b6ce11c2004a01 Mon Sep 17 00:00:00 2001 From: Philip Bauer Date: Mon, 2 Apr 2018 23:57:24 +0200 Subject: [PATCH 1/9] force cookies to be bytes to prevent wrapping bytes as text in py3 (e.g. "b'somevalue'") --- src/ZPublisher/HTTPResponse.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ZPublisher/HTTPResponse.py b/src/ZPublisher/HTTPResponse.py index c248b87f7f..49443a9454 100644 --- a/src/ZPublisher/HTTPResponse.py +++ b/src/ZPublisher/HTTPResponse.py @@ -265,8 +265,10 @@ def setCookie(self, name, value, quoted=True, **kw): This value overwrites any previously set value for the cookie in the Response object. """ - name = str(name) - value = str(value) + if isinstance(name, text_type): + name = name.encode(self.charset) + if isinstance(value, text_type): + value = value.encode(self.charset) cookies = self.cookies if name in cookies: @@ -286,8 +288,10 @@ def appendCookie(self, name, value): cookie has previously been set in the response object, the new value is appended to the old one separated by a colon. """ - name = str(name) - value = str(value) + if isinstance(name, text_type): + name = name.encode(self.charset) + if isinstance(value, text_type): + value = value.encode(self.charset) cookies = self.cookies if name in cookies: @@ -310,7 +314,8 @@ def expireCookie(self, name, **kw): when creating the cookie. The path can be specified as a keyword argument. """ - name = str(name) + if isinstance(name, text_type): + name = name.encode(self.charset) d = kw.copy() if 'value' in d: From 47e5ae04760572881f414f2ad9d943dd873b9a86 Mon Sep 17 00:00:00 2001 From: Philip Bauer Date: Tue, 3 Apr 2018 00:22:55 +0200 Subject: [PATCH 2/9] do not turn cookie key to bytes in py3 --- src/ZPublisher/HTTPResponse.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ZPublisher/HTTPResponse.py b/src/ZPublisher/HTTPResponse.py index 49443a9454..83df63e0a2 100644 --- a/src/ZPublisher/HTTPResponse.py +++ b/src/ZPublisher/HTTPResponse.py @@ -265,7 +265,7 @@ def setCookie(self, name, value, quoted=True, **kw): This value overwrites any previously set value for the cookie in the Response object. """ - if isinstance(name, text_type): + if PY2 and isinstance(name, text_type): name = name.encode(self.charset) if isinstance(value, text_type): value = value.encode(self.charset) @@ -288,7 +288,7 @@ def appendCookie(self, name, value): cookie has previously been set in the response object, the new value is appended to the old one separated by a colon. """ - if isinstance(name, text_type): + if PY2 and isinstance(name, text_type): name = name.encode(self.charset) if isinstance(value, text_type): value = value.encode(self.charset) @@ -314,7 +314,7 @@ def expireCookie(self, name, **kw): when creating the cookie. The path can be specified as a keyword argument. """ - if isinstance(name, text_type): + if PY2 and isinstance(name, text_type): name = name.encode(self.charset) d = kw.copy() From f7aff2f499fe474bbacd376c054b2abe079c9162 Mon Sep 17 00:00:00 2001 From: Steffen Allner Date: Wed, 16 May 2018 14:33:53 +0200 Subject: [PATCH 3/9] Accept bytes and text as cookie values. A cookie value is finally quoted before sending, which needs to be `str` in Python 2 and can be bytes or text in Python 3. In Python 3, a conversion would result in a string representation of bytes. --- CHANGES.rst | 3 +++ src/ZPublisher/HTTPResponse.py | 30 ++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d680a15fe6..482f37859e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,9 @@ https://github.com/zopefoundation/Zope/blob/4.0a6/CHANGES.rst - Use log.warning to avoid deprecation warning for log.warn +- Accept bytes and text as cookie value. + (`#263 `_) + 4.0b4 (2018-04-23) ------------------ diff --git a/src/ZPublisher/HTTPResponse.py b/src/ZPublisher/HTTPResponse.py index 83df63e0a2..d9ad5a378f 100644 --- a/src/ZPublisher/HTTPResponse.py +++ b/src/ZPublisher/HTTPResponse.py @@ -264,11 +264,15 @@ def setCookie(self, name, value, quoted=True, **kw): This value overwrites any previously set value for the cookie in the Response object. + + `name` has to be text in Python 3. + + `value` may be text or bytes. The default encoding of respective python + version is used. """ - if PY2 and isinstance(name, text_type): - name = name.encode(self.charset) - if isinstance(value, text_type): - value = value.encode(self.charset) + if PY2: + name = str(name) + value = str(value) cookies = self.cookies if name in cookies: @@ -287,11 +291,15 @@ def appendCookie(self, name, value): browsers with a key "name" and value "value". If a value for the cookie has previously been set in the response object, the new value is appended to the old one separated by a colon. + + `name` has to be text in Python 3. + + `value` may be text or bytes. The default encoding of respective python + version is used. """ - if PY2 and isinstance(name, text_type): - name = name.encode(self.charset) - if isinstance(value, text_type): - value = value.encode(self.charset) + if PY2: + name = str(name) + value = str(value) cookies = self.cookies if name in cookies: @@ -313,9 +321,11 @@ def expireCookie(self, name, **kw): to be specified - this path must exactly match the path given when creating the cookie. The path can be specified as a keyword argument. + + `name` has to be text in Python 3. """ - if PY2 and isinstance(name, text_type): - name = name.encode(self.charset) + if PY2: + name = str(name) d = kw.copy() if 'value' in d: From 6c508b9cec2b08753a8f04bcf1d3936fd99303dd Mon Sep 17 00:00:00 2001 From: Steffen Allner Date: Wed, 16 May 2018 14:55:43 +0200 Subject: [PATCH 4/9] Add tests for byte and text handling. --- src/ZPublisher/tests/testHTTPResponse.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/ZPublisher/tests/testHTTPResponse.py b/src/ZPublisher/tests/testHTTPResponse.py index fea6e44eac..a3de8f8992 100644 --- a/src/ZPublisher/tests/testHTTPResponse.py +++ b/src/ZPublisher/tests/testHTTPResponse.py @@ -340,6 +340,28 @@ def test_setCookie_unquoted(self): self.assertEqual(len(cookie_list), 1) self.assertEqual(cookie_list[0], ('Set-Cookie', 'foo=bar')) + def test_setCookie_handle_byte_values(self): + response = self._makeOne() + response.setCookie('foo', b'bar') + cookie = response.cookies.get('foo', None) + self.assertEqual(len(cookie), 2) + self.assertEqual(cookie.get('value'), b'bar') + + cookie_list = response._cookie_list() + self.assertEqual(len(cookie_list), 1) + self.assertEqual(cookie_list[0], ('Set-Cookie', 'foo="bar"')) + + def test_setCookie_handle_unicode_values(self): + response = self._makeOne() + response.setCookie('foo', u'bar') + cookie = response.cookies.get('foo', None) + self.assertEqual(len(cookie), 2) + self.assertEqual(cookie.get('value'), u'bar') + + cookie_list = response._cookie_list() + self.assertEqual(len(cookie_list), 1) + self.assertEqual(cookie_list[0], ('Set-Cookie', 'foo="bar"')) + def test_appendCookie_w_existing(self): response = self._makeOne() response.setCookie('foo', 'bar', path='/') From 016a3f1d66a39d7a847d971ae926681815ccee1d Mon Sep 17 00:00:00 2001 From: Robert Buchholz Date: Wed, 16 May 2018 16:55:10 +0200 Subject: [PATCH 5/9] Re-add example section for how to use TemporaryFolder --- src/Zope2/utilities/skel/etc/wsgi.conf.in | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Zope2/utilities/skel/etc/wsgi.conf.in b/src/Zope2/utilities/skel/etc/wsgi.conf.in index 80b97a7d0f..1d22603622 100644 --- a/src/Zope2/utilities/skel/etc/wsgi.conf.in +++ b/src/Zope2/utilities/skel/etc/wsgi.conf.in @@ -8,3 +8,12 @@ instancehome $INSTANCE mount-point / + +# Uncomment this if you use Products.Sessions and Products.TemporaryFolder +# +# +# name Temporary database (for sessions) +# +# mount-point /temp_folder +# container-class Products.TemporaryFolder.TemporaryContainer +# From bba6eee800041d54b7ac2fe6414d889f328e0d18 Mon Sep 17 00:00:00 2001 From: Robert Buchholz Date: Wed, 16 May 2018 16:56:54 +0200 Subject: [PATCH 6/9] Hand Application object to Products through ProductContext Products receive the ProductContext as the first parameter to their initialize() method when they are loaded. This context used to contain a reference to the app until it was removed during Zope 4 preparations. The Application was, however, never exposed through an API. Access to the API is necessary for certain products that automatically add objects to the Application when they are loaded, such as the Products.Sessions and Products.TemporaryFolder. This used to happen in OFS.Application in Zope 2, but was moved out when the products became indepentent. --- src/App/ProductContext.py | 4 ++++ src/OFS/Application.py | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/App/ProductContext.py b/src/App/ProductContext.py index 9d3082b665..6a9745b4f8 100644 --- a/src/App/ProductContext.py +++ b/src/App/ProductContext.py @@ -44,6 +44,7 @@ class ProductContext(object): def __init__(self, product, app, package): self.__prod = product + self.__app = app self.__pack = package def registerClass(self, instance_class=None, meta_type='', @@ -213,6 +214,9 @@ class DummyHelp(object): lastRegistered = None return DummyHelp() + def getApplication(self): + return self.__app + class AttrDict(object): diff --git a/src/OFS/Application.py b/src/OFS/Application.py index 82b05fee98..6571288872 100644 --- a/src/OFS/Application.py +++ b/src/OFS/Application.py @@ -299,7 +299,7 @@ def install_root_view(self): self.commit(u'Added default view for root object') def install_products(self): - return install_products() + return install_products(self.getApp()) def install_standards(self): app = self.getApp() @@ -424,7 +424,7 @@ def install_product(app, product_dir, product_name, meta_types, setattr(Application.misc_, product_name, misc_) productObject = FactoryDispatcher.Product(product_name) - context = ProductContext(productObject, None, product) + context = ProductContext(productObject, app, product) # Look for an 'initialize' method in the product. initmethod = pgetattr(product, 'initialize', None) @@ -439,7 +439,7 @@ def install_package(app, module, init_func, raise_exc=None): product.package_name = name if init_func is not None: - newContext = ProductContext(product, None, module) + newContext = ProductContext(product, app, module) init_func(newContext) package_initialized(module, init_func) From b14d6c100f629fa80a11007ffde38ba39b13162c Mon Sep 17 00:00:00 2001 From: Robert Buchholz Date: Thu, 17 May 2018 10:50:49 +0200 Subject: [PATCH 7/9] Add unit test for application initialization in product --- src/OFS/tests/applicationproduct/__init__.py | 13 ++++++++++++ src/OFS/tests/testAppInitializer.py | 21 ++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/OFS/tests/applicationproduct/__init__.py diff --git a/src/OFS/tests/applicationproduct/__init__.py b/src/OFS/tests/applicationproduct/__init__.py new file mode 100644 index 0000000000..d7b5823d88 --- /dev/null +++ b/src/OFS/tests/applicationproduct/__init__.py @@ -0,0 +1,13 @@ +""" This product uses the application during product initialization + to create a subfolder in the root folder. This is similar to what + Producs.Sessions and Products.TemporaryFolder are doing. +""" + +def initialize(context): + from OFS.Folder import Folder + import transaction + + app = context.getApplication() + folder = Folder('some_folder') + app._setObject('some_folder', folder) + transaction.commit() diff --git a/src/OFS/tests/testAppInitializer.py b/src/OFS/tests/testAppInitializer.py index 10c626730c..081094706f 100644 --- a/src/OFS/tests/testAppInitializer.py +++ b/src/OFS/tests/testAppInitializer.py @@ -124,3 +124,24 @@ def test_install_root_view(self): app = i.getApp() self.assertTrue('index_html' in app) self.assertEqual(app.index_html.meta_type, 'Page Template') + + def test_install_products_which_need_the_application(self): + self.configure(good_cfg) + from Zope2.App import zcml + configure_zcml = ''' + + + + ''' + zcml.load_string(configure_zcml) + + i = self.getOne() + i.install_products() + app = i.getApp() + self.assertEqual(app.some_folder.meta_type, 'Folder') From c4f8c1ba63b93ba05c3260122bce75091c861409 Mon Sep 17 00:00:00 2001 From: Robert Buchholz Date: Thu, 17 May 2018 13:02:16 +0200 Subject: [PATCH 8/9] Add changelog entry --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6c186e886e..3566009c32 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,15 @@ https://github.com/zopefoundation/Zope/blob/4.0a6/CHANGES.rst 4.0b5 (unreleased) ------------------ +New features +++++++++++++ + +- The `ProductContext` handed to a product's `initialize()` method + now has a `getApplication()` method which a product can use to, + e.g., add an object to the Application during startup (as used + by `Products.Sessions`). + (`#277 `_) + Bugfixes ++++++++ From 89ba0e1058789b4667a1db29192ac94cc7ed5f41 Mon Sep 17 00:00:00 2001 From: Robert Buchholz Date: Thu, 17 May 2018 15:18:57 +0200 Subject: [PATCH 9/9] Use native strings for ZODB mount points in Python 2 ZODB requires paths and identifiers to be native strings (unicode on Py3, bytes on Py2). Since the ZConfig parser decodes the config file, keys and valus in the will consistently be unicode. As a result, the ZopeDatabase object needs to encode the value back to str/bytes on Python 2. --- src/Zope2/Startup/datatypes.py | 3 + src/Zope2/Startup/tests/test_datatypes.py | 70 +++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/Zope2/Startup/tests/test_datatypes.py diff --git a/src/Zope2/Startup/datatypes.py b/src/Zope2/Startup/datatypes.py index 3a8b819cc0..9a7abce34a 100644 --- a/src/Zope2/Startup/datatypes.py +++ b/src/Zope2/Startup/datatypes.py @@ -18,6 +18,7 @@ import traceback from six.moves import UserDict +from six import PY2, text_type from ZODB.config import ZODBDatabase from zope.deferredimport import deprecated @@ -165,6 +166,8 @@ def getName(self): def computeMountPaths(self): mps = [] for part in self.config.mount_points: + if PY2 and isinstance(part, text_type): + part = part.encode() real_root = None if ':' in part: # 'virtual_path:real_path' diff --git a/src/Zope2/Startup/tests/test_datatypes.py b/src/Zope2/Startup/tests/test_datatypes.py new file mode 100644 index 0000000000..191bc420db --- /dev/null +++ b/src/Zope2/Startup/tests/test_datatypes.py @@ -0,0 +1,70 @@ +############################################################################## +# +# Copyright (c) 2003 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## + +import io +import shutil +import tempfile +import unittest + +import ZConfig + +from Zope2.Startup.options import ZopeWSGIOptions + +_SCHEMA = None + + +def getSchema(): + global _SCHEMA + if _SCHEMA is None: + opts = ZopeWSGIOptions() + opts.load_schema() + _SCHEMA = opts.schema + return _SCHEMA + + +class ZopeDatabaseTestCase(unittest.TestCase): + + def setUp(self): + self.TEMPNAME = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.TEMPNAME) + + def load_config_text(self, text): + # We have to create a directory of our own since the existence + # of the directory is checked. This handles this in a + # platform-independent way. + text = text.replace("<>", self.TEMPNAME) + sio = io.StringIO(text) + + conf, self.handler = ZConfig.loadConfigFile(getSchema(), sio) + self.assertEqual(conf.instancehome, self.TEMPNAME) + return conf + + def test_parse_mount_points_as_native_strings(self): + conf = self.load_config_text(u""" + instancehome <> + + mount-point /test + + name mappingstorage + + + """) + db = conf.databases[0] + self.assertEqual(u'main', db.name) + virtual_path = db.getVirtualMountPaths()[0] + self.assertEqual('/test', virtual_path) + self.assertIsInstance(virtual_path, str) + self.assertEqual([('/test', None, '/test')], db.computeMountPaths())