From d63fcaa5f05e2da4d52624fd58df822353e1e029 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Tue, 12 Oct 2021 20:11:03 +0200 Subject: [PATCH 01/95] fix typo after refactoring in docs (#123) * fix typo after refactoring in docs * add changelog entry --- README.rst | 2 +- news/123.bugfix | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/123.bugfix diff --git a/README.rst b/README.rst index 4b91180f..ada524ac 100644 --- a/README.rst +++ b/README.rst @@ -212,7 +212,7 @@ stores all path segments in an array in `self.params`. from zope.interface import implementer from zope.publisher.interfaces import IPublishTraverse - @implementer(implementer) + @implementer(IPublishTraverse) class MyService(Service): def __init__(self, context, request): diff --git a/news/123.bugfix b/news/123.bugfix new file mode 100644 index 00000000..ee50058f --- /dev/null +++ b/news/123.bugfix @@ -0,0 +1 @@ +Fix typo in `README.rst` [jensens] From ad94518b844f8dafea40b6f8afd24bddf39243a4 Mon Sep 17 00:00:00 2001 From: Peter Holzer Date: Fri, 12 Nov 2021 16:06:04 +0100 Subject: [PATCH 02/95] fix test to use document_view as default view for site root --- src/plone/rest/tests/test_traversal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/rest/tests/test_traversal.py b/src/plone/rest/tests/test_traversal.py index dc67591e..c260e76b 100644 --- a/src/plone/rest/tests/test_traversal.py +++ b/src/plone/rest/tests/test_traversal.py @@ -60,7 +60,7 @@ def test_json_request_on_content_object_returns_service(self): def test_html_request_on_portal_root_returns_default_view(self): obj = self.traverse(accept="text/html") - self.assertEquals("listing_view", obj.__name__) + self.assertEquals("document_view", obj.__name__) def test_html_request_on_portal_root_returns_dynamic_view(self): self.portal.setLayout("summary_view") From fce405356094f9d7b1f0af3e90137cc847bd5862 Mon Sep 17 00:00:00 2001 From: Peter Holzer Date: Fri, 12 Nov 2021 16:08:08 +0100 Subject: [PATCH 03/95] add news --- news/126.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/126.bugfix diff --git a/news/126.bugfix b/news/126.bugfix new file mode 100644 index 00000000..f1bff43d --- /dev/null +++ b/news/126.bugfix @@ -0,0 +1,2 @@ +Use document_view as default for site root. +[agitator] From 8047c393bcca418f4c421a08291973e008b8d6d3 Mon Sep 17 00:00:00 2001 From: Peter Holzer Date: Mon, 15 Nov 2021 10:19:18 +0100 Subject: [PATCH 04/95] fix test for Plone < 6 --- src/plone/rest/tests/test_traversal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/rest/tests/test_traversal.py b/src/plone/rest/tests/test_traversal.py index c260e76b..c3578fbf 100644 --- a/src/plone/rest/tests/test_traversal.py +++ b/src/plone/rest/tests/test_traversal.py @@ -60,7 +60,7 @@ def test_json_request_on_content_object_returns_service(self): def test_html_request_on_portal_root_returns_default_view(self): obj = self.traverse(accept="text/html") - self.assertEquals("document_view", obj.__name__) + self.assertEquals(self.portal.getDefaultLayout(), obj.__name__) def test_html_request_on_portal_root_returns_dynamic_view(self): self.portal.setLayout("summary_view") From ca4ed7d3d18e51f83ba38d1869e88d24692d048f Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sun, 26 Dec 2021 21:46:55 +0100 Subject: [PATCH 05/95] pyparsing = 2.4.5 (#129) --- versions.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/versions.cfg b/versions.cfg index e1a206c6..d47c243e 100644 --- a/versions.cfg +++ b/versions.cfg @@ -51,3 +51,4 @@ pyrsistent = 0.15.7 Click = 7.1.2 httpie = 1.0.3 check-manifest = 0.41 +pyparsing = 2.4.5 From 70e1ec77bb3b19eb308e0c92406687b4851999db Mon Sep 17 00:00:00 2001 From: Ross Patterson Date: Sun, 26 Dec 2021 23:10:40 -0800 Subject: [PATCH 06/95] test(deprecation): Fix some warnings from our code (#128) Resolve all the deprecation warnings that originate in this package's code that are exposed by running the tests that do not stem from backwards compatibility we still support. IOW, warnings are still emitted that stem from code that needs to work with older versions we still support and cannot be updated without breaking that backwards compatibility. It seems better to me to leave the warnings in place than litter our code with BBB conditionals. --- news/128.bugfix | 3 +++ src/plone/rest/tests/test_redirects.py | 2 +- src/plone/rest/tests/test_traversal.py | 8 ++++---- 3 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 news/128.bugfix diff --git a/news/128.bugfix b/news/128.bugfix new file mode 100644 index 00000000..e8c7ff4a --- /dev/null +++ b/news/128.bugfix @@ -0,0 +1,3 @@ +Resolve all the deprecation warnings that originate in this package's code that are +exposed by running the tests that do not stem from backwards compatibility we support. +[rpatterson] diff --git a/src/plone/rest/tests/test_redirects.py b/src/plone/rest/tests/test_redirects.py index 37c61dca..2dcafc1d 100644 --- a/src/plone/rest/tests/test_redirects.py +++ b/src/plone/rest/tests/test_redirects.py @@ -176,4 +176,4 @@ def test_aborts_redirect_checks_early_for_app_root(self): def test_gracefully_deals_with_missing_request_url(self): error_view = ErrorHandling(self.portal, self.portal.REQUEST) self.portal.REQUEST["ACTUAL_URL"] = None - self.assertEquals(False, error_view.attempt_redirect()) + self.assertEqual(False, error_view.attempt_redirect()) diff --git a/src/plone/rest/tests/test_traversal.py b/src/plone/rest/tests/test_traversal.py index c3578fbf..963d1c43 100644 --- a/src/plone/rest/tests/test_traversal.py +++ b/src/plone/rest/tests/test_traversal.py @@ -60,18 +60,18 @@ def test_json_request_on_content_object_returns_service(self): def test_html_request_on_portal_root_returns_default_view(self): obj = self.traverse(accept="text/html") - self.assertEquals(self.portal.getDefaultLayout(), obj.__name__) + self.assertEqual(self.portal.getDefaultLayout(), obj.__name__) def test_html_request_on_portal_root_returns_dynamic_view(self): self.portal.setLayout("summary_view") obj = self.traverse(accept="text/html") - self.assertEquals("summary_view", obj.__name__) + self.assertEqual("summary_view", obj.__name__) def test_html_request_on_portal_root_returns_default_page(self): self.portal.invokeFactory("Document", id="doc1") self.portal.setDefaultPage("doc1") obj = self.traverse(accept="text/html") - self.assertEquals("document_view", obj.__name__) + self.assertEqual("document_view", obj.__name__) def test_json_request_on_object_with_multihook(self): doc1 = self.portal[self.portal.invokeFactory("Document", id="doc1")] @@ -86,7 +86,7 @@ def btr_test(container, request): obj = self.traverse(path="/plone/doc1") self.assertTrue(isinstance(obj, Service), "Not a service") - self.assertEquals(1, self.request._btr_test_called) + self.assertEqual(1, self.request._btr_test_called) def test_json_request_on_existing_view_returns_named_service(self): obj = self.traverse("/plone/search") From 55f140f87b2981756051ef5a5fba2409305788ec Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Tue, 25 Jan 2022 12:48:22 +0100 Subject: [PATCH 07/95] Preparing release 2.0.0a2 [ci skip] --- CHANGES.rst | 14 ++++++++++++++ news/123.bugfix | 1 - news/126.bugfix | 2 -- news/128.bugfix | 3 --- setup.py | 2 +- 5 files changed, 15 insertions(+), 7 deletions(-) delete mode 100644 news/123.bugfix delete mode 100644 news/126.bugfix delete mode 100644 news/128.bugfix diff --git a/CHANGES.rst b/CHANGES.rst index 3cf81ce6..c477cd9e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,20 @@ Changelog .. towncrier release notes start +2.0.0a2 (2022-01-25) +-------------------- + +Bug fixes: + + +- Fix typo in `README.rst` [jensens] (#123) +- Use document_view as default for site root. + [agitator] (#126) +- Resolve all the deprecation warnings that originate in this package's code that are + exposed by running the tests that do not stem from backwards compatibility we support. + [rpatterson] (#128) + + 2.0.0a1 (2021-10-05) -------------------- diff --git a/news/123.bugfix b/news/123.bugfix deleted file mode 100644 index ee50058f..00000000 --- a/news/123.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix typo in `README.rst` [jensens] diff --git a/news/126.bugfix b/news/126.bugfix deleted file mode 100644 index f1bff43d..00000000 --- a/news/126.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Use document_view as default for site root. -[agitator] diff --git a/news/128.bugfix b/news/128.bugfix deleted file mode 100644 index e8c7ff4a..00000000 --- a/news/128.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Resolve all the deprecation warnings that originate in this package's code that are -exposed by running the tests that do not stem from backwards compatibility we support. -[rpatterson] diff --git a/setup.py b/setup.py index a1e0304c..da257f0d 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "2.0.0a2.dev0" +version = "2.0.0a2" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 55986be8478c38ae7f91d871fa6a3bd89eed5ecc Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Tue, 25 Jan 2022 12:48:57 +0100 Subject: [PATCH 08/95] Back to development: 2.0.0a3 [ci skip] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index da257f0d..178a4116 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "2.0.0a2" +version = "2.0.0a3.dev0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From e6d6df6b470e057cb4528fbfff210a0138b7bb2f Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 12 Feb 2022 06:27:23 +0100 Subject: [PATCH 09/95] Use Black 21.12b0 (#131) * Pin black and install version from versions.cfg * pin black to 21.12b0 (last py2 compatible version) * Pin black==22.1.0 * Install black from versions.cfg in Makefile --- .github/workflows/black.yml | 4 ++-- Makefile | 2 +- plone-5.2.x.cfg | 2 +- versions.cfg | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 9b23c028..fa9a57e8 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -26,9 +26,9 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - # install black + # install black (extract version from versions.cfg) - name: install black - run: pip install black + run: pip install black==$(awk '/^black =/{print $NF}' versions.cfg) # run black - name: run black diff --git a/Makefile b/Makefile index 78f6e9d7..fce73d86 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ update: ## Update Make and Buildout bin/buildout: bin/pip bin/pip install --upgrade pip bin/pip install -r requirements.txt - bin/pip install black || true + bin/pip install pip install black==$$(awk '/^black =/{print $$NF}' versions.cfg) @touch -c $@ bin/python bin/pip: diff --git a/plone-5.2.x.cfg b/plone-5.2.x.cfg index b810a87d..cfd8f76b 100644 --- a/plone-5.2.x.cfg +++ b/plone-5.2.x.cfg @@ -6,7 +6,7 @@ find-links += https://dist.plone.org/thirdparty/ versions=versions [versions] -black = 20.8b1 +black = 21.12b0 # Error: The requirement ('virtualenv>=20.0.35') is not allowed by your [versions] constraint (20.0.26) virtualenv = 20.0.35 diff --git a/versions.cfg b/versions.cfg index d47c243e..226cfdea 100644 --- a/versions.cfg +++ b/versions.cfg @@ -14,6 +14,7 @@ Pygments = 2.5.1 plone.recipe.varnish = 1.3 # Code-analysis +black = 21.12b0 plone.recipe.codeanalysis = 3.0.1 coverage = 3.7.1 pep8 = 1.7.1 From 5a29afdffec18a4427a6f2047254c92ada4e65f8 Mon Sep 17 00:00:00 2001 From: Mauro Amico Date: Sat, 12 Feb 2022 16:35:44 +0100 Subject: [PATCH 10/95] ++api++ traverser should be kept on 30x redirections (#130) * ++api++ traverser should be kept on 30x redirections * reinsert ++api++ traverser * black Co-authored-by: Timo Stollenwerk --- news/127.fix | 1 + src/plone/rest/errors.py | 9 +++++++- src/plone/rest/tests/test_redirects.py | 32 ++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 news/127.fix diff --git a/news/127.fix b/news/127.fix new file mode 100644 index 00000000..e212318c --- /dev/null +++ b/news/127.fix @@ -0,0 +1 @@ +++api++ traverser should be kept on 30x redirections [mamico] diff --git a/src/plone/rest/errors.py b/src/plone/rest/errors.py index 8227ff40..ecfe8a30 100644 --- a/src/plone/rest/errors.py +++ b/src/plone/rest/errors.py @@ -149,6 +149,7 @@ def find_redirect_if_view_or_service(self, old_path_elements, storage): # New URL would match originally requested URL. # Lets not cause a redirect loop. return None + return new_path + "/" + "/".join(remainder) splitpoint -= 1 @@ -178,7 +179,8 @@ def attempt_redirect(self): if storage is None: return False - old_path = "/".join(old_path_elements) + # remove ++api++ traverser + old_path = "/".join(filter("++api++".__ne__, old_path_elements)) # First lets try with query string in cases or content migration @@ -211,6 +213,11 @@ def attempt_redirect(self): url_path = quote(url_path) url = urllib.parse.SplitResult(*(url[:2] + (url_path,) + url[3:])).geturl() else: + # reinsert ++api++ traverser + if "++api++" in old_path_elements: + new_path_elements = new_path.split("/") + new_path_elements.insert(old_path_elements.index("++api++"), "++api++") + new_path = "/".join(new_path_elements) url = self.request.physicalPathToURL(new_path) # some analytics programs might use this info to track diff --git a/src/plone/rest/tests/test_redirects.py b/src/plone/rest/tests/test_redirects.py index 2dcafc1d..39b159bf 100644 --- a/src/plone/rest/tests/test_redirects.py +++ b/src/plone/rest/tests/test_redirects.py @@ -39,6 +39,26 @@ def test_get_to_moved_item_causes_301_redirect(self): self.assertEqual(self.portal_url + "/folder-new", response.headers["Location"]) self.assertEqual(b"", response.raw.read()) + def test_get_to_moved_item_causes_301_redirect_with_api_traverser(self): + response = requests.get( + self.portal_url + "/++api++/folder-old", + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), + allow_redirects=False, + ) + self.assertEqual(301, response.status_code) + self.assertEqual( + self.portal_url + "/++api++/folder-new", response.headers["Location"] + ) + self.assertEqual(b"", response.raw.read()) + # follow the new location + response = requests.get( + response.headers["Location"], + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), + ) + self.assertEqual(200, response.status_code) + self.assertEqual("application/json", response.headers["Content-type"]) + self.assertEqual({"id": "folder-new", "method": "GET"}, response.json()) + def test_post_to_moved_item_causes_308_redirect(self): response = requests.post( self.portal_url + "/folder-old", @@ -50,6 +70,18 @@ def test_post_to_moved_item_causes_308_redirect(self): self.assertEqual(self.portal_url + "/folder-new", response.headers["Location"]) self.assertEqual(b"", response.raw.read()) + def test_post_to_moved_item_causes_308_redirect_with_api_traverser(self): + response = requests.post( + self.portal_url + "/++api++/folder-old", + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), + allow_redirects=False, + ) + self.assertEqual(308, response.status_code) + self.assertEqual( + self.portal_url + "/++api++/folder-new", response.headers["Location"] + ) + self.assertEqual(b"", response.raw.read()) + def test_unauthorized_request_to_item_still_redirects_first(self): response = requests.get( self.portal_url + "/folder-old", From a37928e0b1c38a47c9669ad8685ade8bc4b82cd4 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 12 Feb 2022 16:40:07 +0100 Subject: [PATCH 11/95] Fix towncrier file for #127 --- news/{127.fix => 127.bugfix} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{127.fix => 127.bugfix} (100%) diff --git a/news/127.fix b/news/127.bugfix similarity index 100% rename from news/127.fix rename to news/127.bugfix From 7979c2f76884b7a333ddf7302aa46b129c701b43 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 12 Feb 2022 16:40:56 +0100 Subject: [PATCH 12/95] Add internal towncrier config --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 05b615de..da067fac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,3 +18,8 @@ showcontent = true directory = "bugfix" name = "Bug fixes:" showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal:" +showcontent = true \ No newline at end of file From f23d33676e51d33fedc291456b6bd62324658b2b Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 12 Feb 2022 16:41:11 +0100 Subject: [PATCH 13/95] Preparing release 2.0.0a3 --- CHANGES.rst | 9 +++++++++ news/127.bugfix | 1 - setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/127.bugfix diff --git a/CHANGES.rst b/CHANGES.rst index c477cd9e..0b6ec13a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,15 @@ Changelog .. towncrier release notes start +2.0.0a3 (2022-02-12) +-------------------- + +Bug fixes: + + +- ++api++ traverser should be kept on 30x redirections [mamico] (#127) + + 2.0.0a2 (2022-01-25) -------------------- diff --git a/news/127.bugfix b/news/127.bugfix deleted file mode 100644 index e212318c..00000000 --- a/news/127.bugfix +++ /dev/null @@ -1 +0,0 @@ -++api++ traverser should be kept on 30x redirections [mamico] diff --git a/setup.py b/setup.py index 178a4116..572c6fca 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "2.0.0a3.dev0" +version = "2.0.0a3" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 7426d362ac963949fa07f2cc030f77c769e89b4b Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 12 Feb 2022 16:41:26 +0100 Subject: [PATCH 14/95] Back to development: 2.0.0a4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 572c6fca..2b968741 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "2.0.0a3" +version = "2.0.0a4.dev0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 4891ad1ad61f0c9541c07539c2708bc82054fbea Mon Sep 17 00:00:00 2001 From: Mauro Amico Date: Thu, 24 Mar 2022 16:57:37 +0100 Subject: [PATCH 15/95] redirect with view (#132) * redirect with view * black * black * changes --- news/132.fix | 1 + plone-5.2.x.cfg | 3 ++- src/plone/rest/errors.py | 11 ++++++++--- src/plone/rest/tests/test_redirects.py | 13 +++++++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 news/132.fix diff --git a/news/132.fix b/news/132.fix new file mode 100644 index 00000000..e212318c --- /dev/null +++ b/news/132.fix @@ -0,0 +1 @@ +++api++ traverser should be kept on 30x redirections [mamico] diff --git a/plone-5.2.x.cfg b/plone-5.2.x.cfg index cfd8f76b..82f35ea2 100644 --- a/plone-5.2.x.cfg +++ b/plone-5.2.x.cfg @@ -1,11 +1,12 @@ [buildout] extends = base.cfg - https://dist.plone.org/release/5.2.4/versions.cfg + https://dist.plone.org/release/5.2.7/versions.cfg find-links += https://dist.plone.org/thirdparty/ versions=versions [versions] +plone.rest = black = 21.12b0 # Error: The requirement ('virtualenv>=20.0.35') is not allowed by your [versions] constraint (20.0.26) diff --git a/src/plone/rest/errors.py b/src/plone/rest/errors.py index ecfe8a30..ecc66070 100644 --- a/src/plone/rest/errors.py +++ b/src/plone/rest/errors.py @@ -180,7 +180,12 @@ def attempt_redirect(self): return False # remove ++api++ traverser - old_path = "/".join(filter("++api++".__ne__, old_path_elements)) + if "++api++" in old_path_elements: + api_traverser_pos = old_path_elements.index("++api++") + old_path_elements = [el for el in old_path_elements if el != "++api++"] + else: + api_traverser_pos = None + old_path = "/".join(old_path_elements) # First lets try with query string in cases or content migration @@ -214,9 +219,9 @@ def attempt_redirect(self): url = urllib.parse.SplitResult(*(url[:2] + (url_path,) + url[3:])).geturl() else: # reinsert ++api++ traverser - if "++api++" in old_path_elements: + if api_traverser_pos is not None: new_path_elements = new_path.split("/") - new_path_elements.insert(old_path_elements.index("++api++"), "++api++") + new_path_elements.insert(api_traverser_pos, "++api++") new_path = "/".join(new_path_elements) url = self.request.physicalPathToURL(new_path) diff --git a/src/plone/rest/tests/test_redirects.py b/src/plone/rest/tests/test_redirects.py index 39b159bf..73e7ac7d 100644 --- a/src/plone/rest/tests/test_redirects.py +++ b/src/plone/rest/tests/test_redirects.py @@ -59,6 +59,19 @@ def test_get_to_moved_item_causes_301_redirect_with_api_traverser(self): self.assertEqual("application/json", response.headers["Content-type"]) self.assertEqual({"id": "folder-new", "method": "GET"}, response.json()) + def test_get_to_moved_item_causes_301_redirect_with_rest_view(self): + response = requests.get( + self.portal_url + "/++api++/folder-old/@actions", + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), + allow_redirects=False, + ) + self.assertEqual(301, response.status_code) + self.assertEqual( + self.portal_url + "/++api++/folder-new/@actions", + response.headers["Location"], + ) + self.assertEqual(b"", response.raw.read()) + def test_post_to_moved_item_causes_308_redirect(self): response = requests.post( self.portal_url + "/folder-old", From f1dce1b31ab4720f3a467b7896fc82a1c4925e83 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Thu, 24 Mar 2022 17:00:29 +0100 Subject: [PATCH 16/95] Fix #132 changelog entry --- news/{132.fix => 132.bugfix} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{132.fix => 132.bugfix} (100%) diff --git a/news/132.fix b/news/132.bugfix similarity index 100% rename from news/132.fix rename to news/132.bugfix From e00dd01a6f45a3f8b45c0d16d31b3446b1f1d6c8 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Thu, 24 Mar 2022 17:00:47 +0100 Subject: [PATCH 17/95] Preparing release 2.0.0a4 [ci skip] --- CHANGES.rst | 9 +++++++++ news/132.bugfix | 1 - setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/132.bugfix diff --git a/CHANGES.rst b/CHANGES.rst index 0b6ec13a..4d2d4cd2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,15 @@ Changelog .. towncrier release notes start +2.0.0a4 (2022-03-24) +-------------------- + +Bug fixes: + + +- ++api++ traverser should be kept on 30x redirections [mamico] (#132) + + 2.0.0a3 (2022-02-12) -------------------- diff --git a/news/132.bugfix b/news/132.bugfix deleted file mode 100644 index e212318c..00000000 --- a/news/132.bugfix +++ /dev/null @@ -1 +0,0 @@ -++api++ traverser should be kept on 30x redirections [mamico] diff --git a/setup.py b/setup.py index 2b968741..3825d084 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "2.0.0a4.dev0" +version = "2.0.0a4" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From c0d9db2fdb0ee97da66bae64b33d0640b6456823 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Thu, 24 Mar 2022 17:01:06 +0100 Subject: [PATCH 18/95] Back to development: 2.0.0a5 [ci skip] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3825d084..86de5529 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "2.0.0a4" +version = "2.0.0a5.dev0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 456d2de1e52629a1dfb5312cbf5da2b69d4d2bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Thu, 7 Apr 2022 02:15:29 -0300 Subject: [PATCH 19/95] Fix an infinite loop with redirections from parent to child (#134) * Fix an infinite loop with redirections from parent to child * Pin Click version --- .github/workflows/black.yml | 2 +- news/133.bugfix | 1 + src/plone/rest/errors.py | 3 +-- src/plone/rest/tests/test_redirects.py | 12 ++++++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 news/133.bugfix diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index fa9a57e8..adaf3406 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -28,7 +28,7 @@ jobs: # install black (extract version from versions.cfg) - name: install black - run: pip install black==$(awk '/^black =/{print $NF}' versions.cfg) + run: pip install click==8.0.4 black==$(awk '/^black =/{print $NF}' versions.cfg) # run black - name: run black diff --git a/news/133.bugfix b/news/133.bugfix new file mode 100644 index 00000000..60310dc3 --- /dev/null +++ b/news/133.bugfix @@ -0,0 +1 @@ +Fix an infinite loop with redirections from parent to child [ericof] \ No newline at end of file diff --git a/src/plone/rest/errors.py b/src/plone/rest/errors.py index ecc66070..e690fb0b 100644 --- a/src/plone/rest/errors.py +++ b/src/plone/rest/errors.py @@ -138,14 +138,13 @@ def find_redirect_if_view_or_service(self, old_path_elements, storage): # ['', 'Plone', 'folder', 'item', '@@view', 'param'] # ^ splitpoint = len(old_path_elements) - while splitpoint > 1: possible_obj_path = "/".join(old_path_elements[:splitpoint]) remainder = old_path_elements[splitpoint:] new_path = storage.get(possible_obj_path) if new_path: - if new_path == possible_obj_path: + if new_path.startswith(possible_obj_path): # New URL would match originally requested URL. # Lets not cause a redirect loop. return None diff --git a/src/plone/rest/tests/test_redirects.py b/src/plone/rest/tests/test_redirects.py index 73e7ac7d..16c72073 100644 --- a/src/plone/rest/tests/test_redirects.py +++ b/src/plone/rest/tests/test_redirects.py @@ -214,6 +214,18 @@ def test_handles_redirects_that_include_querystring_in_old_path(self): self.assertEqual(self.portal_url + "/new-item", response.headers["Location"]) self.assertEqual(b"", response.raw.read()) + def test_handles_redirects_that_are_recursive(self): + storage = queryUtility(IRedirectionStorage) + storage.add("/plone/folder-new", "/plone/folder-new/archive") + transaction.commit() + # Request should return 404 + response = requests.get( + self.portal_url + "/folder-new/sub_folder/not-found", + headers={"Accept": "application/json"}, + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), + ) + self.assertEqual(404, response.status_code) + def test_aborts_redirect_checks_early_for_app_root(self): error_view = ErrorHandling(self.portal, self.portal.REQUEST) self.assertIsNone(error_view.find_redirect_if_view_or_service([""], None)) From a95d774c6493bc4048ece8898300b3ec7d413cf6 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Thu, 7 Apr 2022 07:20:13 +0200 Subject: [PATCH 20/95] Upgrade zc.buildout==2.13.4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8466fed9..0c1a2280 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ # Keep this file in sync with: https://github.com/kitconcept/buildout/edit/master/requirements.txt setuptools==42.0.2 -zc.buildout==2.13.3 +zc.buildout==2.13.4 wheel From 964fe9d1876278ce2832a37bd25d33c4d75965c7 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Thu, 7 Apr 2022 07:20:28 +0200 Subject: [PATCH 21/95] Preparing release 2.0.0a5 --- CHANGES.rst | 9 +++++++++ news/133.bugfix | 1 - setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/133.bugfix diff --git a/CHANGES.rst b/CHANGES.rst index 4d2d4cd2..2d917841 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,15 @@ Changelog .. towncrier release notes start +2.0.0a5 (2022-04-07) +-------------------- + +Bug fixes: + + +- Fix an infinite loop with redirections from parent to child [ericof] (#133) + + 2.0.0a4 (2022-03-24) -------------------- diff --git a/news/133.bugfix b/news/133.bugfix deleted file mode 100644 index 60310dc3..00000000 --- a/news/133.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix an infinite loop with redirections from parent to child [ericof] \ No newline at end of file diff --git a/setup.py b/setup.py index 86de5529..f8c0b1eb 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "2.0.0a5.dev0" +version = "2.0.0a5" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From b873ee069098188dd80bbf652c0cd412386148b9 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Thu, 7 Apr 2022 07:20:41 +0200 Subject: [PATCH 22/95] Back to development: 2.0.0a6 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f8c0b1eb..642f34a4 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "2.0.0a5" +version = "2.0.0a6.dev0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 2787319ae86a86b6094e8837aa54b41df02c24d8 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 15 Oct 2022 17:14:05 +0200 Subject: [PATCH 23/95] Add changelog for final re-release --- news/136.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/136.bugfix diff --git a/news/136.bugfix b/news/136.bugfix new file mode 100644 index 00000000..161138d7 --- /dev/null +++ b/news/136.bugfix @@ -0,0 +1 @@ +Re-release 2.0.0a6 as 2.0.0 [tisto] \ No newline at end of file From a28a1cc413a5b1a4c7283feaeea7dd9fb827edc5 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 15 Oct 2022 17:18:33 +0200 Subject: [PATCH 24/95] Preparing release 2.0.0 --- CHANGES.rst | 9 +++++++++ news/136.bugfix | 1 - setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/136.bugfix diff --git a/CHANGES.rst b/CHANGES.rst index 2d917841..9fa5ebbf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,15 @@ Changelog .. towncrier release notes start +2.0.0 (2022-10-15) +------------------ + +Bug fixes: + + +- Re-release 2.0.0a6 as 2.0.0 [tisto] (#136) + + 2.0.0a5 (2022-04-07) -------------------- diff --git a/news/136.bugfix b/news/136.bugfix deleted file mode 100644 index 161138d7..00000000 --- a/news/136.bugfix +++ /dev/null @@ -1 +0,0 @@ -Re-release 2.0.0a6 as 2.0.0 [tisto] \ No newline at end of file diff --git a/setup.py b/setup.py index 642f34a4..4158c49a 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "2.0.0a6.dev0" +version = "2.0.0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From e259ac81f7028d04ada72eb771cb7631d062fdf3 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 15 Oct 2022 17:18:49 +0200 Subject: [PATCH 25/95] Back to development: 2.0.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4158c49a..2d2e937e 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "2.0.0" +version = "2.0.1.dev0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 8960b8fd28cbe38aecabc68eeba91ff25ba8dba9 Mon Sep 17 00:00:00 2001 From: Mauro Amico Date: Sat, 28 Jan 2023 16:44:15 +0100 Subject: [PATCH 26/95] Should redirection response be temporarily instead of permanent (#135) * temporarily_redirect * fix tests + changelog * changelog * Update news/135.bugfix --------- Co-authored-by: David Glick --- .gitignore | 1 + README.rst | 12 ++-- news/135.bugfix | 2 + plone-4.3.x.cfg | 3 + plone-5.1.x.cfg | 5 +- src/plone/rest/errors.py | 10 ++-- src/plone/rest/tests/test_dispatching.py | 72 ++++++++++++------------ src/plone/rest/tests/test_redirects.py | 36 ++++++------ 8 files changed, 75 insertions(+), 66 deletions(-) create mode 100644 news/135.bugfix diff --git a/.gitignore b/.gitignore index 30ab005f..ece0007b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ /.Python /include /lib +/lib64 /.mr.developer.cfg *.mo local/ diff --git a/README.rst b/README.rst index ada524ac..7d432427 100644 --- a/README.rst +++ b/README.rst @@ -33,7 +33,7 @@ It is a software architectural principle to create loosely coupled web APIs. plone.rest provides the basic infrastructure that allows us to build RESTful endpoints in Plone. -The reason for separating this infrastructure into a separate package from the 'main' full `Plone REST API `_ is so you can create alternative endpoints tailored to specific usecases. +The reason for separating this infrastructure into a separate package from the 'main' full `Plone REST API `_ is so you can create alternative endpoints tailored to specific usecases. A number of these specific endpoints are already in active use. @@ -120,10 +120,10 @@ The server then will respond with '200 OK':: "message": "PATCH: Hello World!" } -Why two methods? +Why two methods? Using the 'Accept' header is the intended way of RESTful APIs to get different responses from the same URL. However, if it comes to caching the response in an web accelerator like Varnish or Cloudflare, additional challenges are added. -Setting the `Vary` header to 'Vary: Accept' helps to a certain degree in Varnish. +Setting the `Vary` header to 'Vary: Accept' helps to a certain degree in Varnish. But cache pollution may happen, because different browsers send different headers on normal HTML requests. Hosted services like Cloudflare just do not support the 'Vary' usage and can not be used for sites with REST calls. Thus a second option with different URLs is needed. @@ -319,16 +319,16 @@ plone.rest will handle redirects created by ``plone.app.redirector`` pretty much the same way as regular Plone. If a redirect exists for a given URL, a ``GET`` request will be answered with -``301``, and the new location for the resource is indicated in the ``Location`` +``302``, and the new location for the resource is indicated in the ``Location`` header:: - HTTP/1.1 301 Moved Permanently + HTTP/1.1 302 Moved Temporarily Content-Type: application/json Location: http://localhost:8080/Plone/my-folder-new-location Any other request method than GET (``POST``, ``PATCH``, ...) will be answered -with ``308 Permanent Redirect``. This status code instructs the client that +with ``307 Temporary Redirect``. This status code instructs the client that it should NOT switch the method, but retry (if desired) the request with the *same* method at the new location. diff --git a/news/135.bugfix b/news/135.bugfix new file mode 100644 index 00000000..f2bec6ba --- /dev/null +++ b/news/135.bugfix @@ -0,0 +1,2 @@ +When redirecting from an old alias, the HTTP status of the response is changed from 301 (Moved Permanently) to 302 (Found) for GET requests and to 307 (Temporary Redirect) for other request methods because nothing prevents the URL from being reused in the future. +[mamico] diff --git a/plone-4.3.x.cfg b/plone-4.3.x.cfg index 858527ff..72674c4e 100644 --- a/plone-4.3.x.cfg +++ b/plone-4.3.x.cfg @@ -26,6 +26,9 @@ distlib = 0.3.1 [versions:python27] PyJWT = 1.7.1 pyroma = 2.6.1 +pep517 = <=0.12.0 +readme-renderer = <=28.0 +bleach = <4 # more-itertools >= 6.0.0 dropped python2.7 support more-itertools = 5.0.0 diff --git a/plone-5.1.x.cfg b/plone-5.1.x.cfg index 33ccdcca..9729141e 100644 --- a/plone-5.1.x.cfg +++ b/plone-5.1.x.cfg @@ -37,6 +37,9 @@ astunparse = 1.6.2 [versions:python27] PyJWT = 1.7.1 pyroma = 2.6.1 +pep517 = <=0.12.0 +readme-renderer = <=28.0 +bleach = <4 # more-itertools >= 6.0.0 dropped python2.7 support more-itertools = 5.0.0 @@ -45,4 +48,4 @@ more-itertools = 5.0.0 pyrsistent = 0.15.7 # Click 8 dropped Python 2 support -Click = 7.1.2 \ No newline at end of file +Click = 7.1.2 diff --git a/src/plone/rest/errors.py b/src/plone/rest/errors.py index e690fb0b..454d48ee 100644 --- a/src/plone/rest/errors.py +++ b/src/plone/rest/errors.py @@ -162,7 +162,7 @@ def attempt_redirect(self): This method is based on FourOhFourView.attempt_redirect() from p.a.redirector. It's copied here because we want to answer redirects - to non-GET methods with status 308, but since this method locks the + to non-GET methods with status 307, but since this method locks the response status, we wouldn't be able to change it afterwards. """ url = self._url() @@ -228,14 +228,14 @@ def attempt_redirect(self): if query_string: url += "?" + query_string - # Answer GET requests with 301. Every other method will be answered - # with 308 Permanent Redirect, which instructs the client to NOT + # Answer GET requests with 302. Every other method will be answered + # with 307 Temporary Redirect, which instructs the client to NOT # switch the method (if the original request was a POST, it should # re-POST to the new URL from the Location header). if self.request.method.upper() == "GET": - status = 301 + status = 302 else: - status = 308 + status = 307 self.request.response.redirect(url, status=status, lock=1) return True diff --git a/src/plone/rest/tests/test_dispatching.py b/src/plone/rest/tests/test_dispatching.py index 674e2f67..fb9b7b84 100644 --- a/src/plone/rest/tests/test_dispatching.py +++ b/src/plone/rest/tests/test_dispatching.py @@ -231,12 +231,12 @@ def setUp(self): def test_moved_private_dx_folder_with_creds(self): expectations = [ - ("/private-old", "GET", CREDS, 301), - ("/private-old", "POST", CREDS, 308), - ("/private-old", "PUT", CREDS, 308), - ("/private-old", "PATCH", CREDS, 308), - ("/private-old", "DELETE", CREDS, 308), - ("/private-old", "OPTIONS", CREDS, 308), + ("/private-old", "GET", CREDS, 302), + ("/private-old", "POST", CREDS, 307), + ("/private-old", "PUT", CREDS, 307), + ("/private-old", "PATCH", CREDS, 307), + ("/private-old", "DELETE", CREDS, 307), + ("/private-old", "OPTIONS", CREDS, 307), ] self.validate(expectations) @@ -253,12 +253,12 @@ def test_moved_private_dx_folder_with_creds(self): def test_moved_private_dx_folder_without_creds(self): expectations = [ - ("/private-old", "GET", NO_CREDS, 301), - ("/private-old", "POST", NO_CREDS, 308), - ("/private-old", "PUT", NO_CREDS, 308), - ("/private-old", "PATCH", NO_CREDS, 308), - ("/private-old", "DELETE", NO_CREDS, 308), - ("/private-old", "OPTIONS", NO_CREDS, 308), + ("/private-old", "GET", NO_CREDS, 302), + ("/private-old", "POST", NO_CREDS, 307), + ("/private-old", "PUT", NO_CREDS, 307), + ("/private-old", "PATCH", NO_CREDS, 307), + ("/private-old", "DELETE", NO_CREDS, 307), + ("/private-old", "OPTIONS", NO_CREDS, 307), ] self.validate(expectations) @@ -275,12 +275,12 @@ def test_moved_private_dx_folder_without_creds(self): def test_moved_private_dx_folder_invalid_creds(self): expectations = [ - ("/private-old", "GET", INVALID_CREDS, 301), - ("/private-old", "POST", INVALID_CREDS, 308), - ("/private-old", "PUT", INVALID_CREDS, 308), - ("/private-old", "PATCH", INVALID_CREDS, 308), - ("/private-old", "DELETE", INVALID_CREDS, 308), - ("/private-old", "OPTIONS", INVALID_CREDS, 308), + ("/private-old", "GET", INVALID_CREDS, 302), + ("/private-old", "POST", INVALID_CREDS, 307), + ("/private-old", "PUT", INVALID_CREDS, 307), + ("/private-old", "PATCH", INVALID_CREDS, 307), + ("/private-old", "DELETE", INVALID_CREDS, 307), + ("/private-old", "OPTIONS", INVALID_CREDS, 307), ] self.validate(expectations) @@ -297,12 +297,12 @@ def test_moved_private_dx_folder_invalid_creds(self): def test_moved_public_dx_folder_with_creds(self): expectations = [ - ("/public-old", "GET", CREDS, 301), - ("/public-old", "POST", CREDS, 308), - ("/public-old", "PUT", CREDS, 308), - ("/public-old", "PATCH", CREDS, 308), - ("/public-old", "DELETE", CREDS, 308), - ("/public-old", "OPTIONS", CREDS, 308), + ("/public-old", "GET", CREDS, 302), + ("/public-old", "POST", CREDS, 307), + ("/public-old", "PUT", CREDS, 307), + ("/public-old", "PATCH", CREDS, 307), + ("/public-old", "DELETE", CREDS, 307), + ("/public-old", "OPTIONS", CREDS, 307), ] self.validate(expectations) @@ -319,12 +319,12 @@ def test_moved_public_dx_folder_with_creds(self): def test_moved_public_dx_folder_without_creds(self): expectations = [ - ("/public-old", "GET", NO_CREDS, 301), - ("/public-old", "POST", NO_CREDS, 308), - ("/public-old", "PUT", NO_CREDS, 308), - ("/public-old", "PATCH", NO_CREDS, 308), - ("/public-old", "DELETE", NO_CREDS, 308), - ("/public-old", "OPTIONS", NO_CREDS, 308), + ("/public-old", "GET", NO_CREDS, 302), + ("/public-old", "POST", NO_CREDS, 307), + ("/public-old", "PUT", NO_CREDS, 307), + ("/public-old", "PATCH", NO_CREDS, 307), + ("/public-old", "DELETE", NO_CREDS, 307), + ("/public-old", "OPTIONS", NO_CREDS, 307), ] self.validate(expectations) @@ -341,12 +341,12 @@ def test_moved_public_dx_folder_without_creds(self): def test_moved_public_dx_folder_invalid_creds(self): expectations = [ - ("/public-old", "GET", INVALID_CREDS, 301), - ("/public-old", "POST", INVALID_CREDS, 308), - ("/public-old", "PUT", INVALID_CREDS, 308), - ("/public-old", "PATCH", INVALID_CREDS, 308), - ("/public-old", "DELETE", INVALID_CREDS, 308), - ("/public-old", "OPTIONS", INVALID_CREDS, 308), + ("/public-old", "GET", INVALID_CREDS, 302), + ("/public-old", "POST", INVALID_CREDS, 307), + ("/public-old", "PUT", INVALID_CREDS, 307), + ("/public-old", "PATCH", INVALID_CREDS, 307), + ("/public-old", "DELETE", INVALID_CREDS, 307), + ("/public-old", "OPTIONS", INVALID_CREDS, 307), ] self.validate(expectations) diff --git a/src/plone/rest/tests/test_redirects.py b/src/plone/rest/tests/test_redirects.py index 16c72073..30122daf 100644 --- a/src/plone/rest/tests/test_redirects.py +++ b/src/plone/rest/tests/test_redirects.py @@ -28,24 +28,24 @@ def setUp(self): self.portal.manage_renameObject("folder-old", "folder-new") transaction.commit() - def test_get_to_moved_item_causes_301_redirect(self): + def test_get_to_moved_item_causes_302_redirect(self): response = requests.get( self.portal_url + "/folder-old", headers={"Accept": "application/json"}, auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual(self.portal_url + "/folder-new", response.headers["Location"]) self.assertEqual(b"", response.raw.read()) - def test_get_to_moved_item_causes_301_redirect_with_api_traverser(self): + def test_get_to_moved_item_causes_302_redirect_with_api_traverser(self): response = requests.get( self.portal_url + "/++api++/folder-old", auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual( self.portal_url + "/++api++/folder-new", response.headers["Location"] ) @@ -59,37 +59,37 @@ def test_get_to_moved_item_causes_301_redirect_with_api_traverser(self): self.assertEqual("application/json", response.headers["Content-type"]) self.assertEqual({"id": "folder-new", "method": "GET"}, response.json()) - def test_get_to_moved_item_causes_301_redirect_with_rest_view(self): + def test_get_to_moved_item_causes_302_redirect_with_rest_view(self): response = requests.get( self.portal_url + "/++api++/folder-old/@actions", auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual( self.portal_url + "/++api++/folder-new/@actions", response.headers["Location"], ) self.assertEqual(b"", response.raw.read()) - def test_post_to_moved_item_causes_308_redirect(self): + def test_post_to_moved_item_causes_307_redirect(self): response = requests.post( self.portal_url + "/folder-old", headers={"Accept": "application/json"}, auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(308, response.status_code) + self.assertEqual(307, response.status_code) self.assertEqual(self.portal_url + "/folder-new", response.headers["Location"]) self.assertEqual(b"", response.raw.read()) - def test_post_to_moved_item_causes_308_redirect_with_api_traverser(self): + def test_post_to_moved_item_causes_307_redirect_with_api_traverser(self): response = requests.post( self.portal_url + "/++api++/folder-old", auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(308, response.status_code) + self.assertEqual(307, response.status_code) self.assertEqual( self.portal_url + "/++api++/folder-new", response.headers["Location"] ) @@ -105,7 +105,7 @@ def test_unauthorized_request_to_item_still_redirects_first(self): # A request to the old URL of an item where the user doesn't have # necessary permissions will still result in a redirect - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual(self.portal_url + "/folder-new", response.headers["Location"]) self.assertEqual(b"", response.raw.read()) @@ -125,20 +125,20 @@ def test_query_string_gets_preserved(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual( self.portal_url + "/folder-new?key=value", response.headers["Location"] ) self.assertEqual(b"", response.raw.read()) - def test_named_service_on_moved_item_causes_301_redirect(self): + def test_named_service_on_moved_item_causes_302_redirect(self): response = requests.get( self.portal_url + "/folder-old/namedservice", headers={"Accept": "application/json"}, auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual( self.portal_url + "/folder-new/namedservice", response.headers["Location"] ) @@ -151,7 +151,7 @@ def test_named_service_plus_path_parameter_works(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual( self.portal_url + "/folder-new/namedservice/param", response.headers["Location"], @@ -165,7 +165,7 @@ def test_redirects_for_regular_views_still_work(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual( self.portal_url + "/folder-new/@@some-view", response.headers["Location"] ) @@ -178,7 +178,7 @@ def test_redirects_for_views_plus_params_plus_querystring_works(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual( self.portal_url + "/folder-new/@@some-view/param?k=v", response.headers["Location"], @@ -210,7 +210,7 @@ def test_handles_redirects_that_include_querystring_in_old_path(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual(self.portal_url + "/new-item", response.headers["Location"]) self.assertEqual(b"", response.raw.read()) From a7c8d6f809c339aa4eed9b4d4a9f8543b130587e Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 28 Jan 2023 17:17:38 +0100 Subject: [PATCH 27/95] Add python_requires to setup.py (#138) * Bring back Python 2.7 on CI * Add python_requires to setup.py --- .github/workflows/tests.yml | 3 +-- setup.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c560a38..f517d7ea 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,7 +2,7 @@ name: plone.rest CI on: [push] jobs: build: - runs-on: ubuntu-latest + runs-on: "ubuntu-20.04" # needed to keep Python 2.7 strategy: fail-fast: false matrix: @@ -18,7 +18,6 @@ jobs: - python-version: 3.8 plone-version: 5.1 steps: - # git checkout - uses: actions/checkout@v2 diff --git a/setup.py b/setup.py index 2d2e937e..d13e8dd2 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ import os -from setuptools import setup, find_packages + +from setuptools import find_packages, setup def read(*rnames): @@ -47,6 +48,7 @@ def read(*rnames): namespace_packages=["plone"], include_package_data=True, zip_safe=False, + python_requires=">=2.7", extras_require=dict( test=[ "plone.app.testing[robot]>=4.2.2", From 96c224fefd6f7797aa5961da23c78362528492bc Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 28 Jan 2023 17:46:44 +0100 Subject: [PATCH 28/95] Drop support for Plone 4.3, 5.0, and 5.1 (#142) * Drop support for Plone 4.3, 5.0, and 5.1 * Drop versions on gha as well --- .github/workflows/tests.yml | 11 +---------- news/140.breaking | 2 ++ setup.py | 3 --- 3 files changed, 3 insertions(+), 13 deletions(-) create mode 100644 news/140.breaking diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f517d7ea..da7d1336 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,16 +7,7 @@ jobs: fail-fast: false matrix: python-version: [3.8, 3.7, 2.7] - plone-version: [5.2, 5.1, 4.3] - exclude: - - python-version: 3.7 - plone-version: 4.3 - - python-version: 3.7 - plone-version: 5.1 - - python-version: 3.8 - plone-version: 4.3 - - python-version: 3.8 - plone-version: 5.1 + plone-version: [5.2] steps: # git checkout - uses: actions/checkout@v2 diff --git a/news/140.breaking b/news/140.breaking new file mode 100644 index 00000000..dabb293f --- /dev/null +++ b/news/140.breaking @@ -0,0 +1,2 @@ +Drop official support for Plone 4.3, 5.0 and 5.1 (most likely the package will continue to work though) +[tisto] \ No newline at end of file diff --git a/setup.py b/setup.py index d13e8dd2..46b738fe 100644 --- a/setup.py +++ b/setup.py @@ -23,9 +23,6 @@ def read(*rnames): "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Plone", - "Framework :: Plone :: 4.3", - "Framework :: Plone :: 5.0", - "Framework :: Plone :: 5.1", "Framework :: Plone :: 5.2", "Framework :: Plone :: Core", "Framework :: Zope2", From b0e31a87d05716c3c23942c38054ea375efb564b Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sun, 29 Jan 2023 07:41:44 +0100 Subject: [PATCH 29/95] Add official support for Plone 6 (#144) * Drop support for Plone 4.3, 5.0, and 5.1 * Drop versions on gha as well * Add official support for Plone 6 * Use individual requirements files for 5.2 and 6.0 * Exclude GHA Plone 6 / Py 2.7 * Add changelog for #143 * Add build for Plone 6 to Makefile * No need to test py 3.7 --- .github/workflows/tests.yml | 9 ++++++--- Makefile | 10 ++++++++-- news/143.feature | 2 ++ plone-6.0.x.cfg | 17 +++++++++++++++++ requirements-5.2.x.txt | 12 ++++++++++++ requirements-6.0.x.txt | 11 +++++++++++ setup.py | 1 + 7 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 news/143.feature create mode 100644 plone-6.0.x.cfg create mode 100644 requirements-5.2.x.txt create mode 100644 requirements-6.0.x.txt diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index da7d1336..5455f527 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,8 +6,11 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.7, 2.7] - plone-version: [5.2] + python-version: [3.8, 2.7] + plone-version: ["6.0", 5.2] + exclude: + - python-version: 2.7 + plone-version: 6.0 steps: # git checkout - uses: actions/checkout@v2 @@ -30,7 +33,7 @@ jobs: - run: pip install virtualenv - run: pip install wheel - name: pip install - run: pip install -r requirements.txt + run: pip install -r requirements-${{ matrix.plone-version }}.x.txt - name: choose Plone version run: sed -ie "s#plone-x.x.x.cfg#plone-${{ matrix.plone-version }}.x.cfg#" ci.cfg diff --git a/Makefile b/Makefile index fce73d86..369383b4 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SHELL := /bin/bash CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) -version = 3 +version = 3.8 # We like colors # From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects @@ -12,7 +12,7 @@ GREEN=`tput setaf 2` RESET=`tput sgr0` YELLOW=`tput setaf 3` -all: .installed.cfg +all: build-plone-6.0 # Add the following 'help' target to your Makefile # And add help text after each target name starting with '\#\#' @@ -83,6 +83,12 @@ build-plone-5.2-performance: .installed.cfg ## Build Plone 5.2 bin/pip install -r requirements.txt bin/buildout -c plone-5.2.x-performance.cfg +build-plone-6.0: ## Build Plone 6.0 + python$(version) -m venv . + bin/pip install --upgrade pip + bin/pip install -r requirements-6.0.x.txt + bin/buildout -c plone-6.0.x.cfg + .PHONY: Test test: ## Test bin/test diff --git a/news/143.feature b/news/143.feature new file mode 100644 index 00000000..16a5e19a --- /dev/null +++ b/news/143.feature @@ -0,0 +1,2 @@ +Add official support for Plone 6 +[tisto] diff --git a/plone-6.0.x.cfg b/plone-6.0.x.cfg new file mode 100644 index 00000000..bcb63780 --- /dev/null +++ b/plone-6.0.x.cfg @@ -0,0 +1,17 @@ +[buildout] +extends = + https://dist.plone.org/release/6.0.0/versions.cfg + base.cfg + +[buildout:python38] +parts = + test + code-analysis + +[instance] +recipe = plone.recipe.zope2instance +zodb-temporary-storage = off + +[versions] +black = 21.7b0 +pygments = 2.14.0 \ No newline at end of file diff --git a/requirements-5.2.x.txt b/requirements-5.2.x.txt new file mode 100644 index 00000000..505c1abe --- /dev/null +++ b/requirements-5.2.x.txt @@ -0,0 +1,12 @@ +# Keep this file in sync with: https://dist.plone.org/release/5.2.9/requirements.txt +setuptools==42.0.2 +zc.buildout==2.13.7 +wheel==0.37.1 + +# Windows specific down here (has to be installed here, fails in buildout) +# Dependency of zope.sendmail: +pywin32 ; platform_system == 'Windows' +# SSL Certs on Windows, because Python is missing them otherwise: +certifi ; platform_system == 'Windows' +# Dependency of collective.recipe.omelette: +ntfsutils ; platform_system == 'Windows' and python_version < '3.0' \ No newline at end of file diff --git a/requirements-6.0.x.txt b/requirements-6.0.x.txt new file mode 100644 index 00000000..fe350fe2 --- /dev/null +++ b/requirements-6.0.x.txt @@ -0,0 +1,11 @@ +pip==22.3.1 +setuptools==65.5.1 +wheel==0.38.4 +zc.buildout==3.0.1 + +# Windows specific down here (has to be installed here, fails in buildout) +# Dependency of zope.sendmail: +pywin32 ; platform_system == 'Windows' + +# SSL Certs on windows, because Python is missing them otherwise: +certifi ; platform_system == 'Windows' \ No newline at end of file diff --git a/setup.py b/setup.py index 46b738fe..635fab59 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ def read(*rnames): "Environment :: Web Environment", "Framework :: Plone", "Framework :: Plone :: 5.2", + "Framework :: Plone :: 6.0", "Framework :: Plone :: Core", "Framework :: Zope2", "Framework :: Zope :: 4", From 5a4100fc663eb6c66c92a8472a54fe21f0054aba Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sun, 29 Jan 2023 08:30:38 +0100 Subject: [PATCH 30/95] Add official support for Python 3.9, 3.10, and 3.11 (#145) * Drop support for Plone 4.3, 5.0, and 5.1 * Drop versions on gha as well * Add official support for Plone 6 * Use individual requirements files for 5.2 and 6.0 * Exclude GHA Plone 6 / Py 2.7 * Add changelog for #143 * Add build for Plone 6 to Makefile * No need to test py 3.7 * Test against newer Python versions * Plone 5.2 only supports Python 3.8 and older versions * Fix running 3.10 on gha * Add py 3.9-3.11 to setup.py * Add changelog --- .github/workflows/tests.yml | 10 ++++++++-- news/147.feature | 2 ++ setup.py | 3 +++ 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 news/147.feature diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5455f527..ad015c6a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,11 +6,17 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 2.7] - plone-version: ["6.0", 5.2] + python-version: ["3.11", "3.10", "3.9", "3.8", "2.7"] + plone-version: ["6.0", "5.2"] exclude: - python-version: 2.7 plone-version: 6.0 + - python-version: 3.11 + plone-version: 5.2 + - python-version: 3.10 + plone-version: 5.2 + - python-version: 3.9 + plone-version: 5.2 steps: # git checkout - uses: actions/checkout@v2 diff --git a/news/147.feature b/news/147.feature new file mode 100644 index 00000000..6d1e5fe5 --- /dev/null +++ b/news/147.feature @@ -0,0 +1,2 @@ +Add official support for Python 3.9, 3.10, and 3.11 +[tisto] diff --git a/setup.py b/setup.py index 635fab59..28620432 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,9 @@ def read(*rnames): "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], keywords="rest http", author="Plone Foundation", From 9384e3180a057409bfd7c25994fa435db55a7520 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sun, 29 Jan 2023 08:33:40 +0100 Subject: [PATCH 31/95] Declare #135 as breaking --- news/135.breaking | 3 +++ news/135.bugfix | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 news/135.breaking delete mode 100644 news/135.bugfix diff --git a/news/135.breaking b/news/135.breaking new file mode 100644 index 00000000..3c989ecf --- /dev/null +++ b/news/135.breaking @@ -0,0 +1,3 @@ +Change the HTTP status from 301 (Moved Permanently) to 302 (Found) for GET requests and to 307 (Temporary Redirect) for other request methods. +This fixes problems when an existing redirect is re-used. +[mamico] diff --git a/news/135.bugfix b/news/135.bugfix deleted file mode 100644 index f2bec6ba..00000000 --- a/news/135.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -When redirecting from an old alias, the HTTP status of the response is changed from 301 (Moved Permanently) to 302 (Found) for GET requests and to 307 (Temporary Redirect) for other request methods because nothing prevents the URL from being reused in the future. -[mamico] From 0e8874cf7ee5ca9b37f7bc7df9ae8cadf2116933 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sun, 29 Jan 2023 08:36:43 +0100 Subject: [PATCH 32/95] Add README section for Plone/Python support --- README.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.rst b/README.rst index 7d432427..d2eca202 100644 --- a/README.rst +++ b/README.rst @@ -312,6 +312,20 @@ Install plone.rest by adding it to your buildout:: and then running "bin/buildout" +Plone/Python Support +-------------------- + +plone.restapi currently supports Plone 6 and 5.2. + +plone.restapi supports Python 2.7 and 3.8 for Plone 5.2 and Python 3.8, 3.9, 3.10, and 3.11 for Plone 6. + +Older versions of Python and Plone most likely will continue to work with plone.rest. + +Though, we do not test or officially support them. + +Check older versions of plone.rest for official support. + + Redirects --------- From 86d001f633cc4cb0616d3c3788007d65db3419e4 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sun, 29 Jan 2023 08:37:45 +0100 Subject: [PATCH 33/95] Update maintainer in README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d2eca202..3b960fc7 100644 --- a/README.rst +++ b/README.rst @@ -361,7 +361,7 @@ Contribute Support ------- -This package is maintained by Timo Stollenwerk and Ramon Navarro Bosch . +This package is maintained by Timo Stollenwerk . If you are having issues, please `let us know `_. From 4ab2e83e9e79d91e4773a44f0b982de0820baea2 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sun, 29 Jan 2023 08:45:39 +0100 Subject: [PATCH 34/95] Add credits section --- README.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.rst b/README.rst index 3b960fc7..94a55102 100644 --- a/README.rst +++ b/README.rst @@ -366,6 +366,14 @@ This package is maintained by Timo Stollenwerk . If you are having issues, please `let us know `_. +Credits +------- + +plone.rest has been written by Timo Stollenwerk (`kitconcept GmbH `_) and Ramon Navarro Bosch (`Iskra `_). + +plone.rest was added as a Plone core package with Plone 5.2 (see ``_). + + License ------- From f471ca2389aa557ba2f12d65eee11a8300fc56cc Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sun, 29 Jan 2023 08:51:26 +0100 Subject: [PATCH 35/95] Make rstcheck happy --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 94a55102..8578eb90 100644 --- a/README.rst +++ b/README.rst @@ -70,7 +70,7 @@ plone.rest allows you to register HTTP verbs for Plone content with ZCML. This is how you would register a PATCH request on Dexterity content: -.. code-block:: xml +.. code-block:: XML Date: Sun, 29 Jan 2023 08:52:02 +0100 Subject: [PATCH 36/95] Exclude all requirement files from release --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index bbb2eef8..161b8856 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,7 @@ exclude .flake8 exclude bootstrap-buildout.py exclude Makefile exclude requirements.txt +exclude requirements-*.txt exclude CODEOWNERS global-exclude *.pyc include pyproject.toml From 309da9ece0017a14823172fdc4d4a4dd1e4d4b8e Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sun, 29 Jan 2023 09:15:08 +0100 Subject: [PATCH 37/95] Fix Plone 6 buildout --- plone-6.0.x.cfg | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plone-6.0.x.cfg b/plone-6.0.x.cfg index bcb63780..07cbb269 100644 --- a/plone-6.0.x.cfg +++ b/plone-6.0.x.cfg @@ -3,11 +3,6 @@ extends = https://dist.plone.org/release/6.0.0/versions.cfg base.cfg -[buildout:python38] -parts = - test - code-analysis - [instance] recipe = plone.recipe.zope2instance zodb-temporary-storage = off From dc7dd0003d39adcd0ca15a592e7e14a7c4af216f Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sun, 29 Jan 2023 09:15:29 +0100 Subject: [PATCH 38/95] Preparing release 3.0.0 --- CHANGES.rst | 22 ++++++++++++++++++++++ news/135.breaking | 3 --- news/140.breaking | 2 -- news/143.feature | 2 -- news/147.feature | 2 -- setup.py | 2 +- 6 files changed, 23 insertions(+), 10 deletions(-) delete mode 100644 news/135.breaking delete mode 100644 news/140.breaking delete mode 100644 news/143.feature delete mode 100644 news/147.feature diff --git a/CHANGES.rst b/CHANGES.rst index 9fa5ebbf..81d22405 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,28 @@ Changelog .. towncrier release notes start +3.0.0 (2023-01-29) +------------------ + +Breaking changes: + + +- Change the HTTP status from 301 (Moved Permanently) to 302 (Found) for GET requests and to 307 (Temporary Redirect) for other request methods. + This fixes problems when an existing redirect is re-used. + [mamico] (#135) +- Drop official support for Plone 4.3, 5.0 and 5.1 (most likely the package will continue to work though) + [tisto] (#140) + + +New features: + + +- Add official support for Plone 6 + [tisto] (#143) +- Add official support for Python 3.9, 3.10, and 3.11 + [tisto] (#147) + + 2.0.0 (2022-10-15) ------------------ diff --git a/news/135.breaking b/news/135.breaking deleted file mode 100644 index 3c989ecf..00000000 --- a/news/135.breaking +++ /dev/null @@ -1,3 +0,0 @@ -Change the HTTP status from 301 (Moved Permanently) to 302 (Found) for GET requests and to 307 (Temporary Redirect) for other request methods. -This fixes problems when an existing redirect is re-used. -[mamico] diff --git a/news/140.breaking b/news/140.breaking deleted file mode 100644 index dabb293f..00000000 --- a/news/140.breaking +++ /dev/null @@ -1,2 +0,0 @@ -Drop official support for Plone 4.3, 5.0 and 5.1 (most likely the package will continue to work though) -[tisto] \ No newline at end of file diff --git a/news/143.feature b/news/143.feature deleted file mode 100644 index 16a5e19a..00000000 --- a/news/143.feature +++ /dev/null @@ -1,2 +0,0 @@ -Add official support for Plone 6 -[tisto] diff --git a/news/147.feature b/news/147.feature deleted file mode 100644 index 6d1e5fe5..00000000 --- a/news/147.feature +++ /dev/null @@ -1,2 +0,0 @@ -Add official support for Python 3.9, 3.10, and 3.11 -[tisto] diff --git a/setup.py b/setup.py index 28620432..05176fb9 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "2.0.1.dev0" +version = "3.0.0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 5d0f4dcb15617c6fac84299e23b4d3c45aa36664 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sun, 29 Jan 2023 09:16:02 +0100 Subject: [PATCH 39/95] Back to development: 3.0.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 05176fb9..20a32dd9 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "3.0.0" +version = "3.0.1.dev0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 77c8f06aaca015cc466d6da7c6c9e0323a561b1d Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Wed, 17 May 2023 16:20:03 +0200 Subject: [PATCH 40/95] Dont publish items that are acquired. This has been lifted from collective.explicitacquisition --- base.cfg | 1 + news/explicitacquisition.bugfix | 2 ++ src/plone/rest/configure.zcml | 4 ++++ src/plone/rest/explicitacquisition.py | 8 ++++++++ 4 files changed, 15 insertions(+) create mode 100644 news/explicitacquisition.bugfix create mode 100644 src/plone/rest/explicitacquisition.py diff --git a/base.cfg b/base.cfg index 10182f54..49e57836 100644 --- a/base.cfg +++ b/base.cfg @@ -63,3 +63,4 @@ eggs = plone.dexterity = git git://github.com/plone/plone.dexterity.git pushurl=git@github.com:plone/plone.dexterity.git branch=plip-680 plone.rest = git git://github.com/plone/plone.rest.git pushurl=git@github.com:plone/plone.rest.git branch=master Products.CMFPlone = git git://github.com/plone/Products.CMFPlone.git pushurl=git@github.com:plone/Products.CMFPlone.git branch=4.3.x-plip-680 +Products.CMFCore = git git://github.com/zopefoundation/Products.CMFCore.git branch=explicitacquisition diff --git a/news/explicitacquisition.bugfix b/news/explicitacquisition.bugfix new file mode 100644 index 00000000..37d8c635 --- /dev/null +++ b/news/explicitacquisition.bugfix @@ -0,0 +1,2 @@ +- Make REST endpoints check for acquired items. + [jaroel] diff --git a/src/plone/rest/configure.zcml b/src/plone/rest/configure.zcml index f03a0e85..ecfbd1ad 100644 --- a/src/plone/rest/configure.zcml +++ b/src/plone/rest/configure.zcml @@ -26,4 +26,8 @@ provides="zope.interface.Interface" /> + + diff --git a/src/plone/rest/explicitacquisition.py b/src/plone/rest/explicitacquisition.py new file mode 100644 index 00000000..b5b32f9c --- /dev/null +++ b/src/plone/rest/explicitacquisition.py @@ -0,0 +1,8 @@ +from zope.component import adapter +from Products.CMFCore.interfaces import IExplicitAcquisitionPublishingAllowed +from plone.rest.traverse import RESTWrapper + + +@adapter(RESTWrapper) +def rest_allowed(wrapper): + return IExplicitAcquisitionPublishingAllowed(wrapper.context) From cbe58a3cc2da4894e8657c637f998ac4c5606cde Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Thu, 18 May 2023 11:23:19 +0200 Subject: [PATCH 41/95] Renamed IExplicitAcquisitionPublishingAllowed to IShouldAllowAcquiredItemPublication --- src/plone/rest/configure.zcml | 2 +- src/plone/rest/explicitacquisition.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plone/rest/configure.zcml b/src/plone/rest/configure.zcml index ecfbd1ad..6d745378 100644 --- a/src/plone/rest/configure.zcml +++ b/src/plone/rest/configure.zcml @@ -27,7 +27,7 @@ /> diff --git a/src/plone/rest/explicitacquisition.py b/src/plone/rest/explicitacquisition.py index b5b32f9c..95ce6e33 100644 --- a/src/plone/rest/explicitacquisition.py +++ b/src/plone/rest/explicitacquisition.py @@ -1,8 +1,8 @@ from zope.component import adapter -from Products.CMFCore.interfaces import IExplicitAcquisitionPublishingAllowed +from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication from plone.rest.traverse import RESTWrapper @adapter(RESTWrapper) def rest_allowed(wrapper): - return IExplicitAcquisitionPublishingAllowed(wrapper.context) + return IShouldAllowAcquiredItemPublication(wrapper.context) From 7f71de123ebc2106a0bae94053aa488347f2ed7a Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Thu, 18 May 2023 17:06:20 +0200 Subject: [PATCH 42/95] Add some tests --- .../rest/tests/test_explicitacquisition.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/plone/rest/tests/test_explicitacquisition.py diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py new file mode 100644 index 00000000..977f8a5f --- /dev/null +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -0,0 +1,49 @@ +import unittest +from base64 import b64encode + +from plone.app.testing import ( + SITE_OWNER_NAME, + SITE_OWNER_PASSWORD, + TEST_USER_ID, + setRoles, +) +from zExceptions import NotFound +from zope.event import notify +from ZPublisher.pubevents import PubAfterTraversal, PubStart + +from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING + + +class TestExplicitAcquisition(unittest.TestCase): + layer = PLONE_REST_INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer["portal"] + self.request = self.layer["request"] + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + self.portal.invokeFactory("Document", id="foo") + + def traverse(self, path="/plone", accept="application/json", method="GET"): + request = self.layer["request"] + request.environ["PATH_INFO"] = path + request.environ["PATH_TRANSLATED"] = path + request.environ["HTTP_ACCEPT"] = accept + request.environ["REQUEST_METHOD"] = method + auth = "%s:%s" % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + b64auth = b64encode(auth.encode("utf8")) + request._auth = "Basic %s" % b64auth.decode("utf8") + notify(PubStart(request)) + return request.traverse(path) + + def test_portal_root(self): + self.traverse("/plone") + notify(PubAfterTraversal(self.request)) + + def test_portal_foo(self): + self.traverse("/plone/foo") + notify(PubAfterTraversal(self.request)) + + def test_portal_foo_acquired(self): + self.traverse("/plone/foo/foo") + with self.assertRaises(NotFound): + notify(PubAfterTraversal(self.request)) From db0cb9adb7f483bffb3e1ab06d1270c7db0b04a8 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Tue, 19 Sep 2023 12:16:00 +0200 Subject: [PATCH 43/95] When ++api++ is in the url multiple times, redirect to the proper url. When the url is badly formed, for example `++api++/something/++api++`, give a 404 NotFound. Fixes a denial of service. --- news/1.bugfix | 5 +++++ src/plone/rest/tests/test_traversal.py | 30 ++++++++++++++++++++++++++ src/plone/rest/traverse.py | 13 +++++++++++ 3 files changed, 48 insertions(+) create mode 100644 news/1.bugfix diff --git a/news/1.bugfix b/news/1.bugfix new file mode 100644 index 00000000..019268d5 --- /dev/null +++ b/news/1.bugfix @@ -0,0 +1,5 @@ +When ``++api++`` is in the url multiple times, redirect to the proper url. +When the url is badly formed, for example ``++api++/something/++api++``, give a 404 NotFound. +Fixes a denial of service. +See `security advisory `_. +[maurits] diff --git a/src/plone/rest/tests/test_traversal.py b/src/plone/rest/tests/test_traversal.py index 963d1c43..5d5389db 100644 --- a/src/plone/rest/tests/test_traversal.py +++ b/src/plone/rest/tests/test_traversal.py @@ -10,6 +10,8 @@ from plone.app.testing import TEST_USER_ID from plone.rest.service import Service from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING +from zExceptions import NotFound +from zExceptions import Redirect from zope.event import notify from zope.interface import alsoProvides from zope.publisher.interfaces.browser import IBrowserView @@ -106,6 +108,34 @@ def test_html_request_on_existing_view_returns_view(self): obj = self.traverse(path="/plone/folder1/search", accept="text/html") self.assertFalse(isinstance(obj, Service), "Got a service") + def test_html_request_via_api_returns_service(self): + obj = self.traverse(path="/plone/++api++", accept="text/html") + self.assertTrue(isinstance(obj, Service), "Not a service") + + def test_html_request_via_double_apis_raises_redirect(self): + portal_url = self.portal.absolute_url() + with self.assertRaises(Redirect) as exc: + self.traverse(path="/plone/++api++/++api++", accept="text/html") + self.assertEqual( + exc.exception.headers["Location"], + f"{portal_url}/++api++", + ) + + def test_html_request_via_multiple_apis_raises_redirect(self): + portal_url = self.portal.absolute_url() + with self.assertRaises(Redirect) as exc: + self.traverse( + path="/plone/++api++/++api++/++api++/search", accept="text/html" + ) + self.assertEqual( + exc.exception.headers["Location"], + f"{portal_url}/++api++/search", + ) + + def test_html_request_via_multiple_bad_apis_raises_not_found(self): + with self.assertRaises(NotFound): + self.traverse(path="/plone/++api++/search/++api++", accept="text/html") + def test_virtual_hosting(self): app = self.layer["app"] vhm = VirtualHostMonster() diff --git a/src/plone/rest/traverse.py b/src/plone/rest/traverse.py index f8d4a233..0a151c8c 100644 --- a/src/plone/rest/traverse.py +++ b/src/plone/rest/traverse.py @@ -5,6 +5,7 @@ from plone.rest.interfaces import IAPIRequest from plone.rest.interfaces import IService from plone.rest.events import mark_as_api_request +from zExceptions import Redirect from zope.component import adapter from zope.component import queryMultiAdapter from zope.interface import implementer @@ -64,6 +65,18 @@ def __init__(self, context, request): self.request = request def traverse(self, name_ignored, subpath_ignored): + name = "/++api++" + url = self.request.ACTUAL_URL + if url.count(name) > 1: + # Redirect to proper url. + while f"{name}{name}" in url: + url = url.replace(f"{name}{name}", name) + if url.count(name) > 1: + # Something like: .../++api++/something/++api++ + # Return nothing, so a NotFound is raised. + return + # Raise a redirect exception to stop execution of the current request. + raise Redirect(url) mark_as_api_request(self.request, "application/json") return self.context From 6d51874885df250d85b9869bfc921e99f75dc94d Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Thu, 21 Sep 2023 13:17:28 +0200 Subject: [PATCH 44/95] Preparing release 3.0.1 [ci skip] --- CHANGES.rst | 13 +++++++++++++ news/1.bugfix | 5 ----- setup.py | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) delete mode 100644 news/1.bugfix diff --git a/CHANGES.rst b/CHANGES.rst index 81d22405..971ed441 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,19 @@ Changelog .. towncrier release notes start +3.0.1 (2023-09-21) +------------------ + +Bug fixes: + + +- When ``++api++`` is in the url multiple times, redirect to the proper url. + When the url is badly formed, for example ``++api++/something/++api++``, give a 404 NotFound. + Fixes a denial of service. + See `security advisory `_. + [maurits] (#1) + + 3.0.0 (2023-01-29) ------------------ diff --git a/news/1.bugfix b/news/1.bugfix deleted file mode 100644 index 019268d5..00000000 --- a/news/1.bugfix +++ /dev/null @@ -1,5 +0,0 @@ -When ``++api++`` is in the url multiple times, redirect to the proper url. -When the url is badly formed, for example ``++api++/something/++api++``, give a 404 NotFound. -Fixes a denial of service. -See `security advisory `_. -[maurits] diff --git a/setup.py b/setup.py index 20a32dd9..24c6f03a 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "3.0.1.dev0" +version = "3.0.1" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 0958370c181244db6f4cf8bd010cd30cfb11cc3b Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Thu, 21 Sep 2023 13:17:59 +0200 Subject: [PATCH 45/95] Back to development: 3.0.2 [ci skip] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 24c6f03a..5b271a05 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "3.0.1" +version = "3.0.2.dev0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From de3266d7a9c4aee9ac4977dbd5df5d272eca32a3 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Fri, 22 Sep 2023 09:54:23 +0200 Subject: [PATCH 46/95] Drop Python 2.7, 3.6 and 3.7 (#156) * Upgrade to Plone 6.0.7 * Drop Python 2.7 support * Add changelog, remove 3.6 and 3.7 * Don't break on py 3.6 and 3.7, just drop official support * python_requires>=3.8 --- .github/workflows/tests.yml | 4 +--- news/141.breaking | 1 + plone-6.0.x.cfg | 2 +- setup.py | 7 +++---- 4 files changed, 6 insertions(+), 8 deletions(-) create mode 100644 news/141.breaking diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ad015c6a..1ec8c652 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,11 +6,9 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.10", "3.9", "3.8", "2.7"] + python-version: ["3.11", "3.10", "3.9", "3.8"] plone-version: ["6.0", "5.2"] exclude: - - python-version: 2.7 - plone-version: 6.0 - python-version: 3.11 plone-version: 5.2 - python-version: 3.10 diff --git a/news/141.breaking b/news/141.breaking new file mode 100644 index 00000000..e86d4ff7 --- /dev/null +++ b/news/141.breaking @@ -0,0 +1 @@ +Drop support for Python 2.7, 3.6 and 3.7 @tisto \ No newline at end of file diff --git a/plone-6.0.x.cfg b/plone-6.0.x.cfg index 07cbb269..0ec46f1e 100644 --- a/plone-6.0.x.cfg +++ b/plone-6.0.x.cfg @@ -1,6 +1,6 @@ [buildout] extends = - https://dist.plone.org/release/6.0.0/versions.cfg + https://dist.plone.org/release/6.0.7/versions.cfg base.cfg [instance] diff --git a/setup.py b/setup.py index 5b271a05..76adb4cc 100644 --- a/setup.py +++ b/setup.py @@ -31,13 +31,12 @@ def read(*rnames): "License :: OSI Approved :: GNU General Public License (GPL)", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development :: Libraries :: Python Modules", ], keywords="rest http", author="Plone Foundation", @@ -49,7 +48,7 @@ def read(*rnames): namespace_packages=["plone"], include_package_data=True, zip_safe=False, - python_requires=">=2.7", + python_requires=">=3.8", extras_require=dict( test=[ "plone.app.testing[robot]>=4.2.2", From df7fc7d58d46e6ff4d894b3d34dcbd644649f643 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Fri, 22 Sep 2023 10:00:42 +0200 Subject: [PATCH 47/95] Use ubuntu-latest and rename GHA tests --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ad015c6a..921960c4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,8 +1,8 @@ -name: plone.rest CI +name: Tests on: [push] jobs: build: - runs-on: "ubuntu-20.04" # needed to keep Python 2.7 + runs-on: ubuntu-latest strategy: fail-fast: false matrix: From f6d7bc858786fd99ed515e63d6703f1f1c866172 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Fri, 22 Sep 2023 10:12:37 +0200 Subject: [PATCH 48/95] Fix CI badge in README. Update checkouts to use main instead of master --- README.rst | 4 ++-- base.cfg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 8578eb90..a47e4af4 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ -.. image:: https://github.com/plone/plone.rest/workflows/plone.rest%20CI/badge.svg +.. image:: https://github.com/plone/plone.rest/actions/workflows/tests.yml/badge.svg :alt: Github Actions Status - :target: https://github.com/plone/plone.rest/actions?query=workflow%3A%22plone.rest+CI%22 + :target: https://github.com/plone/plone.rest/actions/workflows/tests.yml .. image:: https://img.shields.io/coveralls/github/plone/plone.rest.svg :alt: Coveralls github diff --git a/base.cfg b/base.cfg index 10182f54..51763bc8 100644 --- a/base.cfg +++ b/base.cfg @@ -61,5 +61,5 @@ eggs = [sources] plone.dexterity = git git://github.com/plone/plone.dexterity.git pushurl=git@github.com:plone/plone.dexterity.git branch=plip-680 -plone.rest = git git://github.com/plone/plone.rest.git pushurl=git@github.com:plone/plone.rest.git branch=master +plone.rest = git git://github.com/plone/plone.rest.git pushurl=git@github.com:plone/plone.rest.git branch=main Products.CMFPlone = git git://github.com/plone/Products.CMFPlone.git pushurl=git@github.com:plone/Products.CMFPlone.git branch=4.3.x-plip-680 From 576f0002af04a831fb7aca035efd30addd2950e5 Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Fri, 22 Sep 2023 15:54:02 +0200 Subject: [PATCH 49/95] Use released Products.CMFCore --- base.cfg | 1 - setup.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/base.cfg b/base.cfg index 69bdf16f..51763bc8 100644 --- a/base.cfg +++ b/base.cfg @@ -63,4 +63,3 @@ eggs = plone.dexterity = git git://github.com/plone/plone.dexterity.git pushurl=git@github.com:plone/plone.dexterity.git branch=plip-680 plone.rest = git git://github.com/plone/plone.rest.git pushurl=git@github.com:plone/plone.rest.git branch=main Products.CMFPlone = git git://github.com/plone/Products.CMFPlone.git pushurl=git@github.com:plone/Products.CMFPlone.git branch=4.3.x-plip-680 -Products.CMFCore = git git://github.com/zopefoundation/Products.CMFCore.git branch=explicitacquisition diff --git a/setup.py b/setup.py index 76adb4cc..acaba841 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ def read(*rnames): "plone.app.testing[robot]>=4.2.2", "plone.app.robotframework", "plone.dexterity", - "Products.CMFCore", + "Products.CMFCore>=3.1", "requests", ] ), @@ -65,7 +65,7 @@ def read(*rnames): "zope.interface", "zope.publisher", "zope.traversing", - "Products.CMFCore", + "Products.CMFCore>=3.1", "Zope2", "six", ], From bd6d1124abcc481c8847c09132592d821ce97d27 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Fri, 22 Sep 2023 18:37:06 +0200 Subject: [PATCH 50/95] Update Plone/Python support section. (#158) * Update Plone/Python support section. * Update README.rst Co-authored-by: Steve Piercy * Update README.rst Co-authored-by: Steve Piercy * Update README * Update README.rst Co-authored-by: Maurits van Rees --------- Co-authored-by: Steve Piercy Co-authored-by: Maurits van Rees --- README.rst | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index a47e4af4..86e24f73 100644 --- a/README.rst +++ b/README.rst @@ -315,16 +315,11 @@ and then running "bin/buildout" Plone/Python Support -------------------- -plone.restapi currently supports Plone 6 and 5.2. +plone.rest 4.x.x supports Plone 5.2 and 6.x on Python 3.8 and newer. -plone.restapi supports Python 2.7 and 3.8 for Plone 5.2 and Python 3.8, 3.9, 3.10, and 3.11 for Plone 6. - -Older versions of Python and Plone most likely will continue to work with plone.rest. - -Though, we do not test or officially support them. - -Check older versions of plone.rest for official support. +plone.rest 3.x.x supports Plone 5.2 on Python 2.7 and 3.6 to 3.8 and Plone 6.0 on Python 3.8 to 3.11. +If you need to use Plone 4.3, 5.0, or 5.1 on Python 2.7, check out plone.rest 2.x.x or 1.x.x. Redirects --------- From 3c79d7cd35dfba049a3d793b728319f505b85aef Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Fri, 22 Sep 2023 18:54:54 +0200 Subject: [PATCH 51/95] Add .pre-commit-config.yaml --- .pre-commit-config.yaml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..90184b8e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +# Generated from: +# https://github.com/plone/meta/tree/master/config/default +ci: + autofix_prs: false + autoupdate_schedule: monthly + +repos: + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.2 + hooks: + - id: pyupgrade + args: [--py38-plus] + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + - repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + - repo: https://github.com/codespell-project/codespell + rev: v2.2.4 + hooks: + - id: codespell + additional_dependencies: + - tomli + - repo: https://github.com/mgedmin/check-manifest + rev: "0.49" + hooks: + - id: check-manifest + - repo: https://github.com/regebro/pyroma + rev: "4.2" + hooks: + - id: pyroma From f61e3c7de95580a4413c6cabc78f13d1aa4e78cd Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Fri, 22 Sep 2023 20:07:40 +0200 Subject: [PATCH 52/95] Minor changelog amendment for #141 --- news/141.breaking | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/141.breaking b/news/141.breaking index e86d4ff7..1e6bade4 100644 --- a/news/141.breaking +++ b/news/141.breaking @@ -1 +1 @@ -Drop support for Python 2.7, 3.6 and 3.7 @tisto \ No newline at end of file +Drop support for Python 2.7, 3.6, and 3.7 @tisto \ No newline at end of file From 21f9de396aed3481efbb23a36ef1e0f0188dc95f Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Fri, 22 Sep 2023 20:09:17 +0200 Subject: [PATCH 53/95] Ignore pre-commit on MANIFEST --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 161b8856..79842040 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,6 +8,7 @@ exclude Makefile exclude requirements.txt exclude requirements-*.txt exclude CODEOWNERS +exclude .pre-commit-config.yaml global-exclude *.pyc include pyproject.toml recursive-exclude news * From 64c5be3f122c9a14c040364f74b13f7af5ab1cab Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Fri, 22 Sep 2023 21:15:38 +0200 Subject: [PATCH 54/95] Preparing release 4.0.0 --- CHANGES.rst | 9 +++++++++ news/141.breaking | 1 - setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/141.breaking diff --git a/CHANGES.rst b/CHANGES.rst index 971ed441..fc4e18a5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,15 @@ Changelog .. towncrier release notes start +4.0.0 (2023-09-22) +------------------ + +Breaking changes: + + +- Drop support for Python 2.7, 3.6, and 3.7 @tisto (#141) + + 3.0.1 (2023-09-21) ------------------ diff --git a/news/141.breaking b/news/141.breaking deleted file mode 100644 index 1e6bade4..00000000 --- a/news/141.breaking +++ /dev/null @@ -1 +0,0 @@ -Drop support for Python 2.7, 3.6, and 3.7 @tisto \ No newline at end of file diff --git a/setup.py b/setup.py index 76adb4cc..f854ca3a 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "3.0.2.dev0" +version = "4.0.0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 6bb29039d62f325e26f0f55142dea97215b5744f Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Fri, 22 Sep 2023 21:16:17 +0200 Subject: [PATCH 55/95] Back to development: 4.0.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f854ca3a..1562c14f 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "4.0.0" +version = "4.0.1.dev0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 33cbd4006c51504b75f0d959398306142fc4b7e8 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Fri, 22 Sep 2023 22:03:45 +0200 Subject: [PATCH 56/95] Use src instead of released egg --- Makefile | 2 +- plone-6.0.x.cfg | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 369383b4..0a28fd41 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SHELL := /bin/bash CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) -version = 3.8 +version = 3.9 # We like colors # From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects diff --git a/plone-6.0.x.cfg b/plone-6.0.x.cfg index 0ec46f1e..746358a2 100644 --- a/plone-6.0.x.cfg +++ b/plone-6.0.x.cfg @@ -8,5 +8,6 @@ recipe = plone.recipe.zope2instance zodb-temporary-storage = off [versions] +plone.rest = black = 21.7b0 pygments = 2.14.0 \ No newline at end of file From 79f1d19f00582fbf49af8ca45533fa065de1ddd4 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sun, 1 Oct 2023 20:04:11 +0200 Subject: [PATCH 57/95] Pre commit tisto (#161) * Run pre-commit.ci, fix three typos * Use same black version as pre-commit.ci * Set black version to pre-commit.ci one * black * Add isort/black config to pyproject.toml --- CHANGES.rst | 4 +- Makefile | 1 + README.rst | 2 +- plone-5.2.x.cfg | 2 +- plone-6.0.x.cfg | 2 +- pyproject.toml | 8 +- setup.py | 5 +- src/plone/rest/__init__.py | 1 - src/plone/rest/cors.py | 4 +- src/plone/rest/demo.py | 1 - src/plone/rest/errors.py | 18 +++-- src/plone/rest/events.py | 1 - src/plone/rest/interfaces.py | 1 - src/plone/rest/negotiation.py | 4 +- src/plone/rest/patches.py | 1 - src/plone/rest/service.py | 5 +- src/plone/rest/testing.py | 5 +- src/plone/rest/tests/__init__.py | 1 - src/plone/rest/tests/test_cors.py | 7 +- src/plone/rest/tests/test_dexterity.py | 88 ++++++++++----------- src/plone/rest/tests/test_dispatching.py | 8 +- src/plone/rest/tests/test_error_handling.py | 16 ++-- src/plone/rest/tests/test_named_services.py | 18 ++--- src/plone/rest/tests/test_negotiation.py | 15 ++-- src/plone/rest/tests/test_permissions.py | 12 ++- src/plone/rest/tests/test_redirects.py | 2 - src/plone/rest/tests/test_siteroot.py | 32 ++++---- src/plone/rest/tests/test_traversal.py | 10 +-- src/plone/rest/traverse.py | 23 +++--- src/plone/rest/zcml.py | 75 +++++++++--------- versions.cfg | 2 +- 31 files changed, 174 insertions(+), 200 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index fc4e18a5..dbb79003 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -245,7 +245,7 @@ Bugfixes: [buchi] - Fallback to regular views during traversal to ensure compatibility with - views beeing called with a specific Accept header. + views being called with a specific Accept header. [buchi] @@ -295,7 +295,7 @@ Bugfixes: - Refactor traversal of REST requests by using a traversal adapter on the site root instead of a traversal adapter for each REST service. This prevents - REST services from being overriden by other traversal adapters. + REST services from being overridden by other traversal adapters. [buchi] diff --git a/Makefile b/Makefile index 0a28fd41..cffb9942 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,7 @@ build-plone-6.0: ## Build Plone 6.0 python$(version) -m venv . bin/pip install --upgrade pip bin/pip install -r requirements-6.0.x.txt + bin/pip install pip install black==$$(awk '/^black =/{print $$NF}' versions.cfg) bin/buildout -c plone-6.0.x.cfg .PHONY: Test diff --git a/README.rst b/README.rst index 86e24f73..6c3a78d6 100644 --- a/README.rst +++ b/README.rst @@ -271,7 +271,7 @@ allow_origin allow_methods A comma separated list of HTTP method names that are allowed by this CORS policy, e.g. "DELETE,GET,OPTIONS,PATCH,POST,PUT". If not specified, all - methods for which there's a service registerd are allowed. + methods for which there's a service registered are allowed. allow_credentials Indicates whether the resource supports user credentials in the request. diff --git a/plone-5.2.x.cfg b/plone-5.2.x.cfg index 82f35ea2..3b37a172 100644 --- a/plone-5.2.x.cfg +++ b/plone-5.2.x.cfg @@ -7,7 +7,7 @@ versions=versions [versions] plone.rest = -black = 21.12b0 +black = 23.3.0 # Error: The requirement ('virtualenv>=20.0.35') is not allowed by your [versions] constraint (20.0.26) virtualenv = 20.0.35 diff --git a/plone-6.0.x.cfg b/plone-6.0.x.cfg index 746358a2..f1fead3e 100644 --- a/plone-6.0.x.cfg +++ b/plone-6.0.x.cfg @@ -9,5 +9,5 @@ zodb-temporary-storage = off [versions] plone.rest = -black = 21.7b0 +black = 23.3.0 pygments = 2.14.0 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index da067fac..a9ead58a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,4 +22,10 @@ showcontent = true [[tool.towncrier.type]] directory = "internal" name = "Internal:" -showcontent = true \ No newline at end of file +showcontent = true + +[tool.isort] +profile = "plone" + +[tool.black] +target-version = ["py38"] \ No newline at end of file diff --git a/setup.py b/setup.py index 1562c14f..7b8c67cf 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ -import os +from setuptools import find_packages +from setuptools import setup -from setuptools import find_packages, setup +import os def read(*rnames): diff --git a/src/plone/rest/__init__.py b/src/plone/rest/__init__.py index 44646e48..ab37f224 100644 --- a/src/plone/rest/__init__.py +++ b/src/plone/rest/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- from plone.rest.service import Service # noqa diff --git a/src/plone/rest/cors.py b/src/plone/rest/cors.py index dbc2035b..737bed84 100644 --- a/src/plone/rest/cors.py +++ b/src/plone/rest/cors.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- from plone.rest.interfaces import ICORSPolicy from zope.interface import implementer + # CORS preflight service registry # A mapping of method -> service_id _services = {} @@ -19,7 +19,7 @@ def lookup_preflight_service_id(method): @implementer(ICORSPolicy) -class CORSPolicy(object): +class CORSPolicy: def __init__(self, context, request): self.context = context self.request = request diff --git a/src/plone/rest/demo.py b/src/plone/rest/demo.py index 4622f978..4a3bcf57 100644 --- a/src/plone/rest/demo.py +++ b/src/plone/rest/demo.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from plone.rest import Service import json diff --git a/src/plone/rest/errors.py b/src/plone/rest/errors.py index 454d48ee..d6a9c536 100644 --- a/src/plone/rest/errors.py +++ b/src/plone/rest/errors.py @@ -1,5 +1,6 @@ from AccessControl import getSecurityManager + try: from plone.app.redirector.interfaces import IRedirectionStorage except ImportError: @@ -10,10 +11,11 @@ from Products.CMFCore.permissions import ManagePortal from Products.Five.browser import BrowserView from six.moves import urllib -from six.moves.urllib.parse import quote -from six.moves.urllib.parse import unquote +from urllib.parse import quote +from urllib.parse import unquote from zExceptions import NotFound + try: from ZPublisher.HTTPRequest import WSGIRequest @@ -61,7 +63,7 @@ def render_exception(self, exception): if six.PY2: name = name.decode("utf-8") message = message.decode("utf-8") - result = {u"type": name, u"message": message} + result = {"type": name, "message": message} policy = queryMultiAdapter((self.context, self.request), ICORSPolicy) if policy is not None: @@ -77,10 +79,10 @@ def render_exception(self, exception): # NotFound exceptions need special handling because their # exception message gets turned into HTML by ZPublisher url = self.request.getURL() - result[u"message"] = u"Resource not found: %s" % url + result["message"] = "Resource not found: %s" % url if getSecurityManager().checkPermission(ManagePortal, getSite()): - result[u"traceback"] = self.render_traceback(exception) + result["traceback"] = self.render_traceback(exception) return result @@ -101,8 +103,8 @@ def render_traceback(self, exception): pass else: return ( - u"ERROR: Another exception happened before we could " - u"render the traceback." + "ERROR: Another exception happened before we could " + "render the traceback." ) raw = "\n".join(traceback.format_tb(exc_traceback)) @@ -192,7 +194,7 @@ def attempt_redirect(self): query_string = self.request.QUERY_STRING if query_string: - new_path = storage.get("%s?%s" % (old_path, query_string)) + new_path = storage.get(f"{old_path}?{query_string}") # if we matched on the query_string we don't want to include it # in redirect if new_path: diff --git a/src/plone/rest/events.py b/src/plone/rest/events.py index 49554d8b..536ea647 100644 --- a/src/plone/rest/events.py +++ b/src/plone/rest/events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from plone.rest.cors import lookup_preflight_service_id from plone.rest.interfaces import IAPIRequest from plone.rest.negotiation import lookup_service_id diff --git a/src/plone/rest/interfaces.py b/src/plone/rest/interfaces.py index ac182248..bf07258d 100644 --- a/src/plone/rest/interfaces.py +++ b/src/plone/rest/interfaces.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from zope.interface import Interface diff --git a/src/plone/rest/negotiation.py b/src/plone/rest/negotiation.py index 7a47d5ab..63f2f312 100644 --- a/src/plone/rest/negotiation.py +++ b/src/plone/rest/negotiation.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Service registry # A mapping of method -> type name -> subtype name -> service id _services = {} @@ -42,7 +40,7 @@ def register_service(method, media_type): """Register a service for the given request method and media type and return it's service id. """ - service_id = u"{}_{}_{}_".format(method, media_type[0], media_type[1]) + service_id = f"{method}_{media_type[0]}_{media_type[1]}_" types = _services.setdefault(method, {}) subtypes = types.setdefault(media_type[0], {}) subtypes[media_type[1]] = service_id diff --git a/src/plone/rest/patches.py b/src/plone/rest/patches.py index 97b618f1..8f4cb5dc 100644 --- a/src/plone/rest/patches.py +++ b/src/plone/rest/patches.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from plone.rest.interfaces import IAPIRequest diff --git a/src/plone/rest/service.py b/src/plone/rest/service.py index 351b111d..7761f8f7 100644 --- a/src/plone/rest/service.py +++ b/src/plone/rest/service.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from plone.rest.interfaces import ICORSPolicy from plone.rest.interfaces import IService from zope.component import queryMultiAdapter @@ -6,7 +5,7 @@ @implementer(IService) -class Service(object): +class Service: def __call__(self): policy = queryMultiAdapter((self.context, self.request), ICORSPolicy) if policy is not None: @@ -29,4 +28,4 @@ def __getattribute__(self, name): # include credentials if name == "__roles__" and self.request._rest_cors_preflight: return ["Anonymous"] - return super(Service, self).__getattribute__(name) + return super().__getattribute__(name) diff --git a/src/plone/rest/testing.py b/src/plone/rest/testing.py index 05268d94..bcd4eeb3 100644 --- a/src/plone/rest/testing.py +++ b/src/plone/rest/testing.py @@ -1,16 +1,13 @@ -# -*- coding: utf-8 -*- from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE from plone.app.testing import FunctionalTesting from plone.app.testing import IntegrationTesting from plone.app.testing import PloneSandboxLayer from plone.rest.service import Service from plone.testing import z2 - from zope.configuration import xmlconfig class PloneRestLayer(PloneSandboxLayer): - defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,) def setUpZope(self, app, configurationContext): @@ -31,7 +28,7 @@ def setUpZope(self, app, configurationContext): class InternalServerErrorService(Service): def __call__(self): - from six.moves.urllib.error import HTTPError + from urllib.error import HTTPError raise HTTPError( "http://nohost/plone/500-internal-server-error", diff --git a/src/plone/rest/tests/__init__.py b/src/plone/rest/tests/__init__.py index 40a96afc..e69de29b 100644 --- a/src/plone/rest/tests/__init__.py +++ b/src/plone/rest/tests/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/plone/rest/tests/test_cors.py b/src/plone/rest/tests/test_cors.py index 964ea1d9..ddf1f1a5 100644 --- a/src/plone/rest/tests/test_cors.py +++ b/src/plone/rest/tests/test_cors.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from ZPublisher.pubevents import PubStart from plone.app.testing import popGlobalRegistry from plone.app.testing import pushGlobalRegistry from plone.rest.cors import CORSPolicy @@ -10,12 +8,12 @@ from zope.event import notify from zope.interface import Interface from zope.publisher.interfaces.browser import IDefaultBrowserLayer +from ZPublisher.pubevents import PubStart import unittest class TestCORSPolicy(unittest.TestCase): - layer = PLONE_REST_INTEGRATION_TESTING def setUp(self): @@ -195,7 +193,6 @@ def test_preflight_cors_sets_status_code_200(self): class TestCORS(unittest.TestCase): - layer = PLONE_REST_INTEGRATION_TESTING def setUp(self): @@ -231,7 +228,7 @@ def test_simple_cors_gets_processed(self): def test_preflight_request_without_cors_policy_doesnt_render_service(self): # "Unregister" the current CORS policy - class NoCORSPolicy(object): + class NoCORSPolicy: def __new__(cls, context, request): return None diff --git a/src/plone/rest/tests/test_dexterity.py b/src/plone/rest/tests/test_dexterity.py index 288ae480..cc411021 100644 --- a/src/plone/rest/tests/test_dexterity.py +++ b/src/plone/rest/tests/test_dexterity.py @@ -1,25 +1,23 @@ -# -*- coding: utf-8 -*- from datetime import datetime from plone.app.testing import setRoles -from plone.app.testing import TEST_USER_ID from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID from plone.app.textfield.value import RichTextValue -from plone.namedfile.file import NamedBlobImage from plone.namedfile.file import NamedBlobFile +from plone.namedfile.file import NamedBlobImage from plone.rest.testing import PLONE_REST_FUNCTIONAL_TESTING from z3c.relationfield import RelationValue from zope.component import getUtility from zope.intid.interfaces import IIntIds -import unittest import os import requests import transaction +import unittest class TestDexterityServiceEndpoints(unittest.TestCase): - layer = PLONE_REST_FUNCTIONAL_TESTING def setUp(self): @@ -39,8 +37,8 @@ def test_dexterity_document_get(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(200, response.status_code) - self.assertEqual(u"doc1", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("doc1", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_document_post(self): response = requests.post( @@ -49,8 +47,8 @@ def test_dexterity_document_post(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(200, response.status_code) - self.assertEqual(u"doc1", response.json().get("id")) - self.assertEqual(u"POST", response.json().get("method")) + self.assertEqual("doc1", response.json().get("id")) + self.assertEqual("POST", response.json().get("method")) def test_dexterity_document_put(self): response = requests.put( @@ -59,8 +57,8 @@ def test_dexterity_document_put(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(200, response.status_code) - self.assertEqual(u"doc1", response.json().get("id")) - self.assertEqual(u"PUT", response.json().get("method")) + self.assertEqual("doc1", response.json().get("id")) + self.assertEqual("PUT", response.json().get("method")) def test_dexterity_document_patch(self): response = requests.patch( @@ -69,8 +67,8 @@ def test_dexterity_document_patch(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(200, response.status_code) - self.assertEqual(u"doc1", response.json().get("id")) - self.assertEqual(u"PATCH", response.json().get("method")) + self.assertEqual("doc1", response.json().get("id")) + self.assertEqual("PATCH", response.json().get("method")) def test_dexterity_document_delete(self): response = requests.delete( @@ -79,8 +77,8 @@ def test_dexterity_document_delete(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(200, response.status_code) - self.assertEqual(u"doc1", response.json().get("id")) - self.assertEqual(u"DELETE", response.json().get("method")) + self.assertEqual("doc1", response.json().get("id")) + self.assertEqual("DELETE", response.json().get("method")) def test_dexterity_document_options(self): response = requests.options( @@ -89,8 +87,8 @@ def test_dexterity_document_options(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(200, response.status_code) - self.assertEqual(u"doc1", response.json().get("id")) - self.assertEqual(u"OPTIONS", response.json().get("method")) + self.assertEqual("doc1", response.json().get("id")) + self.assertEqual("OPTIONS", response.json().get("method")) def test_dexterity_folder_get(self): self.portal.invokeFactory("Folder", id="folder") @@ -103,23 +101,23 @@ def test_dexterity_folder_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"folder", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("folder", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_news_item_get(self): self.portal.invokeFactory("News Item", id="newsitem") self.portal.newsitem.title = "My News Item" - self.portal.newsitem.description = u"This is a news item" + self.portal.newsitem.description = "This is a news item" self.portal.newsitem.text = RichTextValue( - u"Lorem ipsum", "text/plain", "text/html" + "Lorem ipsum", "text/plain", "text/html" ) - image_file = os.path.join(os.path.dirname(__file__), u"image.png") + image_file = os.path.join(os.path.dirname(__file__), "image.png") fd = open(image_file, "rb") self.portal.newsitem.image = NamedBlobImage( - data=fd.read(), contentType="image/png", filename=u"image.png" + data=fd.read(), contentType="image/png", filename="image.png" ) fd.close() - self.portal.newsitem.image_caption = u"This is an image caption." + self.portal.newsitem.image_caption = "This is an image caption." import transaction transaction.commit() @@ -130,13 +128,13 @@ def test_dexterity_news_item_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"newsitem", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("newsitem", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_event_get(self): self.portal.invokeFactory("Event", id="event") self.portal.event.title = "Event" - self.portal.event.description = u"This is an event" + self.portal.event.description = "This is an event" self.portal.event.start = datetime(2013, 1, 1, 10, 0) self.portal.event.end = datetime(2013, 1, 1, 12, 0) import transaction @@ -149,13 +147,13 @@ def test_dexterity_event_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"event", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("event", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_link_get(self): self.portal.invokeFactory("Link", id="link") self.portal.link.title = "My Link" - self.portal.link.description = u"This is a link" + self.portal.link.description = "This is a link" self.portal.remoteUrl = "http://plone.org" import transaction @@ -167,17 +165,17 @@ def test_dexterity_link_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"link", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("link", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_file_get(self): self.portal.invokeFactory("File", id="file") self.portal.file.title = "My File" - self.portal.file.description = u"This is a file" - pdf_file = os.path.join(os.path.dirname(__file__), u"file.pdf") + self.portal.file.description = "This is a file" + pdf_file = os.path.join(os.path.dirname(__file__), "file.pdf") fd = open(pdf_file, "rb") self.portal.file.file = NamedBlobFile( - data=fd.read(), contentType="application/pdf", filename=u"file.pdf" + data=fd.read(), contentType="application/pdf", filename="file.pdf" ) fd.close() intids = getUtility(IIntIds) @@ -194,17 +192,17 @@ def test_dexterity_file_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"file", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("file", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_image_get(self): self.portal.invokeFactory("Image", id="image") self.portal.image.title = "My Image" - self.portal.image.description = u"This is an image" - image_file = os.path.join(os.path.dirname(__file__), u"image.png") + self.portal.image.description = "This is an image" + image_file = os.path.join(os.path.dirname(__file__), "image.png") fd = open(image_file, "rb") self.portal.image.image = NamedBlobImage( - data=fd.read(), contentType="image/png", filename=u"image.png" + data=fd.read(), contentType="image/png", filename="image.png" ) fd.close() import transaction @@ -218,13 +216,13 @@ def test_dexterity_image_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"image", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("image", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_collection_get(self): self.portal.invokeFactory("Collection", id="collection") self.portal.collection.title = "My Collection" - self.portal.collection.description = u"This is a collection with two documents" + self.portal.collection.description = "This is a collection with two documents" self.portal.collection.query = [ { "i": "portal_type", @@ -243,5 +241,5 @@ def test_dexterity_collection_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"collection", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("collection", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) diff --git a/src/plone/rest/tests/test_dispatching.py b/src/plone/rest/tests/test_dispatching.py index fb9b7b84..5c9204d0 100644 --- a/src/plone/rest/tests/test_dispatching.py +++ b/src/plone/rest/tests/test_dispatching.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from plone.app.testing import setRoles from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD @@ -17,7 +16,6 @@ class DispatchingTestCase(unittest.TestCase): - layer = PLONE_REST_FUNCTIONAL_TESTING def setUp(self): @@ -54,7 +52,7 @@ def validate(self, expectations, follow_redirects=False): if failures: msg = "" - for (request_args, expected_status, actual_status) in failures: + for request_args, expected_status, actual_status in failures: msg += ( "\n" "Request: %s\n" @@ -137,7 +135,7 @@ def test_not_found_invalid_creds(self): class TestDispatchingDexterity(DispatchingTestCase): def setUp(self): - super(TestDispatchingDexterity, self).setUp() + super().setUp() self.portal.invokeFactory("Folder", id="private") self.portal.invokeFactory("Folder", id="public") @@ -216,7 +214,7 @@ def test_public_dx_folder_invalid_creds(self): class TestDispatchingRedirects(DispatchingTestCase): def setUp(self): - super(TestDispatchingRedirects, self).setUp() + super().setUp() self.portal.invokeFactory("Folder", id="private-old") self.portal.manage_renameObject("private-old", "private-new") diff --git a/src/plone/rest/tests/test_error_handling.py b/src/plone/rest/tests/test_error_handling.py index b2cf5111..32711a4b 100644 --- a/src/plone/rest/tests/test_error_handling.py +++ b/src/plone/rest/tests/test_error_handling.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- from plone.app.testing import setRoles -from plone.app.testing import TEST_USER_ID -from plone.app.testing import TEST_USER_PASSWORD from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID +from plone.app.testing import TEST_USER_PASSWORD from plone.rest.testing import PLONE_REST_FUNCTIONAL_TESTING import json @@ -13,7 +12,6 @@ class TestErrorHandling(unittest.TestCase): - layer = PLONE_REST_FUNCTIONAL_TESTING def setUp(self): @@ -83,7 +81,7 @@ def test_500_internal_server_error(self): self.assertEqual("HTTPError", response.json()["type"]) self.assertEqual( - {u"type": u"HTTPError", u"message": u"HTTP Error 500: InternalServerError"}, + {"type": "HTTPError", "message": "HTTP Error 500: InternalServerError"}, response.json(), ) @@ -94,7 +92,7 @@ def test_500_traceback_only_for_manager_users(self): headers={"Accept": "application/json"}, auth=(TEST_USER_ID, TEST_USER_PASSWORD), ) - self.assertNotIn(u"traceback", response.json()) + self.assertNotIn("traceback", response.json()) # Manager user response = requests.get( @@ -102,10 +100,10 @@ def test_500_traceback_only_for_manager_users(self): headers={"Accept": "application/json"}, auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) - self.assertIn(u"traceback", response.json()) + self.assertIn("traceback", response.json()) - traceback = response.json()[u"traceback"] + traceback = response.json()["traceback"] self.assertIsInstance(traceback, list) - self.assertRegexpMatches( + self.assertRegex( traceback[0], r'^File "[^"]*", line \d*, in (publish|transaction_pubevents)' ) diff --git a/src/plone/rest/tests/test_named_services.py b/src/plone/rest/tests/test_named_services.py index fe1977bb..05feb60d 100644 --- a/src/plone/rest/tests/test_named_services.py +++ b/src/plone/rest/tests/test_named_services.py @@ -1,17 +1,15 @@ -# -*- coding: utf-8 -*- from plone.app.testing import setRoles -from plone.app.testing import TEST_USER_ID from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID from plone.rest.testing import PLONE_REST_FUNCTIONAL_TESTING -import unittest import requests import transaction +import unittest class TestNamedServiceEndpoints(unittest.TestCase): - layer = PLONE_REST_FUNCTIONAL_TESTING def setUp(self): @@ -31,7 +29,7 @@ def test_dexterity_named_get(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual({u"service": u"named get"}, response.json()) + self.assertEqual({"service": "named get"}, response.json()) def test_dexterity_named_post(self): response = requests.post( @@ -40,7 +38,7 @@ def test_dexterity_named_post(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual({u"service": u"named post"}, response.json()) + self.assertEqual({"service": "named post"}, response.json()) def test_dexterity_named_put(self): response = requests.put( @@ -49,7 +47,7 @@ def test_dexterity_named_put(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual({u"service": u"named put"}, response.json()) + self.assertEqual({"service": "named put"}, response.json()) def test_dexterity_named_patch(self): response = requests.patch( @@ -58,7 +56,7 @@ def test_dexterity_named_patch(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual({u"service": u"named patch"}, response.json()) + self.assertEqual({"service": "named patch"}, response.json()) def test_dexterity_named_delete(self): response = requests.delete( @@ -67,7 +65,7 @@ def test_dexterity_named_delete(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual({u"service": u"named delete"}, response.json()) + self.assertEqual({"service": "named delete"}, response.json()) def test_dexterity_named_options(self): response = requests.options( @@ -76,4 +74,4 @@ def test_dexterity_named_options(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual({u"service": u"named options"}, response.json()) + self.assertEqual({"service": "named options"}, response.json()) diff --git a/src/plone/rest/tests/test_negotiation.py b/src/plone/rest/tests/test_negotiation.py index 70edd5ae..3978b887 100644 --- a/src/plone/rest/tests/test_negotiation.py +++ b/src/plone/rest/tests/test_negotiation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from plone.rest.negotiation import lookup_service_id from plone.rest.negotiation import parse_accept_header from plone.rest.negotiation import register_service @@ -61,24 +60,24 @@ def test_parse_invalid_accept_header(self): class TestServiceRegistry(unittest.TestCase): def test_register_media_type(self): self.assertEqual( - u"GET_application_json_", register_service("GET", ("application", "json")) + "GET_application_json_", register_service("GET", ("application", "json")) ) self.assertEqual( - u"GET_application_json_", lookup_service_id("GET", "application/json") + "GET_application_json_", lookup_service_id("GET", "application/json") ) def test_register_wildcard_subtype(self): - self.assertEqual(u"PATCH_text_*_", register_service("PATCH", ("text", "*"))) - self.assertEqual(u"PATCH_text_*_", lookup_service_id("PATCH", "text/xml")) + self.assertEqual("PATCH_text_*_", register_service("PATCH", ("text", "*"))) + self.assertEqual("PATCH_text_*_", lookup_service_id("PATCH", "text/xml")) def test_register_wilcard_type(self): - self.assertEqual(u"PATCH_*_*_", register_service("PATCH", ("*", "*"))) - self.assertEqual(u"PATCH_*_*_", lookup_service_id("PATCH", "foo/bar")) + self.assertEqual("PATCH_*_*_", register_service("PATCH", ("*", "*"))) + self.assertEqual("PATCH_*_*_", lookup_service_id("PATCH", "foo/bar")) def test_service_id_for_multiple_media_types_is_none(self): register_service("GET", "application/json") self.assertEqual( - None, lookup_service_id("GET", "application/json,application/javascipt") + None, lookup_service_id("GET", "application/json,application/javascript") ) def test_service_id_for_invalid_media_type_is_none(self): diff --git a/src/plone/rest/tests/test_permissions.py b/src/plone/rest/tests/test_permissions.py index 2ab0df6a..166b0974 100644 --- a/src/plone/rest/tests/test_permissions.py +++ b/src/plone/rest/tests/test_permissions.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- -from Products.CMFCore.utils import getToolByName -from ZPublisher.pubevents import PubStart from base64 import b64encode +from plone.app.testing import login +from plone.app.testing import setRoles from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_NAME from plone.app.testing import TEST_USER_PASSWORD -from plone.app.testing import login -from plone.app.testing import setRoles from plone.rest.service import Service from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING +from Products.CMFCore.utils import getToolByName from zExceptions import Unauthorized from zope.event import notify +from ZPublisher.pubevents import PubStart import unittest class TestPermissions(unittest.TestCase): - layer = PLONE_REST_INTEGRATION_TESTING def setUp(self): @@ -36,7 +34,7 @@ def traverse(self, path="/plone", accept="application/json", method="GET"): request.environ["PATH_TRANSLATED"] = path request.environ["HTTP_ACCEPT"] = accept request.environ["REQUEST_METHOD"] = method - auth = "%s:%s" % (TEST_USER_NAME, TEST_USER_PASSWORD) + auth = f"{TEST_USER_NAME}:{TEST_USER_PASSWORD}" b64auth = b64encode(auth.encode("utf8")) request._auth = "Basic %s" % b64auth.decode("utf8") notify(PubStart(request)) diff --git a/src/plone/rest/tests/test_redirects.py b/src/plone/rest/tests/test_redirects.py index 30122daf..a172a4d9 100644 --- a/src/plone/rest/tests/test_redirects.py +++ b/src/plone/rest/tests/test_redirects.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from BTrees.OOBTree import OOSet from plone.app.redirector.interfaces import IRedirectionStorage from plone.app.testing import setRoles @@ -15,7 +14,6 @@ class TestRedirects(unittest.TestCase): - layer = PLONE_REST_FUNCTIONAL_TESTING def setUp(self): diff --git a/src/plone/rest/tests/test_siteroot.py b/src/plone/rest/tests/test_siteroot.py index 6cf71d59..9e992938 100644 --- a/src/plone/rest/tests/test_siteroot.py +++ b/src/plone/rest/tests/test_siteroot.py @@ -1,16 +1,14 @@ -# -*- coding: utf-8 -*- -from plone.rest.testing import PLONE_REST_FUNCTIONAL_TESTING from plone.app.testing import setRoles -from plone.app.testing import TEST_USER_ID from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID +from plone.rest.testing import PLONE_REST_FUNCTIONAL_TESTING -import unittest import requests +import unittest class TestSiteRootServiceEndpoints(unittest.TestCase): - layer = PLONE_REST_FUNCTIONAL_TESTING def setUp(self): @@ -33,8 +31,8 @@ def test_siteroot_get(self): response.status_code ), ) - self.assertEqual(u"plone", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("plone", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_siteroot_post(self): response = requests.post( @@ -49,8 +47,8 @@ def test_siteroot_post(self): response.status_code ), ) - self.assertEqual(u"plone", response.json().get("id")) - self.assertEqual(u"POST", response.json().get("method")) + self.assertEqual("plone", response.json().get("id")) + self.assertEqual("POST", response.json().get("method")) def test_siteroot_delete(self): response = requests.delete( @@ -59,8 +57,8 @@ def test_siteroot_delete(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual(u"plone", response.json().get("id")) - self.assertEqual(u"DELETE", response.json().get("method")) + self.assertEqual("plone", response.json().get("id")) + self.assertEqual("DELETE", response.json().get("method")) def test_siteroot_put(self): response = requests.put( @@ -70,8 +68,8 @@ def test_siteroot_put(self): ) self.assertEqual(response.status_code, 200) - self.assertEqual(u"plone", response.json().get("id")) - self.assertEqual(u"PUT", response.json().get("method")) + self.assertEqual("plone", response.json().get("id")) + self.assertEqual("PUT", response.json().get("method")) def test_siteroot_patch(self): response = requests.patch( @@ -81,8 +79,8 @@ def test_siteroot_patch(self): ) self.assertEqual(response.status_code, 200) - self.assertEqual(u"plone", response.json().get("id")) - self.assertEqual(u"PATCH", response.json().get("method")) + self.assertEqual("plone", response.json().get("id")) + self.assertEqual("PATCH", response.json().get("method")) def test_siteroot_options(self): response = requests.options( @@ -91,5 +89,5 @@ def test_siteroot_options(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual(u"plone", response.json().get("id")) - self.assertEqual(u"OPTIONS", response.json().get("method")) + self.assertEqual("plone", response.json().get("id")) + self.assertEqual("OPTIONS", response.json().get("method")) diff --git a/src/plone/rest/tests/test_traversal.py b/src/plone/rest/tests/test_traversal.py index 5d5389db..43ee6327 100644 --- a/src/plone/rest/tests/test_traversal.py +++ b/src/plone/rest/tests/test_traversal.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from Products.SiteAccess.VirtualHostMonster import VirtualHostMonster -from ZPublisher import BeforeTraverse -from ZPublisher.pubevents import PubStart from base64 import b64encode from plone.app.layout.navigation.interfaces import INavigationRoot from plone.app.testing import setRoles @@ -10,17 +6,19 @@ from plone.app.testing import TEST_USER_ID from plone.rest.service import Service from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING +from Products.SiteAccess.VirtualHostMonster import VirtualHostMonster from zExceptions import NotFound from zExceptions import Redirect from zope.event import notify from zope.interface import alsoProvides from zope.publisher.interfaces.browser import IBrowserView +from ZPublisher import BeforeTraverse +from ZPublisher.pubevents import PubStart import unittest class TestTraversal(unittest.TestCase): - layer = PLONE_REST_INTEGRATION_TESTING def setUp(self): @@ -34,7 +32,7 @@ def traverse(self, path="/plone", accept="application/json", method="GET"): request.environ["PATH_TRANSLATED"] = path request.environ["HTTP_ACCEPT"] = accept request.environ["REQUEST_METHOD"] = method - auth = "%s:%s" % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + auth = f"{SITE_OWNER_NAME}:{SITE_OWNER_PASSWORD}" b64auth = b64encode(auth.encode("utf8")) request._auth = "Basic %s" % b64auth.decode("utf8") notify(PubStart(request)) diff --git a/src/plone/rest/traverse.py b/src/plone/rest/traverse.py index 0a151c8c..bd210312 100644 --- a/src/plone/rest/traverse.py +++ b/src/plone/rest/traverse.py @@ -1,24 +1,23 @@ -# -*- coding: utf-8 -*- -from Products.CMFCore.interfaces import ISiteRoot -from Products.SiteAccess.VirtualHostMonster import VirtualHostMonster -from ZPublisher.BaseRequest import DefaultPublishTraverse +from plone.rest.events import mark_as_api_request from plone.rest.interfaces import IAPIRequest from plone.rest.interfaces import IService -from plone.rest.events import mark_as_api_request +from Products.CMFCore.interfaces import IContentish +from Products.CMFCore.interfaces import ISiteRoot +from Products.SiteAccess.VirtualHostMonster import VirtualHostMonster from zExceptions import Redirect from zope.component import adapter from zope.component import queryMultiAdapter from zope.interface import implementer from zope.publisher.interfaces.browser import IBrowserPublisher from zope.traversing.interfaces import ITraversable -from Products.CMFCore.interfaces import IContentish +from ZPublisher.BaseRequest import DefaultPublishTraverse @adapter(ISiteRoot, IAPIRequest) class RESTTraverse(DefaultPublishTraverse): def publishTraverse(self, request, name): try: - obj = super(RESTTraverse, self).publishTraverse(request, name) + obj = super().publishTraverse(request, name) if not IContentish.providedBy(obj) and not IService.providedBy(obj): if isinstance(obj, VirtualHostMonster): return obj @@ -49,12 +48,12 @@ def publishTraverse(self, request, name): def browserDefault(self, request): # Called when we have reached the end of the path - # In our case this means an unamed service + # In our case this means an unnamed service return self.context, (request._rest_service_id,) @implementer(ITraversable) -class MarkAsRESTTraverser(object): +class MarkAsRESTTraverser: """ Traversal adapter for the ``++api++`` namespace. It marks the request as API request. @@ -82,7 +81,7 @@ def traverse(self, name_ignored, subpath_ignored): @implementer(IBrowserPublisher) -class RESTWrapper(object): +class RESTWrapper: """A wrapper for objects traversed during a REST request.""" def __init__(self, context, request): @@ -99,7 +98,7 @@ def __getitem__(self, name): # Delegate key access to the wrapped object return self.context[name] - # MultiHook requries this to be a class attribute + # MultiHook requires this to be a class attribute def __before_publishing_traverse__(self, arg1, arg2=None): bpth = getattr(self.context, "__before_publishing_traverse__", False) if bpth: @@ -135,5 +134,5 @@ def publishTraverse(self, request, name): def browserDefault(self, request): # Called when we have reached the end of the path - # In our case this means an unamed service + # In our case this means an unnamed service return self.context, (request._rest_service_id,) diff --git a/src/plone/rest/zcml.py b/src/plone/rest/zcml.py index 3e336020..67c108f3 100644 --- a/src/plone/rest/zcml.py +++ b/src/plone/rest/zcml.py @@ -1,13 +1,12 @@ -# -*- coding: utf-8 -*- from AccessControl.class_init import InitializeClass from AccessControl.security import getSecurityInfo from AccessControl.security import protectClass -from Products.Five.browser import BrowserView from plone.rest.cors import CORSPolicy from plone.rest.cors import register_method_for_preflight from plone.rest.interfaces import ICORSPolicy from plone.rest.negotiation import parse_accept_header from plone.rest.negotiation import register_service +from Products.Five.browser import BrowserView from zope.browserpage.metaconfigure import _handle_for from zope.component.zcml import handler from zope.configuration.fields import GlobalInterface @@ -23,48 +22,48 @@ class IService(Interface): """ """ method = TextLine( - title=u"The name of the view that should be the default. " - + u"[get|post|put|delete]", - description=u""" + title="The name of the view that should be the default. " + + "[get|post|put|delete]", + description=""" This name refers to view that should be the view used by default (if no view name is supplied explicitly).""", ) accept = TextLine( - title=u"Acceptable media types", - description=u"""Specifies the media type used for content negotiation. + title="Acceptable media types", + description="""Specifies the media type used for content negotiation. The service is limited to the given media type and only called if the request contains an "Accept" header with the given media type. Multiple media types can be given by separating them with a comma.""", - default=u"application/json", + default="application/json", ) for_ = GlobalObject( - title=u"The interface this view is the default for.", - description=u"""Specifies the interface for which the view is + title="The interface this view is the default for.", + description="""Specifies the interface for which the view is registered. All objects implementing this interface can make use of this view. If this attribute is not specified, the view is available for all objects.""", ) factory = GlobalObject( - title=u"The factory for this service", - description=u"The factory is usually subclass of the Service class.", + title="The factory for this service", + description="The factory is usually subclass of the Service class.", ) name = TextLine( - title=u"The name of the service.", - description=u"""When no name is defined, the service is available at + title="The name of the service.", + description="""When no name is defined, the service is available at the object's absolute URL. When defining a name, the service is available at the object's absolute URL appended with a slash and the service name.""", required=False, - default=u"", + default="", ) layer = GlobalInterface( - title=u"The browser layer for which this service is registered.", - description=u"""Useful for overriding existing services or for making + title="The browser layer for which this service is registered.", + description="""Useful for overriding existing services or for making services available only if a specific add-on has been installed.""", required=False, @@ -72,8 +71,8 @@ class IService(Interface): ) permission = Permission( - title=u"Permission", - description=u"The permission needed to access the service.", + title="Permission", + description="The permission needed to access the service.", required=True, ) @@ -86,9 +85,8 @@ def serviceDirective( for_, permission, layer=IDefaultBrowserLayer, - name=u"", + name="", ): - _handle_for(_context, for_) media_types = parse_accept_header(accept) @@ -137,16 +135,16 @@ class ICORSPolicyDirective(Interface): """Directive for defining CORS policies""" for_ = GlobalObject( - title=u"The interface this CORS policy is for.", - description=u"""Specifies the interface for which the CORS policy is + title="The interface this CORS policy is for.", + description="""Specifies the interface for which the CORS policy is registered. If this attribute is not specified, the CORS policy applies to all objects.""", required=False, ) layer = GlobalInterface( - title=u"The browser layer for which this CORS policy is registered.", - description=u"""Useful for overriding existing policies or for making + title="The browser layer for which this CORS policy is registered.", + description="""Useful for overriding existing policies or for making them available only if a specific add-on has been installed.""", required=False, @@ -154,45 +152,45 @@ class ICORSPolicyDirective(Interface): ) allow_origin = TextLine( - title=u"Origins", - description=u"""Origins that are allowed access to the resource. Either + title="Origins", + description="""Origins that are allowed access to the resource. Either a comma separated list of origins, e.g. "http://example.net, http://mydomain.com" or "*".""", ) allow_methods = TextLine( - title=u"Methods", - description=u"""A comma separated list of HTTP method names that are + title="Methods", + description="""A comma separated list of HTTP method names that are allowed by this CORS policy, e.g. "DELETE,GET,OPTIONS,PATCH,POST,PUT". """, required=False, ) allow_headers = TextLine( - title=u"Headers", - description=u"""A comma separated list of request headers allowed to be + title="Headers", + description="""A comma separated list of request headers allowed to be sent by the client, e.g. "X-My-Header".""", required=False, ) expose_headers = TextLine( - title=u"Exposed Headers", - description=u"""A comma separated list of response headers clients can + title="Exposed Headers", + description="""A comma separated list of response headers clients can access, e.g. "Content-Length,X-My-Header".""", required=False, ) allow_credentials = Bool( - title=u"Support Credentials", - description=u"""Indicates whether the resource supports user + title="Support Credentials", + description="""Indicates whether the resource supports user credentials in the request.""", required=True, default=False, ) max_age = TextLine( - title=u"Max Age", - description=u"""Indicates how long the results of a preflight request + title="Max Age", + description="""Indicates how long the results of a preflight request can be cached.""", required=False, ) @@ -209,7 +207,6 @@ def cors_policy_directive( for_=Interface, layer=IDefaultBrowserLayer, ): - _handle_for(_context, for_) # Create a new policy class and store the CORS policy configuration in @@ -240,7 +237,7 @@ def cors_policy_directive( new_class, (for_, layer), ICORSPolicy, - u"", + "", _context.info, ), ) diff --git a/versions.cfg b/versions.cfg index 226cfdea..ed319548 100644 --- a/versions.cfg +++ b/versions.cfg @@ -14,7 +14,7 @@ Pygments = 2.5.1 plone.recipe.varnish = 1.3 # Code-analysis -black = 21.12b0 +black = 23.3.0 plone.recipe.codeanalysis = 3.0.1 coverage = 3.7.1 pep8 = 1.7.1 From 44fd7a4e63a5d83543434c64edca3c68d542cb6a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 01:34:32 +0000 Subject: [PATCH 58/95] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.2 → v3.14.0](https://github.com/asottile/pyupgrade/compare/v3.3.2...v3.14.0) - [github.com/psf/black: 23.3.0 → 23.9.1](https://github.com/psf/black/compare/23.3.0...23.9.1) - [github.com/PyCQA/flake8: 6.0.0 → 6.1.0](https://github.com/PyCQA/flake8/compare/6.0.0...6.1.0) - [github.com/codespell-project/codespell: v2.2.4 → v2.2.6](https://github.com/codespell-project/codespell/compare/v2.2.4...v2.2.6) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 90184b8e..621be3eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ ci: repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.3.2 + rev: v3.14.0 hooks: - id: pyupgrade args: [--py38-plus] @@ -15,15 +15,15 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.9.1 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 - repo: https://github.com/codespell-project/codespell - rev: v2.2.4 + rev: v2.2.6 hooks: - id: codespell additional_dependencies: From 46fcb23e3f95fe4a79bdd319b06c39e840fcaac9 Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Mon, 9 Oct 2023 14:00:03 +0200 Subject: [PATCH 59/95] Support IShouldAllowAcquiredItemPublication without requiring a newer CMFCore directly. --- setup.py | 4 ++-- src/plone/rest/configure.zcml | 2 +- src/plone/rest/explicitacquisition.py | 2 +- src/plone/rest/interfaces.py | 8 ++++++++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index acaba841..76adb4cc 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ def read(*rnames): "plone.app.testing[robot]>=4.2.2", "plone.app.robotframework", "plone.dexterity", - "Products.CMFCore>=3.1", + "Products.CMFCore", "requests", ] ), @@ -65,7 +65,7 @@ def read(*rnames): "zope.interface", "zope.publisher", "zope.traversing", - "Products.CMFCore>=3.1", + "Products.CMFCore", "Zope2", "six", ], diff --git a/src/plone/rest/configure.zcml b/src/plone/rest/configure.zcml index 6d745378..af6c1153 100644 --- a/src/plone/rest/configure.zcml +++ b/src/plone/rest/configure.zcml @@ -27,7 +27,7 @@ /> diff --git a/src/plone/rest/explicitacquisition.py b/src/plone/rest/explicitacquisition.py index 95ce6e33..8e3bac56 100644 --- a/src/plone/rest/explicitacquisition.py +++ b/src/plone/rest/explicitacquisition.py @@ -1,5 +1,5 @@ from zope.component import adapter -from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication +from plone.rest.interfaces import IShouldAllowAcquiredItemPublication from plone.rest.traverse import RESTWrapper diff --git a/src/plone/rest/interfaces.py b/src/plone/rest/interfaces.py index ac182248..9ba6d96a 100644 --- a/src/plone/rest/interfaces.py +++ b/src/plone/rest/interfaces.py @@ -20,3 +20,11 @@ def process_simple_request(): def process_preflight_request(): """Process a preflight request""" + + +try: + from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication +except ImportError: + + class IShouldAllowAcquiredItemPublication(Interface): + pass From 619577c7b81eab1e75e4c6e516e79164f9be1882 Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Mon, 9 Oct 2023 14:11:48 +0200 Subject: [PATCH 60/95] Test IShouldAllowAcquiredItemPublication conditionally --- src/plone/rest/tests/test_explicitacquisition.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py index 977f8a5f..65735fe5 100644 --- a/src/plone/rest/tests/test_explicitacquisition.py +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -13,7 +13,16 @@ from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING +try: + from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication +except ImportError: + IShouldAllowAcquiredItemPublication = None + +@unittest.skipIf( + IShouldAllowAcquiredItemPublication is None, + "Older Plone versions don't have CMFCore>=3.2", +) class TestExplicitAcquisition(unittest.TestCase): layer = PLONE_REST_INTEGRATION_TESTING From fa9f2d285d3e91f16bf7c4731076b8069c2a6e2b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 12:16:42 +0000 Subject: [PATCH 61/95] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/plone/rest/explicitacquisition.py | 2 +- .../rest/tests/test_explicitacquisition.py | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/plone/rest/explicitacquisition.py b/src/plone/rest/explicitacquisition.py index 8e3bac56..8c8b4961 100644 --- a/src/plone/rest/explicitacquisition.py +++ b/src/plone/rest/explicitacquisition.py @@ -1,6 +1,6 @@ -from zope.component import adapter from plone.rest.interfaces import IShouldAllowAcquiredItemPublication from plone.rest.traverse import RESTWrapper +from zope.component import adapter @adapter(RESTWrapper) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py index 65735fe5..fbd717b7 100644 --- a/src/plone/rest/tests/test_explicitacquisition.py +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -1,17 +1,16 @@ -import unittest from base64 import b64encode - -from plone.app.testing import ( - SITE_OWNER_NAME, - SITE_OWNER_PASSWORD, - TEST_USER_ID, - setRoles, -) +from plone.app.testing import setRoles +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID +from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING from zExceptions import NotFound from zope.event import notify -from ZPublisher.pubevents import PubAfterTraversal, PubStart +from ZPublisher.pubevents import PubAfterTraversal +from ZPublisher.pubevents import PubStart + +import unittest -from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING try: from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication @@ -38,7 +37,7 @@ def traverse(self, path="/plone", accept="application/json", method="GET"): request.environ["PATH_TRANSLATED"] = path request.environ["HTTP_ACCEPT"] = accept request.environ["REQUEST_METHOD"] = method - auth = "%s:%s" % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + auth = "{}:{}".format(SITE_OWNER_NAME, SITE_OWNER_PASSWORD) b64auth = b64encode(auth.encode("utf8")) request._auth = "Basic %s" % b64auth.decode("utf8") notify(PubStart(request)) From 054b0234ae9597544c0485c32ac3d9f38086e844 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 12:37:44 +0000 Subject: [PATCH 62/95] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/plone/rest/tests/test_explicitacquisition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py index fbd717b7..d0af009a 100644 --- a/src/plone/rest/tests/test_explicitacquisition.py +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -37,7 +37,7 @@ def traverse(self, path="/plone", accept="application/json", method="GET"): request.environ["PATH_TRANSLATED"] = path request.environ["HTTP_ACCEPT"] = accept request.environ["REQUEST_METHOD"] = method - auth = "{}:{}".format(SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + auth = f"{SITE_OWNER_NAME}:{SITE_OWNER_PASSWORD}" b64auth = b64encode(auth.encode("utf8")) request._auth = "Basic %s" % b64auth.decode("utf8") notify(PubStart(request)) From 77b4ce6462a18a10a799d48cdc802805939eda80 Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Wed, 11 Oct 2023 14:44:31 +0200 Subject: [PATCH 63/95] Fixup explicitacquisition tests. --- .../rest/tests/test_explicitacquisition.py | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py index d0af009a..29513809 100644 --- a/src/plone/rest/tests/test_explicitacquisition.py +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -4,6 +4,7 @@ from plone.app.testing import SITE_OWNER_PASSWORD from plone.app.testing import TEST_USER_ID from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING +from Products.CMFPlone import __version__ from zExceptions import NotFound from zope.event import notify from ZPublisher.pubevents import PubAfterTraversal @@ -11,9 +12,9 @@ import unittest - try: from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication + import Products.CMFCore.explicitacquisition except ImportError: IShouldAllowAcquiredItemPublication = None @@ -22,7 +23,7 @@ IShouldAllowAcquiredItemPublication is None, "Older Plone versions don't have CMFCore>=3.2", ) -class TestExplicitAcquisition(unittest.TestCase): +class TestExplicitAcquisitionSkipped(unittest.TestCase): layer = PLONE_REST_INTEGRATION_TESTING def setUp(self): @@ -43,6 +44,50 @@ def traverse(self, path="/plone", accept="application/json", method="GET"): notify(PubStart(request)) return request.traverse(path) + def test_is_skipped(self): + self.assertTrue(Products.CMFCore.explicitacquisition.SKIP_PTA) + + def test_portal_root(self): + self.traverse("/plone") + notify(PubAfterTraversal(self.request)) + + def test_portal_foo(self): + self.traverse("/plone/foo") + notify(PubAfterTraversal(self.request)) + + def test_portal_foo_acquired(self): + self.traverse("/plone/foo/foo") + notify(PubAfterTraversal(self.request)) + + +@unittest.skipIf( + __version__ < "7", + "Plone >= 7 enables this check", +) +class TestExplicitAcquisitionEnabled(unittest.TestCase): + layer = PLONE_REST_INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer["portal"] + self.request = self.layer["request"] + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + self.portal.invokeFactory("Document", id="foo") + + def traverse(self, path="/plone", accept="application/json", method="GET"): + request = self.layer["request"] + request.environ["PATH_INFO"] = path + request.environ["PATH_TRANSLATED"] = path + request.environ["HTTP_ACCEPT"] = accept + request.environ["REQUEST_METHOD"] = method + auth = "%s:%s" % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + b64auth = b64encode(auth.encode("utf8")) + request._auth = "Basic %s" % b64auth.decode("utf8") + notify(PubStart(request)) + return request.traverse(path) + + def test_is_not_skipped(self): + self.assertFalse(Products.CMFCore.explicitacquisition.SKIP_PTA) + def test_portal_root(self): self.traverse("/plone") notify(PubAfterTraversal(self.request)) From 00d55e2b21410218891b4ccbf715d558eee68235 Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Thu, 12 Oct 2023 11:52:10 +0200 Subject: [PATCH 64/95] Fixup tests --- .../rest/tests/test_explicitacquisition.py | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py index 29513809..af6e6762 100644 --- a/src/plone/rest/tests/test_explicitacquisition.py +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -13,17 +13,16 @@ import unittest try: - from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication - import Products.CMFCore.explicitacquisition + from Products.CMFCore.explicitacquisition import SKIP_PTA except ImportError: - IShouldAllowAcquiredItemPublication = None + SKIP_PTA = None -@unittest.skipIf( - IShouldAllowAcquiredItemPublication is None, +@unittest.skipUnless( + SKIP_PTA is None, "Older Plone versions don't have CMFCore>=3.2", ) -class TestExplicitAcquisitionSkipped(unittest.TestCase): +class TestExplicitAcquisitionUnavailable(unittest.TestCase): layer = PLONE_REST_INTEGRATION_TESTING def setUp(self): @@ -44,9 +43,6 @@ def traverse(self, path="/plone", accept="application/json", method="GET"): notify(PubStart(request)) return request.traverse(path) - def test_is_skipped(self): - self.assertTrue(Products.CMFCore.explicitacquisition.SKIP_PTA) - def test_portal_root(self): self.traverse("/plone") notify(PubAfterTraversal(self.request)) @@ -60,11 +56,11 @@ def test_portal_foo_acquired(self): notify(PubAfterTraversal(self.request)) -@unittest.skipIf( - __version__ < "7", - "Plone >= 7 enables this check", +@unittest.skipUnless( + SKIP_PTA is not None, + "We have Products.CMFPlone >= 3.2", ) -class TestExplicitAcquisitionEnabled(unittest.TestCase): +class TestExplicitAcquisitionAvailable(unittest.TestCase): layer = PLONE_REST_INTEGRATION_TESTING def setUp(self): @@ -85,18 +81,34 @@ def traverse(self, path="/plone", accept="application/json", method="GET"): notify(PubStart(request)) return request.traverse(path) - def test_is_not_skipped(self): - self.assertFalse(Products.CMFCore.explicitacquisition.SKIP_PTA) - def test_portal_root(self): + import Products.CMFCore.explicitacquisition + self.traverse("/plone") + Products.CMFCore.explicitacquisition.SKIP_PTA = True + notify(PubAfterTraversal(self.request)) + + Products.CMFCore.explicitacquisition.SKIP_PTA = False notify(PubAfterTraversal(self.request)) def test_portal_foo(self): + import Products.CMFCore.explicitacquisition + self.traverse("/plone/foo") + Products.CMFCore.explicitacquisition.SKIP_PTA = True + notify(PubAfterTraversal(self.request)) + + Products.CMFCore.explicitacquisition.SKIP_PTA = False notify(PubAfterTraversal(self.request)) def test_portal_foo_acquired(self): + import Products.CMFCore.explicitacquisition + self.traverse("/plone/foo/foo") + + Products.CMFCore.explicitacquisition.SKIP_PTA = True + notify(PubAfterTraversal(self.request)) + + Products.CMFCore.explicitacquisition.SKIP_PTA = False with self.assertRaises(NotFound): notify(PubAfterTraversal(self.request)) From 0945e85ed6722717b7f92f3479fe9c4897a77225 Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Thu, 12 Oct 2023 11:52:30 +0200 Subject: [PATCH 65/95] Fixup tests --- src/plone/rest/tests/test_explicitacquisition.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py index af6e6762..0f43b7c5 100644 --- a/src/plone/rest/tests/test_explicitacquisition.py +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -4,7 +4,6 @@ from plone.app.testing import SITE_OWNER_PASSWORD from plone.app.testing import TEST_USER_ID from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING -from Products.CMFPlone import __version__ from zExceptions import NotFound from zope.event import notify from ZPublisher.pubevents import PubAfterTraversal From 9c2f4976ae5b5e9458f0fcd9c31bceb03527aebd Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 14 Oct 2023 22:19:44 +0200 Subject: [PATCH 66/95] Add Python 3.12 support --- .github/workflows/tests.yml | 4 +++- setup.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5fd38da6..fa88b0b1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,9 +6,11 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.10", "3.9", "3.8"] + python-version: ["3.12", "3.11", "3.10", "3.9", "3.8"] plone-version: ["6.0", "5.2"] exclude: + - python-version: 3.12 + plone-version: 5.2 - python-version: 3.11 plone-version: 5.2 - python-version: 3.10 diff --git a/setup.py b/setup.py index 7b8c67cf..6f8d83c3 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,7 @@ -from setuptools import find_packages -from setuptools import setup - import os +from setuptools import find_packages, setup + def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() @@ -36,6 +35,7 @@ def read(*rnames): "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries :: Python Modules", ], From 46a19746c18b7d6007cbd273170359e6dbe067b6 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 14 Oct 2023 22:21:12 +0200 Subject: [PATCH 67/95] Add changelog --- news/167.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/167.feature diff --git a/news/167.feature b/news/167.feature new file mode 100644 index 00000000..f450b557 --- /dev/null +++ b/news/167.feature @@ -0,0 +1 @@ +Add support for Python 3.12 @tisto \ No newline at end of file From f04ad7839529e1c2331999592f073fbbed69f96f Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 14 Oct 2023 22:30:46 +0200 Subject: [PATCH 68/95] GHA: update python-setup --- .github/workflows/tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fa88b0b1..155e2828 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,9 +23,10 @@ jobs: # python setup - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + cache: "pip" # python cache - uses: actions/cache@v1 From f41f5a12950ff8f93d52a86f70f8f7efb86f154d Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 14 Oct 2023 22:33:28 +0200 Subject: [PATCH 69/95] GHA: simplify running buildout. Ignore ci.cfg --- .github/workflows/tests.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 155e2828..019239f5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,13 +40,11 @@ jobs: - run: pip install virtualenv - run: pip install wheel - name: pip install - run: pip install -r requirements-${{ matrix.plone-version }}.x.txt - - name: choose Plone version - run: sed -ie "s#plone-x.x.x.cfg#plone-${{ matrix.plone-version }}.x.cfg#" ci.cfg + run: pip install -r requirements-${{ matrix.plone-version }}.txt -r requirements-docs.txt # buildout - name: buildout - run: buildout -t 10 -c ci.cfg + run: buildout -t 10 -c plone-${{ matrix.plone-version }}.x.cfg env: CI: true From aa88ca6f23fb3c60bef6a63287663255d7fe2138 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 14 Oct 2023 22:33:49 +0200 Subject: [PATCH 70/95] Delete ci.cfg --- ci.cfg | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 ci.cfg diff --git a/ci.cfg b/ci.cfg deleted file mode 100644 index af1a4f24..00000000 --- a/ci.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[buildout] -extends = plone-x.x.x.cfg - -[code-analysis] -recipe = plone.recipe.codeanalysis -pre-commit-hook = False -return-status-codes = True From 61f678a3ec0d445eebd1b67e4fb02d26209064cb Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 14 Oct 2023 22:37:08 +0200 Subject: [PATCH 71/95] Fix requirements files --- .github/workflows/tests.yml | 2 +- Makefile | 2 +- requirements-5.2.x.txt => requirements-5.2.txt | 0 requirements-6.0.txt | 1 + requirements-6.0.x.txt | 11 ----------- 5 files changed, 3 insertions(+), 13 deletions(-) rename requirements-5.2.x.txt => requirements-5.2.txt (100%) create mode 100644 requirements-6.0.txt delete mode 100644 requirements-6.0.x.txt diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 019239f5..86225558 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,7 +40,7 @@ jobs: - run: pip install virtualenv - run: pip install wheel - name: pip install - run: pip install -r requirements-${{ matrix.plone-version }}.txt -r requirements-docs.txt + run: pip install -r requirements-${{ matrix.plone-version }}.txt # buildout - name: buildout diff --git a/Makefile b/Makefile index cffb9942..2f903af0 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,7 @@ build-plone-5.2-performance: .installed.cfg ## Build Plone 5.2 build-plone-6.0: ## Build Plone 6.0 python$(version) -m venv . bin/pip install --upgrade pip - bin/pip install -r requirements-6.0.x.txt + bin/pip install -r requirements-6.0.txt bin/pip install pip install black==$$(awk '/^black =/{print $$NF}' versions.cfg) bin/buildout -c plone-6.0.x.cfg diff --git a/requirements-5.2.x.txt b/requirements-5.2.txt similarity index 100% rename from requirements-5.2.x.txt rename to requirements-5.2.txt diff --git a/requirements-6.0.txt b/requirements-6.0.txt new file mode 100644 index 00000000..dcd3abcf --- /dev/null +++ b/requirements-6.0.txt @@ -0,0 +1 @@ +-r https://dist.plone.org/release/6.0.7/requirements.txt diff --git a/requirements-6.0.x.txt b/requirements-6.0.x.txt deleted file mode 100644 index fe350fe2..00000000 --- a/requirements-6.0.x.txt +++ /dev/null @@ -1,11 +0,0 @@ -pip==22.3.1 -setuptools==65.5.1 -wheel==0.38.4 -zc.buildout==3.0.1 - -# Windows specific down here (has to be installed here, fails in buildout) -# Dependency of zope.sendmail: -pywin32 ; platform_system == 'Windows' - -# SSL Certs on windows, because Python is missing them otherwise: -certifi ; platform_system == 'Windows' \ No newline at end of file From ac9ba3f58b4d71bdb5304dace18246babe07a923 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 14 Oct 2023 22:50:00 +0200 Subject: [PATCH 72/95] grpcio-tools = 1.59.0 --- plone-6.0.x.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plone-6.0.x.cfg b/plone-6.0.x.cfg index f1fead3e..bc10f32b 100644 --- a/plone-6.0.x.cfg +++ b/plone-6.0.x.cfg @@ -10,4 +10,5 @@ zodb-temporary-storage = off [versions] plone.rest = black = 23.3.0 -pygments = 2.14.0 \ No newline at end of file +pygments = 2.14.0 +grpcio-tools = 1.59.0 \ No newline at end of file From db725d943b65f652deb0c203df14082ba0bfeb44 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 14 Oct 2023 23:03:35 +0200 Subject: [PATCH 73/95] Pin everything that is needed to make the p.a.contenttypes rf test dep happy --- plone-6.0.x.cfg | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plone-6.0.x.cfg b/plone-6.0.x.cfg index bc10f32b..0f050469 100644 --- a/plone-6.0.x.cfg +++ b/plone-6.0.x.cfg @@ -11,4 +11,10 @@ zodb-temporary-storage = off plone.rest = black = 23.3.0 pygments = 2.14.0 -grpcio-tools = 1.59.0 \ No newline at end of file + +# all this is neccessary to make the p.a.contenttypes (robotframework) test dependency happy :( +robotframework-browser = 17.5.2 +robotframework-assertion-engine = 2.0.0 +robotframework-debuglibrary = 2.3.0 +robotframework-pythonlibcore = 4.2.0 +grpcio-tools = 1.59.0 From 85a22658fcca2eb96f390869b49932cf9f64e7c1 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Mon, 16 Oct 2023 23:12:32 +0200 Subject: [PATCH 74/95] Fix QA complaints --- plone-6.0.x.cfg | 2 +- setup.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plone-6.0.x.cfg b/plone-6.0.x.cfg index 0f050469..dfc2e44f 100644 --- a/plone-6.0.x.cfg +++ b/plone-6.0.x.cfg @@ -12,7 +12,7 @@ plone.rest = black = 23.3.0 pygments = 2.14.0 -# all this is neccessary to make the p.a.contenttypes (robotframework) test dependency happy :( +# all this is necessary to make the p.a.contenttypes (robotframework) test dependency happy :( robotframework-browser = 17.5.2 robotframework-assertion-engine = 2.0.0 robotframework-debuglibrary = 2.3.0 diff --git a/setup.py b/setup.py index 6f8d83c3..928e5e31 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ -import os +from setuptools import find_packages +from setuptools import setup -from setuptools import find_packages, setup +import os def read(*rnames): From 327323adea7dcd1007fae831a4a635da1cc9e7f8 Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Tue, 17 Oct 2023 12:28:30 +0200 Subject: [PATCH 75/95] Use public interface for availability check, not an internal flag --- src/plone/rest/tests/test_explicitacquisition.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py index 0f43b7c5..6a7b4464 100644 --- a/src/plone/rest/tests/test_explicitacquisition.py +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -12,13 +12,15 @@ import unittest try: - from Products.CMFCore.explicitacquisition import SKIP_PTA + from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication + + HAS_CMFCORE_32 = True except ImportError: - SKIP_PTA = None + HAS_CMFCORE_32 = False @unittest.skipUnless( - SKIP_PTA is None, + not HAS_CMFCORE_32, "Older Plone versions don't have CMFCore>=3.2", ) class TestExplicitAcquisitionUnavailable(unittest.TestCase): @@ -56,8 +58,8 @@ def test_portal_foo_acquired(self): @unittest.skipUnless( - SKIP_PTA is not None, - "We have Products.CMFPlone >= 3.2", + HAS_CMFCORE_32, + "We have Products.CMFCore >= 3.2", ) class TestExplicitAcquisitionAvailable(unittest.TestCase): layer = PLONE_REST_INTEGRATION_TESTING From f2c12a12021ffebeb4955d4b26de4121c4684a5e Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Tue, 17 Oct 2023 12:29:57 +0200 Subject: [PATCH 76/95] Use public interface for availability check, not an internal flag --- src/plone/rest/tests/test_explicitacquisition.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py index 6a7b4464..ebf4ae65 100644 --- a/src/plone/rest/tests/test_explicitacquisition.py +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -12,6 +12,7 @@ import unittest try: + # noqa: F401 from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication HAS_CMFCORE_32 = True From 8c973854b9c5e470c525337de420d2d989d0a241 Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Tue, 17 Oct 2023 12:30:06 +0200 Subject: [PATCH 77/95] Use public interface for availability check, not an internal flag --- src/plone/rest/tests/test_explicitacquisition.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py index ebf4ae65..09808f5e 100644 --- a/src/plone/rest/tests/test_explicitacquisition.py +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -12,8 +12,9 @@ import unittest try: - # noqa: F401 - from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication + from Products.CMFCore.interfaces import ( + IShouldAllowAcquiredItemPublication, + ) # noqa: F401 HAS_CMFCORE_32 = True except ImportError: From eb1bd306f4ff5241fad9d7e70c26a49e1ea318c8 Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Tue, 17 Oct 2023 12:32:25 +0200 Subject: [PATCH 78/95] make flake8 happy again --- src/plone/rest/tests/test_explicitacquisition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py index 09808f5e..fc482bd1 100644 --- a/src/plone/rest/tests/test_explicitacquisition.py +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -12,9 +12,9 @@ import unittest try: - from Products.CMFCore.interfaces import ( - IShouldAllowAcquiredItemPublication, - ) # noqa: F401 + from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication + + IShouldAllowAcquiredItemPublication # flake8 HAS_CMFCORE_32 = True except ImportError: From 2f424809e982ba027adcfa61c3c767cd66b783c2 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Tue, 17 Oct 2023 21:49:24 +0200 Subject: [PATCH 79/95] Fixes by pre-commit. --- src/plone/rest/tests/test_explicitacquisition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py index fc482bd1..beee007e 100644 --- a/src/plone/rest/tests/test_explicitacquisition.py +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -11,6 +11,7 @@ import unittest + try: from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication @@ -78,7 +79,7 @@ def traverse(self, path="/plone", accept="application/json", method="GET"): request.environ["PATH_TRANSLATED"] = path request.environ["HTTP_ACCEPT"] = accept request.environ["REQUEST_METHOD"] = method - auth = "%s:%s" % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + auth = f"{SITE_OWNER_NAME}:{SITE_OWNER_PASSWORD}" b64auth = b64encode(auth.encode("utf8")) request._auth = "Basic %s" % b64auth.decode("utf8") notify(PubStart(request)) From 5bdfe5bd4cd2c6291ee3d47b19f29e5c43634f68 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Tue, 17 Oct 2023 22:04:16 +0200 Subject: [PATCH 80/95] Use https://pypi.org/simple index. Did we seriously still use pypi.python.org? gh-actions complained: ``` root: Reading https://pypi.python.org/simple/Unidecode/ Getting distribution for 'Unidecode==1.3.6'. While: Installing instance. Getting distribution for 'Unidecode==1.3.6'. Error: Couldn't find a distribution for 'Unidecode==1.3.6'. ``` Maybe this fixes is, as the version is available just fine on https://pypi.org/simple/unidecode/ Could be a temporary error. --- README.rst | 8 ++++---- base.cfg | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 6c3a78d6..f6923ebf 100644 --- a/README.rst +++ b/README.rst @@ -7,15 +7,15 @@ :target: https://coveralls.io/github/plone/plone.restapi .. image:: https://img.shields.io/pypi/status/plone.rest.svg - :target: https://pypi.python.org/pypi/plone.rest/ + :target: https://pypi.org/project/plone.rest/ :alt: Egg Status .. image:: https://img.shields.io/pypi/v/plone.rest.svg - :target: https://pypi.python.org/pypi/plone.rest/ + :target: https://pypi.org/project/plone.rest/ :alt: Latest Version .. image:: https://img.shields.io/pypi/l/plone.rest.svg - :target: https://pypi.python.org/pypi/plone.rest/ + :target: https://pypi.org/project/plone.rest/ :alt: License @@ -350,7 +350,7 @@ Contribute - Issue Tracker: https://github.com/plone/plone.rest/issues - Source Code: https://github.com/plone/plone.rest -- Documentation: https://pypi.python.org/pypi/plone.rest +- Documentation: https://pypi.org/project/plone.rest/ Support diff --git a/base.cfg b/base.cfg index 51763bc8..152d0905 100644 --- a/base.cfg +++ b/base.cfg @@ -1,5 +1,5 @@ [buildout] -index = https://pypi.python.org/simple +index = https://pypi.org/simple extensions = mr.developer parts = instance From eb59f3df002f388b6d282eb16ab50eabc4c40c80 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Wed, 18 Oct 2023 12:14:38 +0200 Subject: [PATCH 81/95] Use proper changelog format --- news/166.bugfix | 1 + news/167.feature | 2 +- news/explicitacquisition.bugfix | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 news/166.bugfix delete mode 100644 news/explicitacquisition.bugfix diff --git a/news/166.bugfix b/news/166.bugfix new file mode 100644 index 00000000..bc0cc877 --- /dev/null +++ b/news/166.bugfix @@ -0,0 +1 @@ +Make REST endpoints check for acquired items. @jaroel diff --git a/news/167.feature b/news/167.feature index f450b557..a9860dba 100644 --- a/news/167.feature +++ b/news/167.feature @@ -1 +1 @@ -Add support for Python 3.12 @tisto \ No newline at end of file +Add support for Python 3.12. @tisto \ No newline at end of file diff --git a/news/explicitacquisition.bugfix b/news/explicitacquisition.bugfix deleted file mode 100644 index 37d8c635..00000000 --- a/news/explicitacquisition.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -- Make REST endpoints check for acquired items. - [jaroel] From 2b4a9c66e1517cfd32bd3932adc8e9dbeb144f75 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Wed, 18 Oct 2023 12:15:14 +0200 Subject: [PATCH 82/95] Preparing release 4.1.0 [ci skip] --- CHANGES.rst | 15 +++++++++++++++ news/166.bugfix | 1 - news/167.feature | 1 - setup.py | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) delete mode 100644 news/166.bugfix delete mode 100644 news/167.feature diff --git a/CHANGES.rst b/CHANGES.rst index dbb79003..dd49a54e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,21 @@ Changelog .. towncrier release notes start +4.1.0 (2023-10-18) +------------------ + +New features: + + +- Add support for Python 3.12. @tisto (#167) + + +Bug fixes: + + +- Make REST endpoints check for acquired items. @jaroel (#166) + + 4.0.0 (2023-09-22) ------------------ diff --git a/news/166.bugfix b/news/166.bugfix deleted file mode 100644 index bc0cc877..00000000 --- a/news/166.bugfix +++ /dev/null @@ -1 +0,0 @@ -Make REST endpoints check for acquired items. @jaroel diff --git a/news/167.feature b/news/167.feature deleted file mode 100644 index a9860dba..00000000 --- a/news/167.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for Python 3.12. @tisto \ No newline at end of file diff --git a/setup.py b/setup.py index 928e5e31..d52040e0 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "4.0.1.dev0" +version = "4.1.0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 5f9898970f871008e84b254bf73ff13f48218bbc Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Wed, 18 Oct 2023 12:15:49 +0200 Subject: [PATCH 83/95] Back to development: 4.1.1 [ci skip] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d52040e0..43c34a39 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "4.1.0" +version = "4.1.1.dev0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 1bd2477b94a5e25a2fe28f46e6aecef0c781341a Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Fri, 20 Oct 2023 15:28:41 +0200 Subject: [PATCH 84/95] Reset SKIP_PTA to whatever it was. Before it would leak into other tests, enabling the publication check when it shouldn't be active. --- src/plone/rest/tests/test_explicitacquisition.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py index beee007e..7c5d8cc2 100644 --- a/src/plone/rest/tests/test_explicitacquisition.py +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -68,10 +68,18 @@ class TestExplicitAcquisitionAvailable(unittest.TestCase): layer = PLONE_REST_INTEGRATION_TESTING def setUp(self): + import Products.CMFCore.explicitacquisition + self.portal = self.layer["portal"] self.request = self.layer["request"] setRoles(self.portal, TEST_USER_ID, ["Manager"]) self.portal.invokeFactory("Document", id="foo") + self.PREVIOUS_SKIP_PTA = Products.CMFCore.explicitacquisition.SKIP_PTA + + def tearDown(self): + import Products.CMFCore.explicitacquisition + + Products.CMFCore.explicitacquisition.SKIP_PTA = self.PREVIOUS_SKIP_PTA def traverse(self, path="/plone", accept="application/json", method="GET"): request = self.layer["request"] From d83b13b6f3393453a9c7001a8549914f54a83393 Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Fri, 20 Oct 2023 16:18:35 +0200 Subject: [PATCH 85/95] changelog --- news/168.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/168.bugfix diff --git a/news/168.bugfix b/news/168.bugfix new file mode 100644 index 00000000..5b362b6f --- /dev/null +++ b/news/168.bugfix @@ -0,0 +1 @@ +Fix test leakage, enabling the publication check when it shouldn't be active. @jaroel \ No newline at end of file From 65a53943d56710353ac0fac24e2bd715bb0d78c4 Mon Sep 17 00:00:00 2001 From: Roel Bruggink Date: Fri, 20 Oct 2023 16:23:13 +0200 Subject: [PATCH 86/95] Rename 168.bugfix to 168.internal --- news/{168.bugfix => 168.internal} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename news/{168.bugfix => 168.internal} (74%) diff --git a/news/168.bugfix b/news/168.internal similarity index 74% rename from news/168.bugfix rename to news/168.internal index 5b362b6f..b4bdf12b 100644 --- a/news/168.bugfix +++ b/news/168.internal @@ -1 +1 @@ -Fix test leakage, enabling the publication check when it shouldn't be active. @jaroel \ No newline at end of file +Fix test leakage, enabling the publication check when it shouldn't be active. @jaroel From facbb919660c56a30fb6f71bfe26dbe2b261cb31 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Mon, 23 Oct 2023 17:21:44 +0200 Subject: [PATCH 87/95] Preparing release 4.1.1 --- CHANGES.rst | 9 +++++++++ news/168.internal | 1 - setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/168.internal diff --git a/CHANGES.rst b/CHANGES.rst index dd49a54e..85528537 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,15 @@ Changelog .. towncrier release notes start +4.1.1 (2023-10-23) +------------------ + +Internal: + + +- Fix test leakage, enabling the publication check when it shouldn't be active. @jaroel (#168) + + 4.1.0 (2023-10-18) ------------------ diff --git a/news/168.internal b/news/168.internal deleted file mode 100644 index b4bdf12b..00000000 --- a/news/168.internal +++ /dev/null @@ -1 +0,0 @@ -Fix test leakage, enabling the publication check when it shouldn't be active. @jaroel diff --git a/setup.py b/setup.py index 43c34a39..506f8588 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "4.1.1.dev0" +version = "4.1.1" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From 4b1d0867de945769bc55619cb44f01604a4abd24 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Mon, 23 Oct 2023 17:21:57 +0200 Subject: [PATCH 88/95] Back to development: 4.1.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 506f8588..4e3df0b8 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "4.1.1" +version = "4.1.2.dev0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" From ca5bee89636cf3e5da350500cadec19b383fd01c Mon Sep 17 00:00:00 2001 From: Dylan Jay Date: Wed, 23 Aug 2023 09:59:51 +0700 Subject: [PATCH 89/95] fix for weird requests with media types with extra / --- src/plone/rest/negotiation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/rest/negotiation.py b/src/plone/rest/negotiation.py index 63f2f312..3b23eb41 100644 --- a/src/plone/rest/negotiation.py +++ b/src/plone/rest/negotiation.py @@ -11,7 +11,7 @@ def parse_accept_header(accept): for media_range in accept.split(","): media_type = media_range.split(";")[0].strip() if "/" in media_type: - type_, subtype = media_type.split("/") + type_, subtype = media_type.split("/", 1) media_types.append((type_, subtype)) return media_types From c7115e6451a286a3b60abd6464cdcc27d2241def Mon Sep 17 00:00:00 2001 From: David Glick Date: Fri, 8 Sep 2023 22:03:36 -0700 Subject: [PATCH 90/95] add test --- src/plone/rest/tests/test_negotiation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plone/rest/tests/test_negotiation.py b/src/plone/rest/tests/test_negotiation.py index 3978b887..75385a39 100644 --- a/src/plone/rest/tests/test_negotiation.py +++ b/src/plone/rest/tests/test_negotiation.py @@ -56,6 +56,9 @@ def test_parse_all_media_types_accept_header(self): def test_parse_invalid_accept_header(self): self.assertEqual([], parse_accept_header("invalid")) + def test_parse_mimetype_with_extra_slash(self): + self.assertEqual([("application", "x/y")], parse_accept_header("application/x/y")) + class TestServiceRegistry(unittest.TestCase): def test_register_media_type(self): From dc7eccf5c5be11fc8c1b0eb91ca5a20c1c98fe90 Mon Sep 17 00:00:00 2001 From: David Glick Date: Fri, 8 Sep 2023 22:04:57 -0700 Subject: [PATCH 91/95] black --- src/plone/rest/tests/test_negotiation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plone/rest/tests/test_negotiation.py b/src/plone/rest/tests/test_negotiation.py index 75385a39..22502db6 100644 --- a/src/plone/rest/tests/test_negotiation.py +++ b/src/plone/rest/tests/test_negotiation.py @@ -57,7 +57,9 @@ def test_parse_invalid_accept_header(self): self.assertEqual([], parse_accept_header("invalid")) def test_parse_mimetype_with_extra_slash(self): - self.assertEqual([("application", "x/y")], parse_accept_header("application/x/y")) + self.assertEqual( + [("application", "x/y")], parse_accept_header("application/x/y") + ) class TestServiceRegistry(unittest.TestCase): From 7daa91762767e8e5d3e969339450e72a3440a440 Mon Sep 17 00:00:00 2001 From: David Glick Date: Fri, 8 Sep 2023 22:05:39 -0700 Subject: [PATCH 92/95] changelog --- news/153.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/153.bugfix diff --git a/news/153.bugfix b/news/153.bugfix new file mode 100644 index 00000000..152666d3 --- /dev/null +++ b/news/153.bugfix @@ -0,0 +1 @@ +Fix parsing mimetypes in Accept header with an extra slash. @djay From 7ec72473731b1f2bb87845afbf0f383c1d66c7cd Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 26 Oct 2023 21:47:40 +0300 Subject: [PATCH 93/95] Merge with main --- .github/workflows/black.yml | 4 +- .github/workflows/tests.yml | 32 +++-- .gitignore | 1 + .pre-commit-config.yaml | 38 ++++++ CHANGES.rst | 122 ++++++++++++++++- MANIFEST.in | 2 + Makefile | 13 +- README.rst | 55 +++++--- base.cfg | 4 +- ci.cfg | 7 - plone-4.3.x.cfg | 3 + plone-5.1.x.cfg | 5 +- plone-5.2.x.cfg | 5 +- plone-6.0.x.cfg | 20 +++ pyproject.toml | 11 ++ requirements-5.2.txt | 12 ++ requirements-6.0.txt | 1 + requirements.txt | 2 +- setup.py | 20 +-- src/plone/rest/__init__.py | 1 - src/plone/rest/configure.zcml | 4 + src/plone/rest/cors.py | 4 +- src/plone/rest/demo.py | 1 - src/plone/rest/errors.py | 43 +++--- src/plone/rest/events.py | 1 - src/plone/rest/explicitacquisition.py | 8 ++ src/plone/rest/interfaces.py | 9 +- src/plone/rest/negotiation.py | 4 +- src/plone/rest/patches.py | 1 - src/plone/rest/service.py | 5 +- src/plone/rest/testing.py | 5 +- src/plone/rest/tests/__init__.py | 1 - src/plone/rest/tests/test_cors.py | 7 +- src/plone/rest/tests/test_dexterity.py | 88 ++++++------ src/plone/rest/tests/test_dispatching.py | 80 ++++++----- src/plone/rest/tests/test_error_handling.py | 16 +-- .../rest/tests/test_explicitacquisition.py | 126 ++++++++++++++++++ src/plone/rest/tests/test_named_services.py | 18 ++- src/plone/rest/tests/test_negotiation.py | 15 +-- src/plone/rest/tests/test_permissions.py | 12 +- src/plone/rest/tests/test_redirects.py | 85 +++++++++--- src/plone/rest/tests/test_siteroot.py | 32 +++-- src/plone/rest/tests/test_traversal.py | 59 ++++++-- src/plone/rest/traverse.py | 29 +++- src/plone/rest/zcml.py | 75 +++++------ versions.cfg | 2 + 46 files changed, 778 insertions(+), 310 deletions(-) create mode 100644 .pre-commit-config.yaml delete mode 100644 ci.cfg create mode 100644 plone-6.0.x.cfg create mode 100644 requirements-5.2.txt create mode 100644 requirements-6.0.txt create mode 100644 src/plone/rest/explicitacquisition.py create mode 100644 src/plone/rest/tests/test_explicitacquisition.py diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 9b23c028..adaf3406 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -26,9 +26,9 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - # install black + # install black (extract version from versions.cfg) - name: install black - run: pip install black + run: pip install click==8.0.4 black==$(awk '/^black =/{print $NF}' versions.cfg) # run black - name: run black diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c560a38..86225558 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: plone.rest CI +name: Tests on: [push] jobs: build: @@ -6,27 +6,27 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.7, 2.7] - plone-version: [5.2, 5.1, 4.3] + python-version: ["3.12", "3.11", "3.10", "3.9", "3.8"] + plone-version: ["6.0", "5.2"] exclude: - - python-version: 3.7 - plone-version: 4.3 - - python-version: 3.7 - plone-version: 5.1 - - python-version: 3.8 - plone-version: 4.3 - - python-version: 3.8 - plone-version: 5.1 + - python-version: 3.12 + plone-version: 5.2 + - python-version: 3.11 + plone-version: 5.2 + - python-version: 3.10 + plone-version: 5.2 + - python-version: 3.9 + plone-version: 5.2 steps: - # git checkout - uses: actions/checkout@v2 # python setup - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + cache: "pip" # python cache - uses: actions/cache@v1 @@ -40,13 +40,11 @@ jobs: - run: pip install virtualenv - run: pip install wheel - name: pip install - run: pip install -r requirements.txt - - name: choose Plone version - run: sed -ie "s#plone-x.x.x.cfg#plone-${{ matrix.plone-version }}.x.cfg#" ci.cfg + run: pip install -r requirements-${{ matrix.plone-version }}.txt # buildout - name: buildout - run: buildout -t 10 -c ci.cfg + run: buildout -t 10 -c plone-${{ matrix.plone-version }}.x.cfg env: CI: true diff --git a/.gitignore b/.gitignore index 30ab005f..ece0007b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ /.Python /include /lib +/lib64 /.mr.developer.cfg *.mo local/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..621be3eb --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +# Generated from: +# https://github.com/plone/meta/tree/master/config/default +ci: + autofix_prs: false + autoupdate_schedule: monthly + +repos: + - repo: https://github.com/asottile/pyupgrade + rev: v3.14.0 + hooks: + - id: pyupgrade + args: [--py38-plus] + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + - repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + additional_dependencies: + - tomli + - repo: https://github.com/mgedmin/check-manifest + rev: "0.49" + hooks: + - id: check-manifest + - repo: https://github.com/regebro/pyroma + rev: "4.2" + hooks: + - id: pyroma diff --git a/CHANGES.rst b/CHANGES.rst index 3cf81ce6..85528537 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,124 @@ Changelog .. towncrier release notes start +4.1.1 (2023-10-23) +------------------ + +Internal: + + +- Fix test leakage, enabling the publication check when it shouldn't be active. @jaroel (#168) + + +4.1.0 (2023-10-18) +------------------ + +New features: + + +- Add support for Python 3.12. @tisto (#167) + + +Bug fixes: + + +- Make REST endpoints check for acquired items. @jaroel (#166) + + +4.0.0 (2023-09-22) +------------------ + +Breaking changes: + + +- Drop support for Python 2.7, 3.6, and 3.7 @tisto (#141) + + +3.0.1 (2023-09-21) +------------------ + +Bug fixes: + + +- When ``++api++`` is in the url multiple times, redirect to the proper url. + When the url is badly formed, for example ``++api++/something/++api++``, give a 404 NotFound. + Fixes a denial of service. + See `security advisory `_. + [maurits] (#1) + + +3.0.0 (2023-01-29) +------------------ + +Breaking changes: + + +- Change the HTTP status from 301 (Moved Permanently) to 302 (Found) for GET requests and to 307 (Temporary Redirect) for other request methods. + This fixes problems when an existing redirect is re-used. + [mamico] (#135) +- Drop official support for Plone 4.3, 5.0 and 5.1 (most likely the package will continue to work though) + [tisto] (#140) + + +New features: + + +- Add official support for Plone 6 + [tisto] (#143) +- Add official support for Python 3.9, 3.10, and 3.11 + [tisto] (#147) + + +2.0.0 (2022-10-15) +------------------ + +Bug fixes: + + +- Re-release 2.0.0a6 as 2.0.0 [tisto] (#136) + + +2.0.0a5 (2022-04-07) +-------------------- + +Bug fixes: + + +- Fix an infinite loop with redirections from parent to child [ericof] (#133) + + +2.0.0a4 (2022-03-24) +-------------------- + +Bug fixes: + + +- ++api++ traverser should be kept on 30x redirections [mamico] (#132) + + +2.0.0a3 (2022-02-12) +-------------------- + +Bug fixes: + + +- ++api++ traverser should be kept on 30x redirections [mamico] (#127) + + +2.0.0a2 (2022-01-25) +-------------------- + +Bug fixes: + + +- Fix typo in `README.rst` [jensens] (#123) +- Use document_view as default for site root. + [agitator] (#126) +- Resolve all the deprecation warnings that originate in this package's code that are + exposed by running the tests that do not stem from backwards compatibility we support. + [rpatterson] (#128) + + 2.0.0a1 (2021-10-05) -------------------- @@ -151,7 +269,7 @@ Bugfixes: [buchi] - Fallback to regular views during traversal to ensure compatibility with - views beeing called with a specific Accept header. + views being called with a specific Accept header. [buchi] @@ -201,7 +319,7 @@ Bugfixes: - Refactor traversal of REST requests by using a traversal adapter on the site root instead of a traversal adapter for each REST service. This prevents - REST services from being overriden by other traversal adapters. + REST services from being overridden by other traversal adapters. [buchi] diff --git a/MANIFEST.in b/MANIFEST.in index bbb2eef8..79842040 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,7 +6,9 @@ exclude .flake8 exclude bootstrap-buildout.py exclude Makefile exclude requirements.txt +exclude requirements-*.txt exclude CODEOWNERS +exclude .pre-commit-config.yaml global-exclude *.pyc include pyproject.toml recursive-exclude news * diff --git a/Makefile b/Makefile index 78f6e9d7..2f903af0 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SHELL := /bin/bash CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) -version = 3 +version = 3.9 # We like colors # From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects @@ -12,7 +12,7 @@ GREEN=`tput setaf 2` RESET=`tput sgr0` YELLOW=`tput setaf 3` -all: .installed.cfg +all: build-plone-6.0 # Add the following 'help' target to your Makefile # And add help text after each target name starting with '\#\#' @@ -36,7 +36,7 @@ update: ## Update Make and Buildout bin/buildout: bin/pip bin/pip install --upgrade pip bin/pip install -r requirements.txt - bin/pip install black || true + bin/pip install pip install black==$$(awk '/^black =/{print $$NF}' versions.cfg) @touch -c $@ bin/python bin/pip: @@ -83,6 +83,13 @@ build-plone-5.2-performance: .installed.cfg ## Build Plone 5.2 bin/pip install -r requirements.txt bin/buildout -c plone-5.2.x-performance.cfg +build-plone-6.0: ## Build Plone 6.0 + python$(version) -m venv . + bin/pip install --upgrade pip + bin/pip install -r requirements-6.0.txt + bin/pip install pip install black==$$(awk '/^black =/{print $$NF}' versions.cfg) + bin/buildout -c plone-6.0.x.cfg + .PHONY: Test test: ## Test bin/test diff --git a/README.rst b/README.rst index 4b91180f..f6923ebf 100644 --- a/README.rst +++ b/README.rst @@ -1,21 +1,21 @@ -.. image:: https://github.com/plone/plone.rest/workflows/plone.rest%20CI/badge.svg +.. image:: https://github.com/plone/plone.rest/actions/workflows/tests.yml/badge.svg :alt: Github Actions Status - :target: https://github.com/plone/plone.rest/actions?query=workflow%3A%22plone.rest+CI%22 + :target: https://github.com/plone/plone.rest/actions/workflows/tests.yml .. image:: https://img.shields.io/coveralls/github/plone/plone.rest.svg :alt: Coveralls github :target: https://coveralls.io/github/plone/plone.restapi .. image:: https://img.shields.io/pypi/status/plone.rest.svg - :target: https://pypi.python.org/pypi/plone.rest/ + :target: https://pypi.org/project/plone.rest/ :alt: Egg Status .. image:: https://img.shields.io/pypi/v/plone.rest.svg - :target: https://pypi.python.org/pypi/plone.rest/ + :target: https://pypi.org/project/plone.rest/ :alt: Latest Version .. image:: https://img.shields.io/pypi/l/plone.rest.svg - :target: https://pypi.python.org/pypi/plone.rest/ + :target: https://pypi.org/project/plone.rest/ :alt: License @@ -33,7 +33,7 @@ It is a software architectural principle to create loosely coupled web APIs. plone.rest provides the basic infrastructure that allows us to build RESTful endpoints in Plone. -The reason for separating this infrastructure into a separate package from the 'main' full `Plone REST API `_ is so you can create alternative endpoints tailored to specific usecases. +The reason for separating this infrastructure into a separate package from the 'main' full `Plone REST API `_ is so you can create alternative endpoints tailored to specific usecases. A number of these specific endpoints are already in active use. @@ -70,7 +70,7 @@ plone.rest allows you to register HTTP verbs for Plone content with ZCML. This is how you would register a PATCH request on Dexterity content: -.. code-block:: xml +.. code-block:: XML and Ramon Navarro Bosch . +This package is maintained by Timo Stollenwerk . If you are having issues, please `let us know `_. +Credits +------- + +plone.rest has been written by Timo Stollenwerk (`kitconcept GmbH `_) and Ramon Navarro Bosch (`Iskra `_). + +plone.rest was added as a Plone core package with Plone 5.2 (see ``_). + + License ------- diff --git a/base.cfg b/base.cfg index 10182f54..152d0905 100644 --- a/base.cfg +++ b/base.cfg @@ -1,5 +1,5 @@ [buildout] -index = https://pypi.python.org/simple +index = https://pypi.org/simple extensions = mr.developer parts = instance @@ -61,5 +61,5 @@ eggs = [sources] plone.dexterity = git git://github.com/plone/plone.dexterity.git pushurl=git@github.com:plone/plone.dexterity.git branch=plip-680 -plone.rest = git git://github.com/plone/plone.rest.git pushurl=git@github.com:plone/plone.rest.git branch=master +plone.rest = git git://github.com/plone/plone.rest.git pushurl=git@github.com:plone/plone.rest.git branch=main Products.CMFPlone = git git://github.com/plone/Products.CMFPlone.git pushurl=git@github.com:plone/Products.CMFPlone.git branch=4.3.x-plip-680 diff --git a/ci.cfg b/ci.cfg deleted file mode 100644 index af1a4f24..00000000 --- a/ci.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[buildout] -extends = plone-x.x.x.cfg - -[code-analysis] -recipe = plone.recipe.codeanalysis -pre-commit-hook = False -return-status-codes = True diff --git a/plone-4.3.x.cfg b/plone-4.3.x.cfg index 858527ff..72674c4e 100644 --- a/plone-4.3.x.cfg +++ b/plone-4.3.x.cfg @@ -26,6 +26,9 @@ distlib = 0.3.1 [versions:python27] PyJWT = 1.7.1 pyroma = 2.6.1 +pep517 = <=0.12.0 +readme-renderer = <=28.0 +bleach = <4 # more-itertools >= 6.0.0 dropped python2.7 support more-itertools = 5.0.0 diff --git a/plone-5.1.x.cfg b/plone-5.1.x.cfg index 33ccdcca..9729141e 100644 --- a/plone-5.1.x.cfg +++ b/plone-5.1.x.cfg @@ -37,6 +37,9 @@ astunparse = 1.6.2 [versions:python27] PyJWT = 1.7.1 pyroma = 2.6.1 +pep517 = <=0.12.0 +readme-renderer = <=28.0 +bleach = <4 # more-itertools >= 6.0.0 dropped python2.7 support more-itertools = 5.0.0 @@ -45,4 +48,4 @@ more-itertools = 5.0.0 pyrsistent = 0.15.7 # Click 8 dropped Python 2 support -Click = 7.1.2 \ No newline at end of file +Click = 7.1.2 diff --git a/plone-5.2.x.cfg b/plone-5.2.x.cfg index f4d814f8..3eca6c94 100644 --- a/plone-5.2.x.cfg +++ b/plone-5.2.x.cfg @@ -1,12 +1,13 @@ [buildout] extends = base.cfg - https://dist.plone.org/release/5.2.4/versions.cfg + https://dist.plone.org/release/5.2.7/versions.cfg find-links += https://dist.plone.org/thirdparty/ versions=versions [versions] -black = 20.8b1 +plone.rest = +black = 23.3.0 # Error: The requirement ('virtualenv>=20.0.35') is not allowed by your [versions] constraint (20.0.26) virtualenv = 20.0.35 diff --git a/plone-6.0.x.cfg b/plone-6.0.x.cfg new file mode 100644 index 00000000..dfc2e44f --- /dev/null +++ b/plone-6.0.x.cfg @@ -0,0 +1,20 @@ +[buildout] +extends = + https://dist.plone.org/release/6.0.7/versions.cfg + base.cfg + +[instance] +recipe = plone.recipe.zope2instance +zodb-temporary-storage = off + +[versions] +plone.rest = +black = 23.3.0 +pygments = 2.14.0 + +# all this is necessary to make the p.a.contenttypes (robotframework) test dependency happy :( +robotframework-browser = 17.5.2 +robotframework-assertion-engine = 2.0.0 +robotframework-debuglibrary = 2.3.0 +robotframework-pythonlibcore = 4.2.0 +grpcio-tools = 1.59.0 diff --git a/pyproject.toml b/pyproject.toml index 05b615de..a9ead58a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,3 +18,14 @@ showcontent = true directory = "bugfix" name = "Bug fixes:" showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal:" +showcontent = true + +[tool.isort] +profile = "plone" + +[tool.black] +target-version = ["py38"] \ No newline at end of file diff --git a/requirements-5.2.txt b/requirements-5.2.txt new file mode 100644 index 00000000..505c1abe --- /dev/null +++ b/requirements-5.2.txt @@ -0,0 +1,12 @@ +# Keep this file in sync with: https://dist.plone.org/release/5.2.9/requirements.txt +setuptools==42.0.2 +zc.buildout==2.13.7 +wheel==0.37.1 + +# Windows specific down here (has to be installed here, fails in buildout) +# Dependency of zope.sendmail: +pywin32 ; platform_system == 'Windows' +# SSL Certs on Windows, because Python is missing them otherwise: +certifi ; platform_system == 'Windows' +# Dependency of collective.recipe.omelette: +ntfsutils ; platform_system == 'Windows' and python_version < '3.0' \ No newline at end of file diff --git a/requirements-6.0.txt b/requirements-6.0.txt new file mode 100644 index 00000000..dcd3abcf --- /dev/null +++ b/requirements-6.0.txt @@ -0,0 +1 @@ +-r https://dist.plone.org/release/6.0.7/requirements.txt diff --git a/requirements.txt b/requirements.txt index 8466fed9..0c1a2280 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ # Keep this file in sync with: https://github.com/kitconcept/buildout/edit/master/requirements.txt setuptools==42.0.2 -zc.buildout==2.13.3 +zc.buildout==2.13.4 wheel diff --git a/setup.py b/setup.py index a1e0304c..4e3df0b8 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,14 @@ +from setuptools import find_packages +from setuptools import setup + import os -from setuptools import setup, find_packages def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() -version = "2.0.0a2.dev0" +version = "4.1.2.dev0" long_description = read("README.rst") + "\n\n" + read("CHANGES.rst") + "\n\n" @@ -22,20 +24,21 @@ def read(*rnames): "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Plone", - "Framework :: Plone :: 4.3", - "Framework :: Plone :: 5.0", - "Framework :: Plone :: 5.1", "Framework :: Plone :: 5.2", + "Framework :: Plone :: 6.0", "Framework :: Plone :: Core", "Framework :: Zope2", "Framework :: Zope :: 4", "License :: OSI Approved :: GNU General Public License (GPL)", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development :: Libraries :: Python Modules", ], keywords="rest http", author="Plone Foundation", @@ -47,6 +50,7 @@ def read(*rnames): namespace_packages=["plone"], include_package_data=True, zip_safe=False, + python_requires=">=3.8", extras_require=dict( test=[ "plone.app.testing[robot]>=4.2.2", diff --git a/src/plone/rest/__init__.py b/src/plone/rest/__init__.py index 44646e48..ab37f224 100644 --- a/src/plone/rest/__init__.py +++ b/src/plone/rest/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- from plone.rest.service import Service # noqa diff --git a/src/plone/rest/configure.zcml b/src/plone/rest/configure.zcml index f03a0e85..af6c1153 100644 --- a/src/plone/rest/configure.zcml +++ b/src/plone/rest/configure.zcml @@ -26,4 +26,8 @@ provides="zope.interface.Interface" /> + + diff --git a/src/plone/rest/cors.py b/src/plone/rest/cors.py index dbc2035b..737bed84 100644 --- a/src/plone/rest/cors.py +++ b/src/plone/rest/cors.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- from plone.rest.interfaces import ICORSPolicy from zope.interface import implementer + # CORS preflight service registry # A mapping of method -> service_id _services = {} @@ -19,7 +19,7 @@ def lookup_preflight_service_id(method): @implementer(ICORSPolicy) -class CORSPolicy(object): +class CORSPolicy: def __init__(self, context, request): self.context = context self.request = request diff --git a/src/plone/rest/demo.py b/src/plone/rest/demo.py index 4622f978..4a3bcf57 100644 --- a/src/plone/rest/demo.py +++ b/src/plone/rest/demo.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from plone.rest import Service import json diff --git a/src/plone/rest/errors.py b/src/plone/rest/errors.py index 8227ff40..d6a9c536 100644 --- a/src/plone/rest/errors.py +++ b/src/plone/rest/errors.py @@ -1,5 +1,6 @@ from AccessControl import getSecurityManager + try: from plone.app.redirector.interfaces import IRedirectionStorage except ImportError: @@ -10,10 +11,11 @@ from Products.CMFCore.permissions import ManagePortal from Products.Five.browser import BrowserView from six.moves import urllib -from six.moves.urllib.parse import quote -from six.moves.urllib.parse import unquote +from urllib.parse import quote +from urllib.parse import unquote from zExceptions import NotFound + try: from ZPublisher.HTTPRequest import WSGIRequest @@ -61,7 +63,7 @@ def render_exception(self, exception): if six.PY2: name = name.decode("utf-8") message = message.decode("utf-8") - result = {u"type": name, u"message": message} + result = {"type": name, "message": message} policy = queryMultiAdapter((self.context, self.request), ICORSPolicy) if policy is not None: @@ -77,10 +79,10 @@ def render_exception(self, exception): # NotFound exceptions need special handling because their # exception message gets turned into HTML by ZPublisher url = self.request.getURL() - result[u"message"] = u"Resource not found: %s" % url + result["message"] = "Resource not found: %s" % url if getSecurityManager().checkPermission(ManagePortal, getSite()): - result[u"traceback"] = self.render_traceback(exception) + result["traceback"] = self.render_traceback(exception) return result @@ -101,8 +103,8 @@ def render_traceback(self, exception): pass else: return ( - u"ERROR: Another exception happened before we could " - u"render the traceback." + "ERROR: Another exception happened before we could " + "render the traceback." ) raw = "\n".join(traceback.format_tb(exc_traceback)) @@ -138,17 +140,17 @@ def find_redirect_if_view_or_service(self, old_path_elements, storage): # ['', 'Plone', 'folder', 'item', '@@view', 'param'] # ^ splitpoint = len(old_path_elements) - while splitpoint > 1: possible_obj_path = "/".join(old_path_elements[:splitpoint]) remainder = old_path_elements[splitpoint:] new_path = storage.get(possible_obj_path) if new_path: - if new_path == possible_obj_path: + if new_path.startswith(possible_obj_path): # New URL would match originally requested URL. # Lets not cause a redirect loop. return None + return new_path + "/" + "/".join(remainder) splitpoint -= 1 @@ -162,7 +164,7 @@ def attempt_redirect(self): This method is based on FourOhFourView.attempt_redirect() from p.a.redirector. It's copied here because we want to answer redirects - to non-GET methods with status 308, but since this method locks the + to non-GET methods with status 307, but since this method locks the response status, we wouldn't be able to change it afterwards. """ url = self._url() @@ -178,6 +180,12 @@ def attempt_redirect(self): if storage is None: return False + # remove ++api++ traverser + if "++api++" in old_path_elements: + api_traverser_pos = old_path_elements.index("++api++") + old_path_elements = [el for el in old_path_elements if el != "++api++"] + else: + api_traverser_pos = None old_path = "/".join(old_path_elements) # First lets try with query string in cases or content migration @@ -186,7 +194,7 @@ def attempt_redirect(self): query_string = self.request.QUERY_STRING if query_string: - new_path = storage.get("%s?%s" % (old_path, query_string)) + new_path = storage.get(f"{old_path}?{query_string}") # if we matched on the query_string we don't want to include it # in redirect if new_path: @@ -211,20 +219,25 @@ def attempt_redirect(self): url_path = quote(url_path) url = urllib.parse.SplitResult(*(url[:2] + (url_path,) + url[3:])).geturl() else: + # reinsert ++api++ traverser + if api_traverser_pos is not None: + new_path_elements = new_path.split("/") + new_path_elements.insert(api_traverser_pos, "++api++") + new_path = "/".join(new_path_elements) url = self.request.physicalPathToURL(new_path) # some analytics programs might use this info to track if query_string: url += "?" + query_string - # Answer GET requests with 301. Every other method will be answered - # with 308 Permanent Redirect, which instructs the client to NOT + # Answer GET requests with 302. Every other method will be answered + # with 307 Temporary Redirect, which instructs the client to NOT # switch the method (if the original request was a POST, it should # re-POST to the new URL from the Location header). if self.request.method.upper() == "GET": - status = 301 + status = 302 else: - status = 308 + status = 307 self.request.response.redirect(url, status=status, lock=1) return True diff --git a/src/plone/rest/events.py b/src/plone/rest/events.py index 49554d8b..536ea647 100644 --- a/src/plone/rest/events.py +++ b/src/plone/rest/events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from plone.rest.cors import lookup_preflight_service_id from plone.rest.interfaces import IAPIRequest from plone.rest.negotiation import lookup_service_id diff --git a/src/plone/rest/explicitacquisition.py b/src/plone/rest/explicitacquisition.py new file mode 100644 index 00000000..8c8b4961 --- /dev/null +++ b/src/plone/rest/explicitacquisition.py @@ -0,0 +1,8 @@ +from plone.rest.interfaces import IShouldAllowAcquiredItemPublication +from plone.rest.traverse import RESTWrapper +from zope.component import adapter + + +@adapter(RESTWrapper) +def rest_allowed(wrapper): + return IShouldAllowAcquiredItemPublication(wrapper.context) diff --git a/src/plone/rest/interfaces.py b/src/plone/rest/interfaces.py index ac182248..a31868d7 100644 --- a/src/plone/rest/interfaces.py +++ b/src/plone/rest/interfaces.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from zope.interface import Interface @@ -20,3 +19,11 @@ def process_simple_request(): def process_preflight_request(): """Process a preflight request""" + + +try: + from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication +except ImportError: + + class IShouldAllowAcquiredItemPublication(Interface): + pass diff --git a/src/plone/rest/negotiation.py b/src/plone/rest/negotiation.py index 7a47d5ab..63f2f312 100644 --- a/src/plone/rest/negotiation.py +++ b/src/plone/rest/negotiation.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Service registry # A mapping of method -> type name -> subtype name -> service id _services = {} @@ -42,7 +40,7 @@ def register_service(method, media_type): """Register a service for the given request method and media type and return it's service id. """ - service_id = u"{}_{}_{}_".format(method, media_type[0], media_type[1]) + service_id = f"{method}_{media_type[0]}_{media_type[1]}_" types = _services.setdefault(method, {}) subtypes = types.setdefault(media_type[0], {}) subtypes[media_type[1]] = service_id diff --git a/src/plone/rest/patches.py b/src/plone/rest/patches.py index 97b618f1..8f4cb5dc 100644 --- a/src/plone/rest/patches.py +++ b/src/plone/rest/patches.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from plone.rest.interfaces import IAPIRequest diff --git a/src/plone/rest/service.py b/src/plone/rest/service.py index 351b111d..7761f8f7 100644 --- a/src/plone/rest/service.py +++ b/src/plone/rest/service.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from plone.rest.interfaces import ICORSPolicy from plone.rest.interfaces import IService from zope.component import queryMultiAdapter @@ -6,7 +5,7 @@ @implementer(IService) -class Service(object): +class Service: def __call__(self): policy = queryMultiAdapter((self.context, self.request), ICORSPolicy) if policy is not None: @@ -29,4 +28,4 @@ def __getattribute__(self, name): # include credentials if name == "__roles__" and self.request._rest_cors_preflight: return ["Anonymous"] - return super(Service, self).__getattribute__(name) + return super().__getattribute__(name) diff --git a/src/plone/rest/testing.py b/src/plone/rest/testing.py index 05268d94..bcd4eeb3 100644 --- a/src/plone/rest/testing.py +++ b/src/plone/rest/testing.py @@ -1,16 +1,13 @@ -# -*- coding: utf-8 -*- from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE from plone.app.testing import FunctionalTesting from plone.app.testing import IntegrationTesting from plone.app.testing import PloneSandboxLayer from plone.rest.service import Service from plone.testing import z2 - from zope.configuration import xmlconfig class PloneRestLayer(PloneSandboxLayer): - defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,) def setUpZope(self, app, configurationContext): @@ -31,7 +28,7 @@ def setUpZope(self, app, configurationContext): class InternalServerErrorService(Service): def __call__(self): - from six.moves.urllib.error import HTTPError + from urllib.error import HTTPError raise HTTPError( "http://nohost/plone/500-internal-server-error", diff --git a/src/plone/rest/tests/__init__.py b/src/plone/rest/tests/__init__.py index 40a96afc..e69de29b 100644 --- a/src/plone/rest/tests/__init__.py +++ b/src/plone/rest/tests/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/plone/rest/tests/test_cors.py b/src/plone/rest/tests/test_cors.py index 964ea1d9..ddf1f1a5 100644 --- a/src/plone/rest/tests/test_cors.py +++ b/src/plone/rest/tests/test_cors.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from ZPublisher.pubevents import PubStart from plone.app.testing import popGlobalRegistry from plone.app.testing import pushGlobalRegistry from plone.rest.cors import CORSPolicy @@ -10,12 +8,12 @@ from zope.event import notify from zope.interface import Interface from zope.publisher.interfaces.browser import IDefaultBrowserLayer +from ZPublisher.pubevents import PubStart import unittest class TestCORSPolicy(unittest.TestCase): - layer = PLONE_REST_INTEGRATION_TESTING def setUp(self): @@ -195,7 +193,6 @@ def test_preflight_cors_sets_status_code_200(self): class TestCORS(unittest.TestCase): - layer = PLONE_REST_INTEGRATION_TESTING def setUp(self): @@ -231,7 +228,7 @@ def test_simple_cors_gets_processed(self): def test_preflight_request_without_cors_policy_doesnt_render_service(self): # "Unregister" the current CORS policy - class NoCORSPolicy(object): + class NoCORSPolicy: def __new__(cls, context, request): return None diff --git a/src/plone/rest/tests/test_dexterity.py b/src/plone/rest/tests/test_dexterity.py index 288ae480..cc411021 100644 --- a/src/plone/rest/tests/test_dexterity.py +++ b/src/plone/rest/tests/test_dexterity.py @@ -1,25 +1,23 @@ -# -*- coding: utf-8 -*- from datetime import datetime from plone.app.testing import setRoles -from plone.app.testing import TEST_USER_ID from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID from plone.app.textfield.value import RichTextValue -from plone.namedfile.file import NamedBlobImage from plone.namedfile.file import NamedBlobFile +from plone.namedfile.file import NamedBlobImage from plone.rest.testing import PLONE_REST_FUNCTIONAL_TESTING from z3c.relationfield import RelationValue from zope.component import getUtility from zope.intid.interfaces import IIntIds -import unittest import os import requests import transaction +import unittest class TestDexterityServiceEndpoints(unittest.TestCase): - layer = PLONE_REST_FUNCTIONAL_TESTING def setUp(self): @@ -39,8 +37,8 @@ def test_dexterity_document_get(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(200, response.status_code) - self.assertEqual(u"doc1", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("doc1", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_document_post(self): response = requests.post( @@ -49,8 +47,8 @@ def test_dexterity_document_post(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(200, response.status_code) - self.assertEqual(u"doc1", response.json().get("id")) - self.assertEqual(u"POST", response.json().get("method")) + self.assertEqual("doc1", response.json().get("id")) + self.assertEqual("POST", response.json().get("method")) def test_dexterity_document_put(self): response = requests.put( @@ -59,8 +57,8 @@ def test_dexterity_document_put(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(200, response.status_code) - self.assertEqual(u"doc1", response.json().get("id")) - self.assertEqual(u"PUT", response.json().get("method")) + self.assertEqual("doc1", response.json().get("id")) + self.assertEqual("PUT", response.json().get("method")) def test_dexterity_document_patch(self): response = requests.patch( @@ -69,8 +67,8 @@ def test_dexterity_document_patch(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(200, response.status_code) - self.assertEqual(u"doc1", response.json().get("id")) - self.assertEqual(u"PATCH", response.json().get("method")) + self.assertEqual("doc1", response.json().get("id")) + self.assertEqual("PATCH", response.json().get("method")) def test_dexterity_document_delete(self): response = requests.delete( @@ -79,8 +77,8 @@ def test_dexterity_document_delete(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(200, response.status_code) - self.assertEqual(u"doc1", response.json().get("id")) - self.assertEqual(u"DELETE", response.json().get("method")) + self.assertEqual("doc1", response.json().get("id")) + self.assertEqual("DELETE", response.json().get("method")) def test_dexterity_document_options(self): response = requests.options( @@ -89,8 +87,8 @@ def test_dexterity_document_options(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(200, response.status_code) - self.assertEqual(u"doc1", response.json().get("id")) - self.assertEqual(u"OPTIONS", response.json().get("method")) + self.assertEqual("doc1", response.json().get("id")) + self.assertEqual("OPTIONS", response.json().get("method")) def test_dexterity_folder_get(self): self.portal.invokeFactory("Folder", id="folder") @@ -103,23 +101,23 @@ def test_dexterity_folder_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"folder", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("folder", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_news_item_get(self): self.portal.invokeFactory("News Item", id="newsitem") self.portal.newsitem.title = "My News Item" - self.portal.newsitem.description = u"This is a news item" + self.portal.newsitem.description = "This is a news item" self.portal.newsitem.text = RichTextValue( - u"Lorem ipsum", "text/plain", "text/html" + "Lorem ipsum", "text/plain", "text/html" ) - image_file = os.path.join(os.path.dirname(__file__), u"image.png") + image_file = os.path.join(os.path.dirname(__file__), "image.png") fd = open(image_file, "rb") self.portal.newsitem.image = NamedBlobImage( - data=fd.read(), contentType="image/png", filename=u"image.png" + data=fd.read(), contentType="image/png", filename="image.png" ) fd.close() - self.portal.newsitem.image_caption = u"This is an image caption." + self.portal.newsitem.image_caption = "This is an image caption." import transaction transaction.commit() @@ -130,13 +128,13 @@ def test_dexterity_news_item_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"newsitem", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("newsitem", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_event_get(self): self.portal.invokeFactory("Event", id="event") self.portal.event.title = "Event" - self.portal.event.description = u"This is an event" + self.portal.event.description = "This is an event" self.portal.event.start = datetime(2013, 1, 1, 10, 0) self.portal.event.end = datetime(2013, 1, 1, 12, 0) import transaction @@ -149,13 +147,13 @@ def test_dexterity_event_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"event", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("event", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_link_get(self): self.portal.invokeFactory("Link", id="link") self.portal.link.title = "My Link" - self.portal.link.description = u"This is a link" + self.portal.link.description = "This is a link" self.portal.remoteUrl = "http://plone.org" import transaction @@ -167,17 +165,17 @@ def test_dexterity_link_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"link", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("link", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_file_get(self): self.portal.invokeFactory("File", id="file") self.portal.file.title = "My File" - self.portal.file.description = u"This is a file" - pdf_file = os.path.join(os.path.dirname(__file__), u"file.pdf") + self.portal.file.description = "This is a file" + pdf_file = os.path.join(os.path.dirname(__file__), "file.pdf") fd = open(pdf_file, "rb") self.portal.file.file = NamedBlobFile( - data=fd.read(), contentType="application/pdf", filename=u"file.pdf" + data=fd.read(), contentType="application/pdf", filename="file.pdf" ) fd.close() intids = getUtility(IIntIds) @@ -194,17 +192,17 @@ def test_dexterity_file_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"file", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("file", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_image_get(self): self.portal.invokeFactory("Image", id="image") self.portal.image.title = "My Image" - self.portal.image.description = u"This is an image" - image_file = os.path.join(os.path.dirname(__file__), u"image.png") + self.portal.image.description = "This is an image" + image_file = os.path.join(os.path.dirname(__file__), "image.png") fd = open(image_file, "rb") self.portal.image.image = NamedBlobImage( - data=fd.read(), contentType="image/png", filename=u"image.png" + data=fd.read(), contentType="image/png", filename="image.png" ) fd.close() import transaction @@ -218,13 +216,13 @@ def test_dexterity_image_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"image", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("image", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_dexterity_collection_get(self): self.portal.invokeFactory("Collection", id="collection") self.portal.collection.title = "My Collection" - self.portal.collection.description = u"This is a collection with two documents" + self.portal.collection.description = "This is a collection with two documents" self.portal.collection.query = [ { "i": "portal_type", @@ -243,5 +241,5 @@ def test_dexterity_collection_get(self): ) self.assertEqual(200, response.status_code) - self.assertEqual(u"collection", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("collection", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) diff --git a/src/plone/rest/tests/test_dispatching.py b/src/plone/rest/tests/test_dispatching.py index 674e2f67..5c9204d0 100644 --- a/src/plone/rest/tests/test_dispatching.py +++ b/src/plone/rest/tests/test_dispatching.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from plone.app.testing import setRoles from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD @@ -17,7 +16,6 @@ class DispatchingTestCase(unittest.TestCase): - layer = PLONE_REST_FUNCTIONAL_TESTING def setUp(self): @@ -54,7 +52,7 @@ def validate(self, expectations, follow_redirects=False): if failures: msg = "" - for (request_args, expected_status, actual_status) in failures: + for request_args, expected_status, actual_status in failures: msg += ( "\n" "Request: %s\n" @@ -137,7 +135,7 @@ def test_not_found_invalid_creds(self): class TestDispatchingDexterity(DispatchingTestCase): def setUp(self): - super(TestDispatchingDexterity, self).setUp() + super().setUp() self.portal.invokeFactory("Folder", id="private") self.portal.invokeFactory("Folder", id="public") @@ -216,7 +214,7 @@ def test_public_dx_folder_invalid_creds(self): class TestDispatchingRedirects(DispatchingTestCase): def setUp(self): - super(TestDispatchingRedirects, self).setUp() + super().setUp() self.portal.invokeFactory("Folder", id="private-old") self.portal.manage_renameObject("private-old", "private-new") @@ -231,12 +229,12 @@ def setUp(self): def test_moved_private_dx_folder_with_creds(self): expectations = [ - ("/private-old", "GET", CREDS, 301), - ("/private-old", "POST", CREDS, 308), - ("/private-old", "PUT", CREDS, 308), - ("/private-old", "PATCH", CREDS, 308), - ("/private-old", "DELETE", CREDS, 308), - ("/private-old", "OPTIONS", CREDS, 308), + ("/private-old", "GET", CREDS, 302), + ("/private-old", "POST", CREDS, 307), + ("/private-old", "PUT", CREDS, 307), + ("/private-old", "PATCH", CREDS, 307), + ("/private-old", "DELETE", CREDS, 307), + ("/private-old", "OPTIONS", CREDS, 307), ] self.validate(expectations) @@ -253,12 +251,12 @@ def test_moved_private_dx_folder_with_creds(self): def test_moved_private_dx_folder_without_creds(self): expectations = [ - ("/private-old", "GET", NO_CREDS, 301), - ("/private-old", "POST", NO_CREDS, 308), - ("/private-old", "PUT", NO_CREDS, 308), - ("/private-old", "PATCH", NO_CREDS, 308), - ("/private-old", "DELETE", NO_CREDS, 308), - ("/private-old", "OPTIONS", NO_CREDS, 308), + ("/private-old", "GET", NO_CREDS, 302), + ("/private-old", "POST", NO_CREDS, 307), + ("/private-old", "PUT", NO_CREDS, 307), + ("/private-old", "PATCH", NO_CREDS, 307), + ("/private-old", "DELETE", NO_CREDS, 307), + ("/private-old", "OPTIONS", NO_CREDS, 307), ] self.validate(expectations) @@ -275,12 +273,12 @@ def test_moved_private_dx_folder_without_creds(self): def test_moved_private_dx_folder_invalid_creds(self): expectations = [ - ("/private-old", "GET", INVALID_CREDS, 301), - ("/private-old", "POST", INVALID_CREDS, 308), - ("/private-old", "PUT", INVALID_CREDS, 308), - ("/private-old", "PATCH", INVALID_CREDS, 308), - ("/private-old", "DELETE", INVALID_CREDS, 308), - ("/private-old", "OPTIONS", INVALID_CREDS, 308), + ("/private-old", "GET", INVALID_CREDS, 302), + ("/private-old", "POST", INVALID_CREDS, 307), + ("/private-old", "PUT", INVALID_CREDS, 307), + ("/private-old", "PATCH", INVALID_CREDS, 307), + ("/private-old", "DELETE", INVALID_CREDS, 307), + ("/private-old", "OPTIONS", INVALID_CREDS, 307), ] self.validate(expectations) @@ -297,12 +295,12 @@ def test_moved_private_dx_folder_invalid_creds(self): def test_moved_public_dx_folder_with_creds(self): expectations = [ - ("/public-old", "GET", CREDS, 301), - ("/public-old", "POST", CREDS, 308), - ("/public-old", "PUT", CREDS, 308), - ("/public-old", "PATCH", CREDS, 308), - ("/public-old", "DELETE", CREDS, 308), - ("/public-old", "OPTIONS", CREDS, 308), + ("/public-old", "GET", CREDS, 302), + ("/public-old", "POST", CREDS, 307), + ("/public-old", "PUT", CREDS, 307), + ("/public-old", "PATCH", CREDS, 307), + ("/public-old", "DELETE", CREDS, 307), + ("/public-old", "OPTIONS", CREDS, 307), ] self.validate(expectations) @@ -319,12 +317,12 @@ def test_moved_public_dx_folder_with_creds(self): def test_moved_public_dx_folder_without_creds(self): expectations = [ - ("/public-old", "GET", NO_CREDS, 301), - ("/public-old", "POST", NO_CREDS, 308), - ("/public-old", "PUT", NO_CREDS, 308), - ("/public-old", "PATCH", NO_CREDS, 308), - ("/public-old", "DELETE", NO_CREDS, 308), - ("/public-old", "OPTIONS", NO_CREDS, 308), + ("/public-old", "GET", NO_CREDS, 302), + ("/public-old", "POST", NO_CREDS, 307), + ("/public-old", "PUT", NO_CREDS, 307), + ("/public-old", "PATCH", NO_CREDS, 307), + ("/public-old", "DELETE", NO_CREDS, 307), + ("/public-old", "OPTIONS", NO_CREDS, 307), ] self.validate(expectations) @@ -341,12 +339,12 @@ def test_moved_public_dx_folder_without_creds(self): def test_moved_public_dx_folder_invalid_creds(self): expectations = [ - ("/public-old", "GET", INVALID_CREDS, 301), - ("/public-old", "POST", INVALID_CREDS, 308), - ("/public-old", "PUT", INVALID_CREDS, 308), - ("/public-old", "PATCH", INVALID_CREDS, 308), - ("/public-old", "DELETE", INVALID_CREDS, 308), - ("/public-old", "OPTIONS", INVALID_CREDS, 308), + ("/public-old", "GET", INVALID_CREDS, 302), + ("/public-old", "POST", INVALID_CREDS, 307), + ("/public-old", "PUT", INVALID_CREDS, 307), + ("/public-old", "PATCH", INVALID_CREDS, 307), + ("/public-old", "DELETE", INVALID_CREDS, 307), + ("/public-old", "OPTIONS", INVALID_CREDS, 307), ] self.validate(expectations) diff --git a/src/plone/rest/tests/test_error_handling.py b/src/plone/rest/tests/test_error_handling.py index b2cf5111..32711a4b 100644 --- a/src/plone/rest/tests/test_error_handling.py +++ b/src/plone/rest/tests/test_error_handling.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- from plone.app.testing import setRoles -from plone.app.testing import TEST_USER_ID -from plone.app.testing import TEST_USER_PASSWORD from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID +from plone.app.testing import TEST_USER_PASSWORD from plone.rest.testing import PLONE_REST_FUNCTIONAL_TESTING import json @@ -13,7 +12,6 @@ class TestErrorHandling(unittest.TestCase): - layer = PLONE_REST_FUNCTIONAL_TESTING def setUp(self): @@ -83,7 +81,7 @@ def test_500_internal_server_error(self): self.assertEqual("HTTPError", response.json()["type"]) self.assertEqual( - {u"type": u"HTTPError", u"message": u"HTTP Error 500: InternalServerError"}, + {"type": "HTTPError", "message": "HTTP Error 500: InternalServerError"}, response.json(), ) @@ -94,7 +92,7 @@ def test_500_traceback_only_for_manager_users(self): headers={"Accept": "application/json"}, auth=(TEST_USER_ID, TEST_USER_PASSWORD), ) - self.assertNotIn(u"traceback", response.json()) + self.assertNotIn("traceback", response.json()) # Manager user response = requests.get( @@ -102,10 +100,10 @@ def test_500_traceback_only_for_manager_users(self): headers={"Accept": "application/json"}, auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) - self.assertIn(u"traceback", response.json()) + self.assertIn("traceback", response.json()) - traceback = response.json()[u"traceback"] + traceback = response.json()["traceback"] self.assertIsInstance(traceback, list) - self.assertRegexpMatches( + self.assertRegex( traceback[0], r'^File "[^"]*", line \d*, in (publish|transaction_pubevents)' ) diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py new file mode 100644 index 00000000..7c5d8cc2 --- /dev/null +++ b/src/plone/rest/tests/test_explicitacquisition.py @@ -0,0 +1,126 @@ +from base64 import b64encode +from plone.app.testing import setRoles +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID +from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING +from zExceptions import NotFound +from zope.event import notify +from ZPublisher.pubevents import PubAfterTraversal +from ZPublisher.pubevents import PubStart + +import unittest + + +try: + from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication + + IShouldAllowAcquiredItemPublication # flake8 + + HAS_CMFCORE_32 = True +except ImportError: + HAS_CMFCORE_32 = False + + +@unittest.skipUnless( + not HAS_CMFCORE_32, + "Older Plone versions don't have CMFCore>=3.2", +) +class TestExplicitAcquisitionUnavailable(unittest.TestCase): + layer = PLONE_REST_INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer["portal"] + self.request = self.layer["request"] + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + self.portal.invokeFactory("Document", id="foo") + + def traverse(self, path="/plone", accept="application/json", method="GET"): + request = self.layer["request"] + request.environ["PATH_INFO"] = path + request.environ["PATH_TRANSLATED"] = path + request.environ["HTTP_ACCEPT"] = accept + request.environ["REQUEST_METHOD"] = method + auth = f"{SITE_OWNER_NAME}:{SITE_OWNER_PASSWORD}" + b64auth = b64encode(auth.encode("utf8")) + request._auth = "Basic %s" % b64auth.decode("utf8") + notify(PubStart(request)) + return request.traverse(path) + + def test_portal_root(self): + self.traverse("/plone") + notify(PubAfterTraversal(self.request)) + + def test_portal_foo(self): + self.traverse("/plone/foo") + notify(PubAfterTraversal(self.request)) + + def test_portal_foo_acquired(self): + self.traverse("/plone/foo/foo") + notify(PubAfterTraversal(self.request)) + + +@unittest.skipUnless( + HAS_CMFCORE_32, + "We have Products.CMFCore >= 3.2", +) +class TestExplicitAcquisitionAvailable(unittest.TestCase): + layer = PLONE_REST_INTEGRATION_TESTING + + def setUp(self): + import Products.CMFCore.explicitacquisition + + self.portal = self.layer["portal"] + self.request = self.layer["request"] + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + self.portal.invokeFactory("Document", id="foo") + self.PREVIOUS_SKIP_PTA = Products.CMFCore.explicitacquisition.SKIP_PTA + + def tearDown(self): + import Products.CMFCore.explicitacquisition + + Products.CMFCore.explicitacquisition.SKIP_PTA = self.PREVIOUS_SKIP_PTA + + def traverse(self, path="/plone", accept="application/json", method="GET"): + request = self.layer["request"] + request.environ["PATH_INFO"] = path + request.environ["PATH_TRANSLATED"] = path + request.environ["HTTP_ACCEPT"] = accept + request.environ["REQUEST_METHOD"] = method + auth = f"{SITE_OWNER_NAME}:{SITE_OWNER_PASSWORD}" + b64auth = b64encode(auth.encode("utf8")) + request._auth = "Basic %s" % b64auth.decode("utf8") + notify(PubStart(request)) + return request.traverse(path) + + def test_portal_root(self): + import Products.CMFCore.explicitacquisition + + self.traverse("/plone") + Products.CMFCore.explicitacquisition.SKIP_PTA = True + notify(PubAfterTraversal(self.request)) + + Products.CMFCore.explicitacquisition.SKIP_PTA = False + notify(PubAfterTraversal(self.request)) + + def test_portal_foo(self): + import Products.CMFCore.explicitacquisition + + self.traverse("/plone/foo") + Products.CMFCore.explicitacquisition.SKIP_PTA = True + notify(PubAfterTraversal(self.request)) + + Products.CMFCore.explicitacquisition.SKIP_PTA = False + notify(PubAfterTraversal(self.request)) + + def test_portal_foo_acquired(self): + import Products.CMFCore.explicitacquisition + + self.traverse("/plone/foo/foo") + + Products.CMFCore.explicitacquisition.SKIP_PTA = True + notify(PubAfterTraversal(self.request)) + + Products.CMFCore.explicitacquisition.SKIP_PTA = False + with self.assertRaises(NotFound): + notify(PubAfterTraversal(self.request)) diff --git a/src/plone/rest/tests/test_named_services.py b/src/plone/rest/tests/test_named_services.py index fe1977bb..05feb60d 100644 --- a/src/plone/rest/tests/test_named_services.py +++ b/src/plone/rest/tests/test_named_services.py @@ -1,17 +1,15 @@ -# -*- coding: utf-8 -*- from plone.app.testing import setRoles -from plone.app.testing import TEST_USER_ID from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID from plone.rest.testing import PLONE_REST_FUNCTIONAL_TESTING -import unittest import requests import transaction +import unittest class TestNamedServiceEndpoints(unittest.TestCase): - layer = PLONE_REST_FUNCTIONAL_TESTING def setUp(self): @@ -31,7 +29,7 @@ def test_dexterity_named_get(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual({u"service": u"named get"}, response.json()) + self.assertEqual({"service": "named get"}, response.json()) def test_dexterity_named_post(self): response = requests.post( @@ -40,7 +38,7 @@ def test_dexterity_named_post(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual({u"service": u"named post"}, response.json()) + self.assertEqual({"service": "named post"}, response.json()) def test_dexterity_named_put(self): response = requests.put( @@ -49,7 +47,7 @@ def test_dexterity_named_put(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual({u"service": u"named put"}, response.json()) + self.assertEqual({"service": "named put"}, response.json()) def test_dexterity_named_patch(self): response = requests.patch( @@ -58,7 +56,7 @@ def test_dexterity_named_patch(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual({u"service": u"named patch"}, response.json()) + self.assertEqual({"service": "named patch"}, response.json()) def test_dexterity_named_delete(self): response = requests.delete( @@ -67,7 +65,7 @@ def test_dexterity_named_delete(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual({u"service": u"named delete"}, response.json()) + self.assertEqual({"service": "named delete"}, response.json()) def test_dexterity_named_options(self): response = requests.options( @@ -76,4 +74,4 @@ def test_dexterity_named_options(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual({u"service": u"named options"}, response.json()) + self.assertEqual({"service": "named options"}, response.json()) diff --git a/src/plone/rest/tests/test_negotiation.py b/src/plone/rest/tests/test_negotiation.py index 70edd5ae..3978b887 100644 --- a/src/plone/rest/tests/test_negotiation.py +++ b/src/plone/rest/tests/test_negotiation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from plone.rest.negotiation import lookup_service_id from plone.rest.negotiation import parse_accept_header from plone.rest.negotiation import register_service @@ -61,24 +60,24 @@ def test_parse_invalid_accept_header(self): class TestServiceRegistry(unittest.TestCase): def test_register_media_type(self): self.assertEqual( - u"GET_application_json_", register_service("GET", ("application", "json")) + "GET_application_json_", register_service("GET", ("application", "json")) ) self.assertEqual( - u"GET_application_json_", lookup_service_id("GET", "application/json") + "GET_application_json_", lookup_service_id("GET", "application/json") ) def test_register_wildcard_subtype(self): - self.assertEqual(u"PATCH_text_*_", register_service("PATCH", ("text", "*"))) - self.assertEqual(u"PATCH_text_*_", lookup_service_id("PATCH", "text/xml")) + self.assertEqual("PATCH_text_*_", register_service("PATCH", ("text", "*"))) + self.assertEqual("PATCH_text_*_", lookup_service_id("PATCH", "text/xml")) def test_register_wilcard_type(self): - self.assertEqual(u"PATCH_*_*_", register_service("PATCH", ("*", "*"))) - self.assertEqual(u"PATCH_*_*_", lookup_service_id("PATCH", "foo/bar")) + self.assertEqual("PATCH_*_*_", register_service("PATCH", ("*", "*"))) + self.assertEqual("PATCH_*_*_", lookup_service_id("PATCH", "foo/bar")) def test_service_id_for_multiple_media_types_is_none(self): register_service("GET", "application/json") self.assertEqual( - None, lookup_service_id("GET", "application/json,application/javascipt") + None, lookup_service_id("GET", "application/json,application/javascript") ) def test_service_id_for_invalid_media_type_is_none(self): diff --git a/src/plone/rest/tests/test_permissions.py b/src/plone/rest/tests/test_permissions.py index 2ab0df6a..166b0974 100644 --- a/src/plone/rest/tests/test_permissions.py +++ b/src/plone/rest/tests/test_permissions.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- -from Products.CMFCore.utils import getToolByName -from ZPublisher.pubevents import PubStart from base64 import b64encode +from plone.app.testing import login +from plone.app.testing import setRoles from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_NAME from plone.app.testing import TEST_USER_PASSWORD -from plone.app.testing import login -from plone.app.testing import setRoles from plone.rest.service import Service from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING +from Products.CMFCore.utils import getToolByName from zExceptions import Unauthorized from zope.event import notify +from ZPublisher.pubevents import PubStart import unittest class TestPermissions(unittest.TestCase): - layer = PLONE_REST_INTEGRATION_TESTING def setUp(self): @@ -36,7 +34,7 @@ def traverse(self, path="/plone", accept="application/json", method="GET"): request.environ["PATH_TRANSLATED"] = path request.environ["HTTP_ACCEPT"] = accept request.environ["REQUEST_METHOD"] = method - auth = "%s:%s" % (TEST_USER_NAME, TEST_USER_PASSWORD) + auth = f"{TEST_USER_NAME}:{TEST_USER_PASSWORD}" b64auth = b64encode(auth.encode("utf8")) request._auth = "Basic %s" % b64auth.decode("utf8") notify(PubStart(request)) diff --git a/src/plone/rest/tests/test_redirects.py b/src/plone/rest/tests/test_redirects.py index 37c61dca..a172a4d9 100644 --- a/src/plone/rest/tests/test_redirects.py +++ b/src/plone/rest/tests/test_redirects.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from BTrees.OOBTree import OOSet from plone.app.redirector.interfaces import IRedirectionStorage from plone.app.testing import setRoles @@ -15,7 +14,6 @@ class TestRedirects(unittest.TestCase): - layer = PLONE_REST_FUNCTIONAL_TESTING def setUp(self): @@ -28,28 +26,73 @@ def setUp(self): self.portal.manage_renameObject("folder-old", "folder-new") transaction.commit() - def test_get_to_moved_item_causes_301_redirect(self): + def test_get_to_moved_item_causes_302_redirect(self): response = requests.get( self.portal_url + "/folder-old", headers={"Accept": "application/json"}, auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual(self.portal_url + "/folder-new", response.headers["Location"]) self.assertEqual(b"", response.raw.read()) - def test_post_to_moved_item_causes_308_redirect(self): + def test_get_to_moved_item_causes_302_redirect_with_api_traverser(self): + response = requests.get( + self.portal_url + "/++api++/folder-old", + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), + allow_redirects=False, + ) + self.assertEqual(302, response.status_code) + self.assertEqual( + self.portal_url + "/++api++/folder-new", response.headers["Location"] + ) + self.assertEqual(b"", response.raw.read()) + # follow the new location + response = requests.get( + response.headers["Location"], + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), + ) + self.assertEqual(200, response.status_code) + self.assertEqual("application/json", response.headers["Content-type"]) + self.assertEqual({"id": "folder-new", "method": "GET"}, response.json()) + + def test_get_to_moved_item_causes_302_redirect_with_rest_view(self): + response = requests.get( + self.portal_url + "/++api++/folder-old/@actions", + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), + allow_redirects=False, + ) + self.assertEqual(302, response.status_code) + self.assertEqual( + self.portal_url + "/++api++/folder-new/@actions", + response.headers["Location"], + ) + self.assertEqual(b"", response.raw.read()) + + def test_post_to_moved_item_causes_307_redirect(self): response = requests.post( self.portal_url + "/folder-old", headers={"Accept": "application/json"}, auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(308, response.status_code) + self.assertEqual(307, response.status_code) self.assertEqual(self.portal_url + "/folder-new", response.headers["Location"]) self.assertEqual(b"", response.raw.read()) + def test_post_to_moved_item_causes_307_redirect_with_api_traverser(self): + response = requests.post( + self.portal_url + "/++api++/folder-old", + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), + allow_redirects=False, + ) + self.assertEqual(307, response.status_code) + self.assertEqual( + self.portal_url + "/++api++/folder-new", response.headers["Location"] + ) + self.assertEqual(b"", response.raw.read()) + def test_unauthorized_request_to_item_still_redirects_first(self): response = requests.get( self.portal_url + "/folder-old", @@ -60,7 +103,7 @@ def test_unauthorized_request_to_item_still_redirects_first(self): # A request to the old URL of an item where the user doesn't have # necessary permissions will still result in a redirect - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual(self.portal_url + "/folder-new", response.headers["Location"]) self.assertEqual(b"", response.raw.read()) @@ -80,20 +123,20 @@ def test_query_string_gets_preserved(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual( self.portal_url + "/folder-new?key=value", response.headers["Location"] ) self.assertEqual(b"", response.raw.read()) - def test_named_service_on_moved_item_causes_301_redirect(self): + def test_named_service_on_moved_item_causes_302_redirect(self): response = requests.get( self.portal_url + "/folder-old/namedservice", headers={"Accept": "application/json"}, auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual( self.portal_url + "/folder-new/namedservice", response.headers["Location"] ) @@ -106,7 +149,7 @@ def test_named_service_plus_path_parameter_works(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual( self.portal_url + "/folder-new/namedservice/param", response.headers["Location"], @@ -120,7 +163,7 @@ def test_redirects_for_regular_views_still_work(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual( self.portal_url + "/folder-new/@@some-view", response.headers["Location"] ) @@ -133,7 +176,7 @@ def test_redirects_for_views_plus_params_plus_querystring_works(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual( self.portal_url + "/folder-new/@@some-view/param?k=v", response.headers["Location"], @@ -165,10 +208,22 @@ def test_handles_redirects_that_include_querystring_in_old_path(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), allow_redirects=False, ) - self.assertEqual(301, response.status_code) + self.assertEqual(302, response.status_code) self.assertEqual(self.portal_url + "/new-item", response.headers["Location"]) self.assertEqual(b"", response.raw.read()) + def test_handles_redirects_that_are_recursive(self): + storage = queryUtility(IRedirectionStorage) + storage.add("/plone/folder-new", "/plone/folder-new/archive") + transaction.commit() + # Request should return 404 + response = requests.get( + self.portal_url + "/folder-new/sub_folder/not-found", + headers={"Accept": "application/json"}, + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), + ) + self.assertEqual(404, response.status_code) + def test_aborts_redirect_checks_early_for_app_root(self): error_view = ErrorHandling(self.portal, self.portal.REQUEST) self.assertIsNone(error_view.find_redirect_if_view_or_service([""], None)) @@ -176,4 +231,4 @@ def test_aborts_redirect_checks_early_for_app_root(self): def test_gracefully_deals_with_missing_request_url(self): error_view = ErrorHandling(self.portal, self.portal.REQUEST) self.portal.REQUEST["ACTUAL_URL"] = None - self.assertEquals(False, error_view.attempt_redirect()) + self.assertEqual(False, error_view.attempt_redirect()) diff --git a/src/plone/rest/tests/test_siteroot.py b/src/plone/rest/tests/test_siteroot.py index 6cf71d59..9e992938 100644 --- a/src/plone/rest/tests/test_siteroot.py +++ b/src/plone/rest/tests/test_siteroot.py @@ -1,16 +1,14 @@ -# -*- coding: utf-8 -*- -from plone.rest.testing import PLONE_REST_FUNCTIONAL_TESTING from plone.app.testing import setRoles -from plone.app.testing import TEST_USER_ID from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID +from plone.rest.testing import PLONE_REST_FUNCTIONAL_TESTING -import unittest import requests +import unittest class TestSiteRootServiceEndpoints(unittest.TestCase): - layer = PLONE_REST_FUNCTIONAL_TESTING def setUp(self): @@ -33,8 +31,8 @@ def test_siteroot_get(self): response.status_code ), ) - self.assertEqual(u"plone", response.json().get("id")) - self.assertEqual(u"GET", response.json().get("method")) + self.assertEqual("plone", response.json().get("id")) + self.assertEqual("GET", response.json().get("method")) def test_siteroot_post(self): response = requests.post( @@ -49,8 +47,8 @@ def test_siteroot_post(self): response.status_code ), ) - self.assertEqual(u"plone", response.json().get("id")) - self.assertEqual(u"POST", response.json().get("method")) + self.assertEqual("plone", response.json().get("id")) + self.assertEqual("POST", response.json().get("method")) def test_siteroot_delete(self): response = requests.delete( @@ -59,8 +57,8 @@ def test_siteroot_delete(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual(u"plone", response.json().get("id")) - self.assertEqual(u"DELETE", response.json().get("method")) + self.assertEqual("plone", response.json().get("id")) + self.assertEqual("DELETE", response.json().get("method")) def test_siteroot_put(self): response = requests.put( @@ -70,8 +68,8 @@ def test_siteroot_put(self): ) self.assertEqual(response.status_code, 200) - self.assertEqual(u"plone", response.json().get("id")) - self.assertEqual(u"PUT", response.json().get("method")) + self.assertEqual("plone", response.json().get("id")) + self.assertEqual("PUT", response.json().get("method")) def test_siteroot_patch(self): response = requests.patch( @@ -81,8 +79,8 @@ def test_siteroot_patch(self): ) self.assertEqual(response.status_code, 200) - self.assertEqual(u"plone", response.json().get("id")) - self.assertEqual(u"PATCH", response.json().get("method")) + self.assertEqual("plone", response.json().get("id")) + self.assertEqual("PATCH", response.json().get("method")) def test_siteroot_options(self): response = requests.options( @@ -91,5 +89,5 @@ def test_siteroot_options(self): auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD), ) self.assertEqual(response.status_code, 200) - self.assertEqual(u"plone", response.json().get("id")) - self.assertEqual(u"OPTIONS", response.json().get("method")) + self.assertEqual("plone", response.json().get("id")) + self.assertEqual("OPTIONS", response.json().get("method")) diff --git a/src/plone/rest/tests/test_traversal.py b/src/plone/rest/tests/test_traversal.py index 8701c2cb..a9061cab 100644 --- a/src/plone/rest/tests/test_traversal.py +++ b/src/plone/rest/tests/test_traversal.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from Products.SiteAccess.VirtualHostMonster import VirtualHostMonster -from ZPublisher import BeforeTraverse -from ZPublisher.pubevents import PubStart from base64 import b64encode from plone.app.layout.navigation.interfaces import INavigationRoot from plone.app.testing import setRoles @@ -10,15 +6,19 @@ from plone.app.testing import TEST_USER_ID from plone.rest.service import Service from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING +from Products.SiteAccess.VirtualHostMonster import VirtualHostMonster +from zExceptions import NotFound +from zExceptions import Redirect from zope.event import notify from zope.interface import alsoProvides from zope.publisher.interfaces.browser import IBrowserView +from ZPublisher import BeforeTraverse +from ZPublisher.pubevents import PubStart import unittest class TestTraversal(unittest.TestCase): - layer = PLONE_REST_INTEGRATION_TESTING def setUp(self): @@ -32,7 +32,7 @@ def traverse(self, path="/plone", accept="application/json", method="GET"): request.environ["PATH_TRANSLATED"] = path request.environ["HTTP_ACCEPT"] = accept request.environ["REQUEST_METHOD"] = method - auth = "%s:%s" % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + auth = f"{SITE_OWNER_NAME}:{SITE_OWNER_PASSWORD}" b64auth = b64encode(auth.encode("utf8")) request._auth = "Basic %s" % b64auth.decode("utf8") notify(PubStart(request)) @@ -60,18 +60,18 @@ def test_json_request_on_content_object_returns_service(self): def test_html_request_on_portal_root_returns_default_view(self): obj = self.traverse(accept="text/html") - self.assertEquals("listing_view", obj.__name__) + self.assertEqual(self.portal.getDefaultLayout(), obj.__name__) def test_html_request_on_portal_root_returns_dynamic_view(self): self.portal.setLayout("summary_view") obj = self.traverse(accept="text/html") - self.assertEquals("summary_view", obj.__name__) + self.assertEqual("summary_view", obj.__name__) def test_html_request_on_portal_root_returns_default_page(self): self.portal.invokeFactory("Document", id="doc1") self.portal.setDefaultPage("doc1") obj = self.traverse(accept="text/html") - self.assertEquals("document_view", obj.__name__) + self.assertEqual("document_view", obj.__name__) def test_json_request_on_object_with_multihook(self): doc1 = self.portal[self.portal.invokeFactory("Document", id="doc1")] @@ -86,7 +86,7 @@ def btr_test(container, request): obj = self.traverse(path="/plone/doc1") self.assertTrue(isinstance(obj, Service), "Not a service") - self.assertEquals(1, self.request._btr_test_called) + self.assertEqual(1, self.request._btr_test_called) def test_json_request_on_existing_view_returns_named_service(self): obj = self.traverse("/plone/search") @@ -106,6 +106,45 @@ def test_html_request_on_existing_view_returns_view(self): obj = self.traverse(path="/plone/folder1/search", accept="text/html") self.assertFalse(isinstance(obj, Service), "Got a service") + def test_html_request_via_api_returns_service(self): + obj = self.traverse(path="/plone/++api++", accept="text/html") + self.assertTrue(isinstance(obj, Service), "Not a service") + + def test_html_request_via_double_apis_raises_redirect(self): + portal_url = self.portal.absolute_url() + with self.assertRaises(Redirect) as exc: + self.traverse(path="/plone/++api++/++api++", accept="text/html") + self.assertEqual( + exc.exception.headers["Location"], + f"{portal_url}/++api++", + ) + + def test_html_request_via_multiple_apis_raises_redirect(self): + portal_url = self.portal.absolute_url() + with self.assertRaises(Redirect) as exc: + self.traverse( + path="/plone/++api++/++api++/++api++/search", accept="text/html" + ) + self.assertEqual( + exc.exception.headers["Location"], + f"{portal_url}/++api++/search", + ) + + def test_html_request_via_multiple_bad_apis_raises_not_found(self): + with self.assertRaises(NotFound): + self.traverse(path="/plone/++api++/search/++api++", accept="text/html") + + # def test_virtual_hosting(self): + # app = self.layer["app"] + # vhm = VirtualHostMonster() + # vhm.id = "virtual_hosting" + # vhm.addToContainer(app) + # obj = self.traverse( + # path="/VirtualHostBase/http/localhost:8080/plone/VirtualHostRoot/" + # ) # noqa + # self.assertTrue(isinstance(obj, Service), "Not a service") + # del app["virtual_hosting"] + def test_json_request_to_regular_view_returns_view(self): obj = self.traverse("/plone/folder_contents") self.assertTrue(IBrowserView.providedBy(obj), "IBrowserView expected") diff --git a/src/plone/rest/traverse.py b/src/plone/rest/traverse.py index 99573895..576eab21 100644 --- a/src/plone/rest/traverse.py +++ b/src/plone/rest/traverse.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- -from plone.rest.interfaces import IAPIRequest from plone.rest.events import mark_as_api_request +from plone.rest.interfaces import IAPIRequest +# from plone.rest.interfaces import IService +from Products.CMFCore.interfaces import IContentish +from Products.CMFCore.interfaces import ISiteRoot +# from Products.SiteAccess.VirtualHostMonster import VirtualHostMonster +from zExceptions import Redirect from zope.component import adapter from zope.component import queryMultiAdapter from zope.interface import implementer from zope.publisher.interfaces.browser import IBrowserPublisher -from ZPublisher.BaseRequest import DefaultPublishTraverse from zope.traversing.interfaces import ITraversable -from Products.CMFCore.interfaces import IContentish -from Products.CMFCore.interfaces import ISiteRoot +from ZPublisher.BaseRequest import DefaultPublishTraverse class RESTPublishTraverse(object): @@ -31,7 +34,7 @@ def publishTraverse(self, request, name): def browserDefault(self, request): # Called when we have reached the end of the path - # In our case this means an unamed service + # In our case this means an unnamed service return self.context, (request._rest_service_id,) @@ -41,7 +44,7 @@ class RESTTraverse(RESTPublishTraverse, DefaultPublishTraverse): @implementer(ITraversable) -class MarkAsRESTTraverser(object): +class MarkAsRESTTraverser: """ Traversal adapter for the ``++api++`` namespace. It marks the request as API request. @@ -52,6 +55,18 @@ def __init__(self, context, request): self.request = request def traverse(self, name_ignored, subpath_ignored): + name = "/++api++" + url = self.request.ACTUAL_URL + if url.count(name) > 1: + # Redirect to proper url. + while f"{name}{name}" in url: + url = url.replace(f"{name}{name}", name) + if url.count(name) > 1: + # Something like: .../++api++/something/++api++ + # Return nothing, so a NotFound is raised. + return + # Raise a redirect exception to stop execution of the current request. + raise Redirect(url) mark_as_api_request(self.request, "application/json") return self.context @@ -74,7 +89,7 @@ def __getitem__(self, name): # Delegate key access to the wrapped object return self.context[name] - # MultiHook requries this to be a class attribute + # MultiHook requires this to be a class attribute def __before_publishing_traverse__(self, arg1, arg2=None): bpth = getattr(self.context, "__before_publishing_traverse__", False) if bpth: diff --git a/src/plone/rest/zcml.py b/src/plone/rest/zcml.py index 3e336020..67c108f3 100644 --- a/src/plone/rest/zcml.py +++ b/src/plone/rest/zcml.py @@ -1,13 +1,12 @@ -# -*- coding: utf-8 -*- from AccessControl.class_init import InitializeClass from AccessControl.security import getSecurityInfo from AccessControl.security import protectClass -from Products.Five.browser import BrowserView from plone.rest.cors import CORSPolicy from plone.rest.cors import register_method_for_preflight from plone.rest.interfaces import ICORSPolicy from plone.rest.negotiation import parse_accept_header from plone.rest.negotiation import register_service +from Products.Five.browser import BrowserView from zope.browserpage.metaconfigure import _handle_for from zope.component.zcml import handler from zope.configuration.fields import GlobalInterface @@ -23,48 +22,48 @@ class IService(Interface): """ """ method = TextLine( - title=u"The name of the view that should be the default. " - + u"[get|post|put|delete]", - description=u""" + title="The name of the view that should be the default. " + + "[get|post|put|delete]", + description=""" This name refers to view that should be the view used by default (if no view name is supplied explicitly).""", ) accept = TextLine( - title=u"Acceptable media types", - description=u"""Specifies the media type used for content negotiation. + title="Acceptable media types", + description="""Specifies the media type used for content negotiation. The service is limited to the given media type and only called if the request contains an "Accept" header with the given media type. Multiple media types can be given by separating them with a comma.""", - default=u"application/json", + default="application/json", ) for_ = GlobalObject( - title=u"The interface this view is the default for.", - description=u"""Specifies the interface for which the view is + title="The interface this view is the default for.", + description="""Specifies the interface for which the view is registered. All objects implementing this interface can make use of this view. If this attribute is not specified, the view is available for all objects.""", ) factory = GlobalObject( - title=u"The factory for this service", - description=u"The factory is usually subclass of the Service class.", + title="The factory for this service", + description="The factory is usually subclass of the Service class.", ) name = TextLine( - title=u"The name of the service.", - description=u"""When no name is defined, the service is available at + title="The name of the service.", + description="""When no name is defined, the service is available at the object's absolute URL. When defining a name, the service is available at the object's absolute URL appended with a slash and the service name.""", required=False, - default=u"", + default="", ) layer = GlobalInterface( - title=u"The browser layer for which this service is registered.", - description=u"""Useful for overriding existing services or for making + title="The browser layer for which this service is registered.", + description="""Useful for overriding existing services or for making services available only if a specific add-on has been installed.""", required=False, @@ -72,8 +71,8 @@ class IService(Interface): ) permission = Permission( - title=u"Permission", - description=u"The permission needed to access the service.", + title="Permission", + description="The permission needed to access the service.", required=True, ) @@ -86,9 +85,8 @@ def serviceDirective( for_, permission, layer=IDefaultBrowserLayer, - name=u"", + name="", ): - _handle_for(_context, for_) media_types = parse_accept_header(accept) @@ -137,16 +135,16 @@ class ICORSPolicyDirective(Interface): """Directive for defining CORS policies""" for_ = GlobalObject( - title=u"The interface this CORS policy is for.", - description=u"""Specifies the interface for which the CORS policy is + title="The interface this CORS policy is for.", + description="""Specifies the interface for which the CORS policy is registered. If this attribute is not specified, the CORS policy applies to all objects.""", required=False, ) layer = GlobalInterface( - title=u"The browser layer for which this CORS policy is registered.", - description=u"""Useful for overriding existing policies or for making + title="The browser layer for which this CORS policy is registered.", + description="""Useful for overriding existing policies or for making them available only if a specific add-on has been installed.""", required=False, @@ -154,45 +152,45 @@ class ICORSPolicyDirective(Interface): ) allow_origin = TextLine( - title=u"Origins", - description=u"""Origins that are allowed access to the resource. Either + title="Origins", + description="""Origins that are allowed access to the resource. Either a comma separated list of origins, e.g. "http://example.net, http://mydomain.com" or "*".""", ) allow_methods = TextLine( - title=u"Methods", - description=u"""A comma separated list of HTTP method names that are + title="Methods", + description="""A comma separated list of HTTP method names that are allowed by this CORS policy, e.g. "DELETE,GET,OPTIONS,PATCH,POST,PUT". """, required=False, ) allow_headers = TextLine( - title=u"Headers", - description=u"""A comma separated list of request headers allowed to be + title="Headers", + description="""A comma separated list of request headers allowed to be sent by the client, e.g. "X-My-Header".""", required=False, ) expose_headers = TextLine( - title=u"Exposed Headers", - description=u"""A comma separated list of response headers clients can + title="Exposed Headers", + description="""A comma separated list of response headers clients can access, e.g. "Content-Length,X-My-Header".""", required=False, ) allow_credentials = Bool( - title=u"Support Credentials", - description=u"""Indicates whether the resource supports user + title="Support Credentials", + description="""Indicates whether the resource supports user credentials in the request.""", required=True, default=False, ) max_age = TextLine( - title=u"Max Age", - description=u"""Indicates how long the results of a preflight request + title="Max Age", + description="""Indicates how long the results of a preflight request can be cached.""", required=False, ) @@ -209,7 +207,6 @@ def cors_policy_directive( for_=Interface, layer=IDefaultBrowserLayer, ): - _handle_for(_context, for_) # Create a new policy class and store the CORS policy configuration in @@ -240,7 +237,7 @@ def cors_policy_directive( new_class, (for_, layer), ICORSPolicy, - u"", + "", _context.info, ), ) diff --git a/versions.cfg b/versions.cfg index e1a206c6..ed319548 100644 --- a/versions.cfg +++ b/versions.cfg @@ -14,6 +14,7 @@ Pygments = 2.5.1 plone.recipe.varnish = 1.3 # Code-analysis +black = 23.3.0 plone.recipe.codeanalysis = 3.0.1 coverage = 3.7.1 pep8 = 1.7.1 @@ -51,3 +52,4 @@ pyrsistent = 0.15.7 Click = 7.1.2 httpie = 1.0.3 check-manifest = 0.41 +pyparsing = 2.4.5 From 73a7712b967edd6b08b4f11ba2276471b8525124 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 26 Oct 2023 22:56:37 +0300 Subject: [PATCH 94/95] Run black --- src/plone/rest/traverse.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plone/rest/traverse.py b/src/plone/rest/traverse.py index 576eab21..2d3ed293 100644 --- a/src/plone/rest/traverse.py +++ b/src/plone/rest/traverse.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- from plone.rest.events import mark_as_api_request from plone.rest.interfaces import IAPIRequest + # from plone.rest.interfaces import IService from Products.CMFCore.interfaces import IContentish from Products.CMFCore.interfaces import ISiteRoot + # from Products.SiteAccess.VirtualHostMonster import VirtualHostMonster from zExceptions import Redirect from zope.component import adapter From 08eb5225be9186502ab8f553c200aa2d5cf3ecf1 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 27 Oct 2023 08:20:19 +0300 Subject: [PATCH 95/95] Remove duplicated lines in test --- src/plone/rest/tests/test_traversal.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/plone/rest/tests/test_traversal.py b/src/plone/rest/tests/test_traversal.py index b02b9fe6..7127dab2 100644 --- a/src/plone/rest/tests/test_traversal.py +++ b/src/plone/rest/tests/test_traversal.py @@ -134,30 +134,6 @@ def test_html_request_via_multiple_bad_apis_raises_not_found(self): with self.assertRaises(NotFound): self.traverse(path="/plone/++api++/search/++api++", accept="text/html") - def test_html_request_via_double_apis_raises_redirect(self): - portal_url = self.portal.absolute_url() - with self.assertRaises(Redirect) as exc: - self.traverse(path="/plone/++api++/++api++", accept="text/html") - self.assertEqual( - exc.exception.headers["Location"], - f"{portal_url}/++api++", - ) - - def test_html_request_via_multiple_apis_raises_redirect(self): - portal_url = self.portal.absolute_url() - with self.assertRaises(Redirect) as exc: - self.traverse( - path="/plone/++api++/++api++/++api++/search", accept="text/html" - ) - self.assertEqual( - exc.exception.headers["Location"], - f"{portal_url}/++api++/search", - ) - - def test_html_request_via_multiple_bad_apis_raises_not_found(self): - with self.assertRaises(NotFound): - self.traverse(path="/plone/++api++/search/++api++", accept="text/html") - def test_json_request_to_regular_view_returns_view(self): obj = self.traverse("/plone/folder_contents") self.assertTrue(IBrowserView.providedBy(obj), "IBrowserView expected")