diff --git a/last_commit.txt b/last_commit.txt index a1cf4519b5..35107b7ac8 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,179 +1,42 @@ -Repository: plone.rest +Repository: plone.app.content -Branch: refs/heads/main -Date: 2023-05-17T16:20:03+02:00 -Author: Roel Bruggink (jaroel) -Commit: https://github.com/plone/plone.rest/commit/77c8f06aaca015cc466d6da7c6c9e0323a561b1d +Branch: refs/heads/master +Date: 2023-09-22T22:47:48+02:00 +Author: Laurent Lasudry (laulaz) +Commit: https://github.com/plone/plone.app.content/commit/f68091eb8c77e2604e2862197d4538c04a07b569 -Dont publish items that are acquired. +Fix cut / delete for content with lock created by current user -This has been lifted from collective.explicitacquisition +This refs #266 Files changed: -A news/explicitacquisition.bugfix -A src/plone/rest/explicitacquisition.py -M base.cfg -M src/plone/rest/configure.zcml +A news/266.bugfix +M plone/app/content/browser/actions.py +M plone/app/content/browser/contents/cut.py +M plone/app/content/browser/contents/delete.py +M plone/app/content/tests/test_folder.py -b'diff --git a/base.cfg b/base.cfg\nindex 10182f5..49e5783 100644\n--- a/base.cfg\n+++ b/base.cfg\n@@ -63,3 +63,4 @@ eggs =\n plone.dexterity = git git://github.com/plone/plone.dexterity.git pushurl=git@github.com:plone/plone.dexterity.git branch=plip-680\n plone.rest = git git://github.com/plone/plone.rest.git pushurl=git@github.com:plone/plone.rest.git branch=master\n Products.CMFPlone = git git://github.com/plone/Products.CMFPlone.git pushurl=git@github.com:plone/Products.CMFPlone.git branch=4.3.x-plip-680\n+Products.CMFCore = git git://github.com/zopefoundation/Products.CMFCore.git branch=explicitacquisition\ndiff --git a/news/explicitacquisition.bugfix b/news/explicitacquisition.bugfix\nnew file mode 100644\nindex 0000000..37d8c63\n--- /dev/null\n+++ b/news/explicitacquisition.bugfix\n@@ -0,0 +1,2 @@\n+- Make REST endpoints check for acquired items.\n+ [jaroel]\ndiff --git a/src/plone/rest/configure.zcml b/src/plone/rest/configure.zcml\nindex f03a0e8..ecfbd1a 100644\n--- a/src/plone/rest/configure.zcml\n+++ b/src/plone/rest/configure.zcml\n@@ -26,4 +26,8 @@\n provides="zope.interface.Interface"\n />\n \n+ \n+\n \ndiff --git a/src/plone/rest/explicitacquisition.py b/src/plone/rest/explicitacquisition.py\nnew file mode 100644\nindex 0000000..b5b32f9\n--- /dev/null\n+++ b/src/plone/rest/explicitacquisition.py\n@@ -0,0 +1,8 @@\n+from zope.component import adapter\n+from Products.CMFCore.interfaces import IExplicitAcquisitionPublishingAllowed\n+from plone.rest.traverse import RESTWrapper\n+\n+\n+@adapter(RESTWrapper)\n+def rest_allowed(wrapper):\n+ return IExplicitAcquisitionPublishingAllowed(wrapper.context)\n' +b'diff --git a/news/266.bugfix b/news/266.bugfix\nnew file mode 100644\nindex 00000000..571aae7d\n--- /dev/null\n+++ b/news/266.bugfix\n@@ -0,0 +1 @@\n+Fix cut / delete for content with lock created by current user. [laulaz]\ndiff --git a/plone/app/content/browser/actions.py b/plone/app/content/browser/actions.py\nindex e760bf18..b939b341 100644\n--- a/plone/app/content/browser/actions.py\n+++ b/plone/app/content/browser/actions.py\n@@ -5,6 +5,7 @@\n from plone.base import PloneMessageFactory as _\n from plone.base.utils import get_user_friendly_types\n from plone.base.utils import safe_text\n+from plone.locking.interfaces import ILockable\n from Products.CMFCore.utils import getToolByName\n from Products.Five.browser import BrowserView\n from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile\n@@ -76,6 +77,14 @@ def handle_delete(self, action):\n # has the context object been acquired from a place it should not have\n # been?\n if self.context.aq_chain == self.context.aq_inner.aq_chain:\n+ try:\n+ lock_info = self.context.restrictedTraverse("@@plone_lock_info")\n+ except AttributeError:\n+ lock_info = None\n+ if lock_info is not None:\n+ if lock_info.is_locked() and not lock_info.is_locked_for_current_user():\n+ # unlock object as it is locked by current user\n+ ILockable(self.context).unlock()\n parent.manage_delObjects(self.context.getId())\n IStatusMessage(self.request).add(\n _("${title} has been deleted.", mapping={"title": title})\n@@ -219,7 +228,7 @@ def updateActions(self):\n self.actions["Cancel"].addClass("btn-secondary")\n \n \n-class ObjectCutView(LockingBase):\n+class ObjectCutView(BrowserView):\n @property\n def title(self):\n return self.context.Title()\n@@ -251,16 +260,24 @@ def do_redirect(self, url, message=None, message_type="info", raise_exception=No\n raise raise_exception\n \n def do_action(self):\n- if self.is_locked:\n- return self.do_redirect(\n- self.view_url,\n- _(\n- "${title} is locked and cannot be cut.",\n- mapping={\n- "title": self.title,\n- },\n- ),\n- )\n+ try:\n+ lock_info = self.context.restrictedTraverse("@@plone_lock_info")\n+ except AttributeError:\n+ lock_info = None\n+ if lock_info is not None:\n+ if lock_info.is_locked_for_current_user():\n+ return self.do_redirect(\n+ self.view_url,\n+ _(\n+ "${title} is locked and cannot be cut.",\n+ mapping={\n+ "title": self.title,\n+ },\n+ ),\n+ )\n+ elif lock_info.is_locked():\n+ # unlock object as it is locked by current user\n+ ILockable(self.context).unlock()\n \n try:\n cp = self.parent.manage_cutObjects(self.context.getId())\ndiff --git a/plone/app/content/browser/contents/cut.py b/plone/app/content/browser/contents/cut.py\nindex 1e87b455..e785931f 100644\n--- a/plone/app/content/browser/contents/cut.py\n+++ b/plone/app/content/browser/contents/cut.py\n@@ -4,6 +4,7 @@\n from plone.app.content.browser.contents import ContentsBaseAction\n from plone.app.content.interfaces import IStructureAction\n from plone.base import PloneMessageFactory as _\n+from plone.locking.interfaces import ILockable\n from zope.i18n import translate\n from zope.interface import implementer\n \n@@ -36,14 +37,23 @@ def action(self, obj):\n def finish(self):\n oblist = []\n for ob in self.oblist:\n- if ob.wl_isLocked():\n- self.errors.append(\n- _(\n- "${title} is being edited and cannot be cut.",\n- mapping={"title": self.objectTitle(ob)},\n+ try:\n+ lock_info = ob.restrictedTraverse("@@plone_lock_info")\n+ except AttributeError:\n+ lock_info = None\n+ if lock_info is not None:\n+ if lock_info.is_locked_for_current_user():\n+ self.errors.append(\n+ _(\n+ "${title} is being edited and cannot be cut.",\n+ mapping={"title": self.objectTitle(ob)},\n+ )\n )\n- )\n- continue\n+ continue\n+ elif lock_info.is_locked():\n+ # unlock object as it is locked by current user\n+ ILockable(ob).unlock()\n+\n if not ob.cb_isMoveable():\n self.errors.append(\n _(\ndiff --git a/plone/app/content/browser/contents/delete.py b/plone/app/content/browser/contents/delete.py\nindex 658e665d..5adac14a 100644\n--- a/plone/app/content/browser/contents/delete.py\n+++ b/plone/app/content/browser/contents/delete.py\n@@ -3,6 +3,7 @@\n from plone.app.content.browser.contents import ContentsBaseAction\n from plone.app.content.interfaces import IStructureAction\n from plone.base import PloneMessageFactory as _\n+from plone.locking.interfaces import ILockable\n from Products.CMFCore.utils import getToolByName\n from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile\n from zope.component import getMultiAdapter\n@@ -70,19 +71,25 @@ def action(self, obj):\n lock_info = obj.restrictedTraverse("@@plone_lock_info")\n except AttributeError:\n lock_info = None\n-\n- if lock_info is not None and lock_info.is_locked():\n- self.errors.append(\n- _("${title} is locked and cannot be deleted.", mapping={"title": title})\n- )\n- return\n- else:\n- try:\n- parent.manage_delObjects(obj.getId())\n- except Unauthorized:\n+ if lock_info is not None:\n+ if lock_info.is_locked_for_current_user():\n self.errors.append(\n _(\n- "You are not authorized to delete ${title}.",\n- mapping={"title": self.objectTitle(self.dest)},\n+ "${title} is locked and cannot be deleted.",\n+ mapping={"title": title},\n )\n )\n+ return\n+ elif lock_info.is_locked():\n+ # unlock object as it is locked by current user\n+ ILockable(obj).unlock()\n+\n+ try:\n+ parent.manage_delObjects(obj.getId())\n+ except Unauthorized:\n+ self.errors.append(\n+ _(\n+ "You are not authorized to delete ${title}.",\n+ mapping={"title": self.objectTitle(self.dest)},\n+ )\n+ )\ndiff --git a/plone/app/content/tests/test_folder.py b/plone/app/content/tests/test_folder.py\nindex c0e7a3fd..01d2764f 100644\n--- a/plone/app/content/tests/test_folder.py\n+++ b/plone/app/content/tests/test_folder.py\n@@ -2,9 +2,11 @@\n from plone.app.content.testing import PLONE_APP_CONTENT_DX_FUNCTIONAL_TESTING\n from plone.app.content.testing import PLONE_APP_CONTENT_DX_INTEGRATION_TESTING\n from plone.app.testing import login\n+from plone.app.testing import logout\n from plone.app.testing import setRoles\n from plone.app.testing import TEST_USER_ID\n from plone.app.testing import TEST_USER_NAME\n+from plone.app.testing import TEST_USER_PASSWORD\n from plone.dexterity.fti import DexterityFTI\n from plone.locking.interfaces import IRefreshableLockable\n from plone.protect.authenticator import createToken\n@@ -224,6 +226,10 @@ def setUp(self):\n login(self.portal, TEST_USER_NAME)\n setRoles(self.portal, TEST_USER_ID, ["Manager"])\n \n+ self.portal.acl_users.userFolderAddUser(\n+ "editor", TEST_USER_PASSWORD, ["Editor"], []\n+ )\n+\n self.portal.invokeFactory("Document", id="page", title="page")\n self.portal.page.reindexObject()\n \n@@ -237,14 +243,33 @@ def setUp(self):\n }\n self.request.REQUEST_METHOD = "POST"\n \n- def test_cut_object_when_locked(self):\n+ def test_cut_object_when_locked_by_current_user(self):\n from plone.app.content.browser.contents.cut import CutActionView\n \n+ plone_lock_info = self.portal.page.restrictedTraverse("@@plone_lock_info")\n lockable = IRefreshableLockable(self.portal.page)\n lockable.lock()\n+ self.assertTrue(plone_lock_info.is_locked())\n+ view = CutActionView(self.portal, self.request)\n+ view()\n+ self.assertEqual(len(view.errors), 0)\n+ self.assertFalse(plone_lock_info.is_locked())\n+\n+ def test_cut_object_when_locked_by_other_user(self):\n+ from plone.app.content.browser.contents.cut import CutActionView\n+\n+ plone_lock_info = self.portal.page.restrictedTraverse("@@plone_lock_info")\n+ lockable = IRefreshableLockable(self.portal.page)\n+ lockable.lock()\n+ logout()\n+\n+ login(self.portal, "editor")\n+ self.assertTrue(plone_lock_info.is_locked())\n+ self.request.form["_authenticator"] = createToken()\n view = CutActionView(self.portal, self.request)\n view()\n self.assertEqual(len(view.errors), 1)\n+ self.assertTrue(plone_lock_info.is_locked())\n \n \n class DeleteDXTest(BaseTest):\n@@ -257,6 +282,10 @@ def setUp(self):\n login(self.portal, TEST_USER_NAME)\n setRoles(self.portal, TEST_USER_ID, ["Manager"])\n \n+ self.portal.acl_users.userFolderAddUser(\n+ "editor", TEST_USER_PASSWORD, ["Editor"], []\n+ )\n+\n self.portal.invokeFactory("Document", id="page", title="page")\n self.portal.page.reindexObject()\n \n@@ -285,14 +314,32 @@ def test_delete_object(self):\n view()\n self.assertTrue(page_id not in self.portal)\n \n- def test_delete_object_when_locked(self):\n+ def test_delete_object_when_locked_by_current_user(self):\n from plone.app.content.browser.contents.delete import DeleteActionView\n \n+ page_id = self.portal.page.id\n lockable = IRefreshableLockable(self.portal.page)\n lockable.lock()\n view = DeleteActionView(self.portal, self.request)\n view()\n+ self.assertEqual(len(view.errors), 0)\n+ self.assertTrue(page_id not in self.portal)\n+\n+ def test_delete_object_when_locked_by_other_user(self):\n+ from plone.app.content.browser.contents.delete import DeleteActionView\n+\n+ plone_lock_info = self.portal.page.restrictedTraverse("@@plone_lock_info")\n+ lockable = IRefreshableLockable(self.portal.page)\n+ lockable.lock()\n+ logout()\n+\n+ login(self.portal, "editor")\n+ self.assertTrue(plone_lock_info.is_locked())\n+ self.request.form["_authenticator"] = createToken()\n+ view = DeleteActionView(self.portal, self.request)\n+ view()\n self.assertEqual(len(view.errors), 1)\n+ self.assertTrue(plone_lock_info.is_locked())\n \n def test_delete_wrong_object_by_acquisition(self):\n page_id = self.portal.page.id\n' -Repository: plone.rest +Repository: plone.app.content -Branch: refs/heads/main -Date: 2023-05-18T11:23:19+02:00 -Author: Roel Bruggink (jaroel) -Commit: https://github.com/plone/plone.rest/commit/cbe58a3cc2da4894e8657c637f998ac4c5606cde +Branch: refs/heads/master +Date: 2023-10-13T17:07:04-07:00 +Author: David Glick (davisagli) +Commit: https://github.com/plone/plone.app.content/commit/fcd73064346ed0ab0b4e9269a0a27d056009ace1 -Renamed IExplicitAcquisitionPublishingAllowed to IShouldAllowAcquiredItemPublication +Merge pull request #267 from plone/laulaz-issue-266-cut-delete-with-lock -Files changed: -M src/plone/rest/configure.zcml -M src/plone/rest/explicitacquisition.py - -b'diff --git a/src/plone/rest/configure.zcml b/src/plone/rest/configure.zcml\nindex ecfbd1a..6d74537 100644\n--- a/src/plone/rest/configure.zcml\n+++ b/src/plone/rest/configure.zcml\n@@ -27,7 +27,7 @@\n />\n \n \n \n \ndiff --git a/src/plone/rest/explicitacquisition.py b/src/plone/rest/explicitacquisition.py\nindex b5b32f9..95ce6e3 100644\n--- a/src/plone/rest/explicitacquisition.py\n+++ b/src/plone/rest/explicitacquisition.py\n@@ -1,8 +1,8 @@\n from zope.component import adapter\n-from Products.CMFCore.interfaces import IExplicitAcquisitionPublishingAllowed\n+from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication\n from plone.rest.traverse import RESTWrapper\n \n \n @adapter(RESTWrapper)\n def rest_allowed(wrapper):\n- return IExplicitAcquisitionPublishingAllowed(wrapper.context)\n+ return IShouldAllowAcquiredItemPublication(wrapper.context)\n' - -Repository: plone.rest - - -Branch: refs/heads/main -Date: 2023-05-18T17:06:20+02:00 -Author: Roel Bruggink (jaroel) -Commit: https://github.com/plone/plone.rest/commit/7f71de123ebc2106a0bae94053aa488347f2ed7a - -Add some tests - -Files changed: -A src/plone/rest/tests/test_explicitacquisition.py - -b'diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py\nnew file mode 100644\nindex 0000000..977f8a5\n--- /dev/null\n+++ b/src/plone/rest/tests/test_explicitacquisition.py\n@@ -0,0 +1,49 @@\n+import unittest\n+from base64 import b64encode\n+\n+from plone.app.testing import (\n+ SITE_OWNER_NAME,\n+ SITE_OWNER_PASSWORD,\n+ TEST_USER_ID,\n+ setRoles,\n+)\n+from zExceptions import NotFound\n+from zope.event import notify\n+from ZPublisher.pubevents import PubAfterTraversal, PubStart\n+\n+from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING\n+\n+\n+class TestExplicitAcquisition(unittest.TestCase):\n+ layer = PLONE_REST_INTEGRATION_TESTING\n+\n+ def setUp(self):\n+ self.portal = self.layer["portal"]\n+ self.request = self.layer["request"]\n+ setRoles(self.portal, TEST_USER_ID, ["Manager"])\n+ self.portal.invokeFactory("Document", id="foo")\n+\n+ def traverse(self, path="/plone", accept="application/json", method="GET"):\n+ request = self.layer["request"]\n+ request.environ["PATH_INFO"] = path\n+ request.environ["PATH_TRANSLATED"] = path\n+ request.environ["HTTP_ACCEPT"] = accept\n+ request.environ["REQUEST_METHOD"] = method\n+ auth = "%s:%s" % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)\n+ b64auth = b64encode(auth.encode("utf8"))\n+ request._auth = "Basic %s" % b64auth.decode("utf8")\n+ notify(PubStart(request))\n+ return request.traverse(path)\n+\n+ def test_portal_root(self):\n+ self.traverse("/plone")\n+ notify(PubAfterTraversal(self.request))\n+\n+ def test_portal_foo(self):\n+ self.traverse("/plone/foo")\n+ notify(PubAfterTraversal(self.request))\n+\n+ def test_portal_foo_acquired(self):\n+ self.traverse("/plone/foo/foo")\n+ with self.assertRaises(NotFound):\n+ notify(PubAfterTraversal(self.request))\n' - -Repository: plone.rest - - -Branch: refs/heads/main -Date: 2023-09-22T15:51:54+02:00 -Author: Roel Bruggink (jaroel) -Commit: https://github.com/plone/plone.rest/commit/4ead81e896e7771688e315abaaeea9979b8f93db - -Merge branch 'main' into explicitacquisition - -Files changed: -A news/141.breaking -M .github/workflows/tests.yml -M CHANGES.rst -M README.rst -M base.cfg -M plone-6.0.x.cfg -M setup.py -M src/plone/rest/tests/test_traversal.py -M src/plone/rest/traverse.py - -b'diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml\nindex ad015c6..5fd38da 100644\n--- a/.github/workflows/tests.yml\n+++ b/.github/workflows/tests.yml\n@@ -1,16 +1,14 @@\n-name: plone.rest CI\n+name: Tests\n on: [push]\n jobs:\n build:\n- runs-on: "ubuntu-20.04" # needed to keep Python 2.7\n+ runs-on: ubuntu-latest\n strategy:\n fail-fast: false\n matrix:\n- python-version: ["3.11", "3.10", "3.9", "3.8", "2.7"]\n+ python-version: ["3.11", "3.10", "3.9", "3.8"]\n plone-version: ["6.0", "5.2"]\n exclude:\n- - python-version: 2.7\n- plone-version: 6.0\n - python-version: 3.11\n plone-version: 5.2\n - python-version: 3.10\ndiff --git a/CHANGES.rst b/CHANGES.rst\nindex 81d2240..971ed44 100644\n--- a/CHANGES.rst\n+++ b/CHANGES.rst\n@@ -8,6 +8,19 @@ Changelog\n \n .. towncrier release notes start\n \n+3.0.1 (2023-09-21)\n+------------------\n+\n+Bug fixes:\n+\n+\n+- When ``++api++`` is in the url multiple times, redirect to the proper url.\n+ When the url is badly formed, for example ``++api++/something/++api++``, give a 404 NotFound.\n+ Fixes a denial of service.\n+ See `security advisory `_.\n+ [maurits] (#1)\n+\n+\n 3.0.0 (2023-01-29)\n ------------------\n \ndiff --git a/README.rst b/README.rst\nindex 8578eb9..a47e4af 100644\n--- a/README.rst\n+++ b/README.rst\n@@ -1,6 +1,6 @@\n-.. image:: https://github.com/plone/plone.rest/workflows/plone.rest%20CI/badge.svg\n+.. image:: https://github.com/plone/plone.rest/actions/workflows/tests.yml/badge.svg\n :alt: Github Actions Status\n- :target: https://github.com/plone/plone.rest/actions?query=workflow%3A%22plone.rest+CI%22\n+ :target: https://github.com/plone/plone.rest/actions/workflows/tests.yml\n \n .. image:: https://img.shields.io/coveralls/github/plone/plone.rest.svg\n :alt: Coveralls github\ndiff --git a/base.cfg b/base.cfg\nindex 49e5783..69bdf16 100644\n--- a/base.cfg\n+++ b/base.cfg\n@@ -61,6 +61,6 @@ eggs =\n \n [sources]\n plone.dexterity = git git://github.com/plone/plone.dexterity.git pushurl=git@github.com:plone/plone.dexterity.git branch=plip-680\n-plone.rest = git git://github.com/plone/plone.rest.git pushurl=git@github.com:plone/plone.rest.git branch=master\n+plone.rest = git git://github.com/plone/plone.rest.git pushurl=git@github.com:plone/plone.rest.git branch=main\n Products.CMFPlone = git git://github.com/plone/Products.CMFPlone.git pushurl=git@github.com:plone/Products.CMFPlone.git branch=4.3.x-plip-680\n Products.CMFCore = git git://github.com/zopefoundation/Products.CMFCore.git branch=explicitacquisition\ndiff --git a/news/141.breaking b/news/141.breaking\nnew file mode 100644\nindex 0000000..e86d4ff\n--- /dev/null\n+++ b/news/141.breaking\n@@ -0,0 +1 @@\n+Drop support for Python 2.7, 3.6 and 3.7 @tisto\n\\ No newline at end of file\ndiff --git a/plone-6.0.x.cfg b/plone-6.0.x.cfg\nindex 07cbb26..0ec46f1 100644\n--- a/plone-6.0.x.cfg\n+++ b/plone-6.0.x.cfg\n@@ -1,6 +1,6 @@\n [buildout]\n extends =\n- https://dist.plone.org/release/6.0.0/versions.cfg\n+ https://dist.plone.org/release/6.0.7/versions.cfg\n base.cfg\n \n [instance]\ndiff --git a/setup.py b/setup.py\nindex 20a32dd..76adb4c 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -7,7 +7,7 @@ def read(*rnames):\n return open(os.path.join(os.path.dirname(__file__), *rnames)).read()\n \n \n-version = "3.0.1.dev0"\n+version = "3.0.2.dev0"\n \n long_description = read("README.rst") + "\\n\\n" + read("CHANGES.rst") + "\\n\\n"\n \n@@ -31,13 +31,12 @@ def read(*rnames):\n "License :: OSI Approved :: GNU General Public License (GPL)",\n "Operating System :: OS Independent",\n "Programming Language :: Python",\n- "Programming Language :: Python :: 2.7",\n- "Programming Language :: Python :: 3.6",\n- "Programming Language :: Python :: 3.7",\n "Programming Language :: Python :: 3.8",\n "Programming Language :: Python :: 3.9",\n "Programming Language :: Python :: 3.10",\n "Programming Language :: Python :: 3.11",\n+ "Programming Language :: Python :: 3 :: Only",\n+ "Topic :: Software Development :: Libraries :: Python Modules",\n ],\n keywords="rest http",\n author="Plone Foundation",\n@@ -49,7 +48,7 @@ def read(*rnames):\n namespace_packages=["plone"],\n include_package_data=True,\n zip_safe=False,\n- python_requires=">=2.7",\n+ python_requires=">=3.8",\n extras_require=dict(\n test=[\n "plone.app.testing[robot]>=4.2.2",\ndiff --git a/src/plone/rest/tests/test_traversal.py b/src/plone/rest/tests/test_traversal.py\nindex 963d1c4..5d5389d 100644\n--- a/src/plone/rest/tests/test_traversal.py\n+++ b/src/plone/rest/tests/test_traversal.py\n@@ -10,6 +10,8 @@\n from plone.app.testing import TEST_USER_ID\n from plone.rest.service import Service\n from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING\n+from zExceptions import NotFound\n+from zExceptions import Redirect\n from zope.event import notify\n from zope.interface import alsoProvides\n from zope.publisher.interfaces.browser import IBrowserView\n@@ -106,6 +108,34 @@ def test_html_request_on_existing_view_returns_view(self):\n obj = self.traverse(path="/plone/folder1/search", accept="text/html")\n self.assertFalse(isinstance(obj, Service), "Got a service")\n \n+ def test_html_request_via_api_returns_service(self):\n+ obj = self.traverse(path="/plone/++api++", accept="text/html")\n+ self.assertTrue(isinstance(obj, Service), "Not a service")\n+\n+ def test_html_request_via_double_apis_raises_redirect(self):\n+ portal_url = self.portal.absolute_url()\n+ with self.assertRaises(Redirect) as exc:\n+ self.traverse(path="/plone/++api++/++api++", accept="text/html")\n+ self.assertEqual(\n+ exc.exception.headers["Location"],\n+ f"{portal_url}/++api++",\n+ )\n+\n+ def test_html_request_via_multiple_apis_raises_redirect(self):\n+ portal_url = self.portal.absolute_url()\n+ with self.assertRaises(Redirect) as exc:\n+ self.traverse(\n+ path="/plone/++api++/++api++/++api++/search", accept="text/html"\n+ )\n+ self.assertEqual(\n+ exc.exception.headers["Location"],\n+ f"{portal_url}/++api++/search",\n+ )\n+\n+ def test_html_request_via_multiple_bad_apis_raises_not_found(self):\n+ with self.assertRaises(NotFound):\n+ self.traverse(path="/plone/++api++/search/++api++", accept="text/html")\n+\n def test_virtual_hosting(self):\n app = self.layer["app"]\n vhm = VirtualHostMonster()\ndiff --git a/src/plone/rest/traverse.py b/src/plone/rest/traverse.py\nindex f8d4a23..0a151c8 100644\n--- a/src/plone/rest/traverse.py\n+++ b/src/plone/rest/traverse.py\n@@ -5,6 +5,7 @@\n from plone.rest.interfaces import IAPIRequest\n from plone.rest.interfaces import IService\n from plone.rest.events import mark_as_api_request\n+from zExceptions import Redirect\n from zope.component import adapter\n from zope.component import queryMultiAdapter\n from zope.interface import implementer\n@@ -64,6 +65,18 @@ def __init__(self, context, request):\n self.request = request\n \n def traverse(self, name_ignored, subpath_ignored):\n+ name = "/++api++"\n+ url = self.request.ACTUAL_URL\n+ if url.count(name) > 1:\n+ # Redirect to proper url.\n+ while f"{name}{name}" in url:\n+ url = url.replace(f"{name}{name}", name)\n+ if url.count(name) > 1:\n+ # Something like: .../++api++/something/++api++\n+ # Return nothing, so a NotFound is raised.\n+ return\n+ # Raise a redirect exception to stop execution of the current request.\n+ raise Redirect(url)\n mark_as_api_request(self.request, "application/json")\n return self.context\n \n' - -Repository: plone.rest - - -Branch: refs/heads/main -Date: 2023-09-22T15:54:02+02:00 -Author: Roel Bruggink (jaroel) -Commit: https://github.com/plone/plone.rest/commit/576f0002af04a831fb7aca035efd30addd2950e5 - -Use released Products.CMFCore - -Files changed: -M base.cfg -M setup.py - -b'diff --git a/base.cfg b/base.cfg\nindex 69bdf16..51763bc 100644\n--- a/base.cfg\n+++ b/base.cfg\n@@ -63,4 +63,3 @@ eggs =\n plone.dexterity = git git://github.com/plone/plone.dexterity.git pushurl=git@github.com:plone/plone.dexterity.git branch=plip-680\n plone.rest = git git://github.com/plone/plone.rest.git pushurl=git@github.com:plone/plone.rest.git branch=main\n Products.CMFPlone = git git://github.com/plone/Products.CMFPlone.git pushurl=git@github.com:plone/Products.CMFPlone.git branch=4.3.x-plip-680\n-Products.CMFCore = git git://github.com/zopefoundation/Products.CMFCore.git branch=explicitacquisition\ndiff --git a/setup.py b/setup.py\nindex 76adb4c..acaba84 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -54,7 +54,7 @@ def read(*rnames):\n "plone.app.testing[robot]>=4.2.2",\n "plone.app.robotframework",\n "plone.dexterity",\n- "Products.CMFCore",\n+ "Products.CMFCore>=3.1",\n "requests",\n ]\n ),\n@@ -65,7 +65,7 @@ def read(*rnames):\n "zope.interface",\n "zope.publisher",\n "zope.traversing",\n- "Products.CMFCore",\n+ "Products.CMFCore>=3.1",\n "Zope2",\n "six",\n ],\n' - -Repository: plone.rest - - -Branch: refs/heads/main -Date: 2023-10-09T14:00:03+02:00 -Author: Roel Bruggink (jaroel) -Commit: https://github.com/plone/plone.rest/commit/46fcb23e3f95fe4a79bdd319b06c39e840fcaac9 - -Support IShouldAllowAcquiredItemPublication without requiring a newer CMFCore directly. - -Files changed: -M setup.py -M src/plone/rest/configure.zcml -M src/plone/rest/explicitacquisition.py -M src/plone/rest/interfaces.py - -b'diff --git a/setup.py b/setup.py\nindex acaba84..76adb4c 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -54,7 +54,7 @@ def read(*rnames):\n "plone.app.testing[robot]>=4.2.2",\n "plone.app.robotframework",\n "plone.dexterity",\n- "Products.CMFCore>=3.1",\n+ "Products.CMFCore",\n "requests",\n ]\n ),\n@@ -65,7 +65,7 @@ def read(*rnames):\n "zope.interface",\n "zope.publisher",\n "zope.traversing",\n- "Products.CMFCore>=3.1",\n+ "Products.CMFCore",\n "Zope2",\n "six",\n ],\ndiff --git a/src/plone/rest/configure.zcml b/src/plone/rest/configure.zcml\nindex 6d74537..af6c115 100644\n--- a/src/plone/rest/configure.zcml\n+++ b/src/plone/rest/configure.zcml\n@@ -27,7 +27,7 @@\n />\n \n \n \n \ndiff --git a/src/plone/rest/explicitacquisition.py b/src/plone/rest/explicitacquisition.py\nindex 95ce6e3..8e3bac5 100644\n--- a/src/plone/rest/explicitacquisition.py\n+++ b/src/plone/rest/explicitacquisition.py\n@@ -1,5 +1,5 @@\n from zope.component import adapter\n-from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication\n+from plone.rest.interfaces import IShouldAllowAcquiredItemPublication\n from plone.rest.traverse import RESTWrapper\n \n \ndiff --git a/src/plone/rest/interfaces.py b/src/plone/rest/interfaces.py\nindex ac18224..9ba6d96 100644\n--- a/src/plone/rest/interfaces.py\n+++ b/src/plone/rest/interfaces.py\n@@ -20,3 +20,11 @@ def process_simple_request():\n \n def process_preflight_request():\n """Process a preflight request"""\n+\n+\n+try:\n+ from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication\n+except ImportError:\n+\n+ class IShouldAllowAcquiredItemPublication(Interface):\n+ pass\n' - -Repository: plone.rest - - -Branch: refs/heads/main -Date: 2023-10-09T14:11:48+02:00 -Author: Roel Bruggink (jaroel) -Commit: https://github.com/plone/plone.rest/commit/619577c7b81eab1e75e4c6e516e79164f9be1882 - -Test IShouldAllowAcquiredItemPublication conditionally - -Files changed: -M src/plone/rest/tests/test_explicitacquisition.py - -b'diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py\nindex 977f8a5..65735fe 100644\n--- a/src/plone/rest/tests/test_explicitacquisition.py\n+++ b/src/plone/rest/tests/test_explicitacquisition.py\n@@ -13,7 +13,16 @@\n \n from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING\n \n+try:\n+ from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication\n+except ImportError:\n+ IShouldAllowAcquiredItemPublication = None\n \n+\n+@unittest.skipIf(\n+ IShouldAllowAcquiredItemPublication is None,\n+ "Older Plone versions don\'t have CMFCore>=3.2",\n+)\n class TestExplicitAcquisition(unittest.TestCase):\n layer = PLONE_REST_INTEGRATION_TESTING\n \n' - -Repository: plone.rest - - -Branch: refs/heads/main -Date: 2023-10-09T12:16:42Z -Author: pre-commit-ci[bot] (pre-commit-ci[bot]) <66853113+pre-commit-ci[bot]@users.noreply.github.com> -Commit: https://github.com/plone/plone.rest/commit/fa9f2d285d3e91f16bf7c4731076b8069c2a6e2b - -[pre-commit.ci] auto fixes from pre-commit.com hooks - -for more information, see https://pre-commit.ci - -Files changed: -M src/plone/rest/explicitacquisition.py -M src/plone/rest/tests/test_explicitacquisition.py - -b'diff --git a/src/plone/rest/explicitacquisition.py b/src/plone/rest/explicitacquisition.py\nindex 8e3bac5..8c8b496 100644\n--- a/src/plone/rest/explicitacquisition.py\n+++ b/src/plone/rest/explicitacquisition.py\n@@ -1,6 +1,6 @@\n-from zope.component import adapter\n from plone.rest.interfaces import IShouldAllowAcquiredItemPublication\n from plone.rest.traverse import RESTWrapper\n+from zope.component import adapter\n \n \n @adapter(RESTWrapper)\ndiff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py\nindex 65735fe..fbd717b 100644\n--- a/src/plone/rest/tests/test_explicitacquisition.py\n+++ b/src/plone/rest/tests/test_explicitacquisition.py\n@@ -1,17 +1,16 @@\n-import unittest\n from base64 import b64encode\n-\n-from plone.app.testing import (\n- SITE_OWNER_NAME,\n- SITE_OWNER_PASSWORD,\n- TEST_USER_ID,\n- setRoles,\n-)\n+from plone.app.testing import setRoles\n+from plone.app.testing import SITE_OWNER_NAME\n+from plone.app.testing import SITE_OWNER_PASSWORD\n+from plone.app.testing import TEST_USER_ID\n+from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING\n from zExceptions import NotFound\n from zope.event import notify\n-from ZPublisher.pubevents import PubAfterTraversal, PubStart\n+from ZPublisher.pubevents import PubAfterTraversal\n+from ZPublisher.pubevents import PubStart\n+\n+import unittest\n \n-from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING\n \n try:\n from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication\n@@ -38,7 +37,7 @@ def traverse(self, path="/plone", accept="application/json", method="GET"):\n request.environ["PATH_TRANSLATED"] = path\n request.environ["HTTP_ACCEPT"] = accept\n request.environ["REQUEST_METHOD"] = method\n- auth = "%s:%s" % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)\n+ auth = "{}:{}".format(SITE_OWNER_NAME, SITE_OWNER_PASSWORD)\n b64auth = b64encode(auth.encode("utf8"))\n request._auth = "Basic %s" % b64auth.decode("utf8")\n notify(PubStart(request))\n' - -Repository: plone.rest - - -Branch: refs/heads/main -Date: 2023-10-09T12:37:44Z -Author: pre-commit-ci[bot] (pre-commit-ci[bot]) <66853113+pre-commit-ci[bot]@users.noreply.github.com> -Commit: https://github.com/plone/plone.rest/commit/054b0234ae9597544c0485c32ac3d9f38086e844 - -[pre-commit.ci] auto fixes from pre-commit.com hooks - -for more information, see https://pre-commit.ci - -Files changed: -M src/plone/rest/tests/test_explicitacquisition.py - -b'diff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py\nindex fbd717b..d0af009 100644\n--- a/src/plone/rest/tests/test_explicitacquisition.py\n+++ b/src/plone/rest/tests/test_explicitacquisition.py\n@@ -37,7 +37,7 @@ def traverse(self, path="/plone", accept="application/json", method="GET"):\n request.environ["PATH_TRANSLATED"] = path\n request.environ["HTTP_ACCEPT"] = accept\n request.environ["REQUEST_METHOD"] = method\n- auth = "{}:{}".format(SITE_OWNER_NAME, SITE_OWNER_PASSWORD)\n+ auth = f"{SITE_OWNER_NAME}:{SITE_OWNER_PASSWORD}"\n b64auth = b64encode(auth.encode("utf8"))\n request._auth = "Basic %s" % b64auth.decode("utf8")\n notify(PubStart(request))\n' - -Repository: plone.rest - - -Branch: refs/heads/main -Date: 2023-10-10T23:41:33+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.rest/commit/809346bd08451bb56a7d4fd29bc80634218769c9 - -Merge pull request #151 from plone/explicitacquisition - -Dont publish items that are acquired. +Fix cut / delete for content with lock created by current user Files changed: -A news/explicitacquisition.bugfix -A src/plone/rest/explicitacquisition.py -A src/plone/rest/tests/test_explicitacquisition.py -M src/plone/rest/configure.zcml -M src/plone/rest/interfaces.py +A news/266.bugfix +M plone/app/content/browser/actions.py +M plone/app/content/browser/contents/cut.py +M plone/app/content/browser/contents/delete.py +M plone/app/content/tests/test_folder.py -b'diff --git a/news/explicitacquisition.bugfix b/news/explicitacquisition.bugfix\nnew file mode 100644\nindex 0000000..37d8c63\n--- /dev/null\n+++ b/news/explicitacquisition.bugfix\n@@ -0,0 +1,2 @@\n+- Make REST endpoints check for acquired items.\n+ [jaroel]\ndiff --git a/src/plone/rest/configure.zcml b/src/plone/rest/configure.zcml\nindex f03a0e8..af6c115 100644\n--- a/src/plone/rest/configure.zcml\n+++ b/src/plone/rest/configure.zcml\n@@ -26,4 +26,8 @@\n provides="zope.interface.Interface"\n />\n \n+ \n+\n \ndiff --git a/src/plone/rest/explicitacquisition.py b/src/plone/rest/explicitacquisition.py\nnew file mode 100644\nindex 0000000..8c8b496\n--- /dev/null\n+++ b/src/plone/rest/explicitacquisition.py\n@@ -0,0 +1,8 @@\n+from plone.rest.interfaces import IShouldAllowAcquiredItemPublication\n+from plone.rest.traverse import RESTWrapper\n+from zope.component import adapter\n+\n+\n+@adapter(RESTWrapper)\n+def rest_allowed(wrapper):\n+ return IShouldAllowAcquiredItemPublication(wrapper.context)\ndiff --git a/src/plone/rest/interfaces.py b/src/plone/rest/interfaces.py\nindex bf07258..a31868d 100644\n--- a/src/plone/rest/interfaces.py\n+++ b/src/plone/rest/interfaces.py\n@@ -19,3 +19,11 @@ def process_simple_request():\n \n def process_preflight_request():\n """Process a preflight request"""\n+\n+\n+try:\n+ from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication\n+except ImportError:\n+\n+ class IShouldAllowAcquiredItemPublication(Interface):\n+ pass\ndiff --git a/src/plone/rest/tests/test_explicitacquisition.py b/src/plone/rest/tests/test_explicitacquisition.py\nnew file mode 100644\nindex 0000000..d0af009\n--- /dev/null\n+++ b/src/plone/rest/tests/test_explicitacquisition.py\n@@ -0,0 +1,57 @@\n+from base64 import b64encode\n+from plone.app.testing import setRoles\n+from plone.app.testing import SITE_OWNER_NAME\n+from plone.app.testing import SITE_OWNER_PASSWORD\n+from plone.app.testing import TEST_USER_ID\n+from plone.rest.testing import PLONE_REST_INTEGRATION_TESTING\n+from zExceptions import NotFound\n+from zope.event import notify\n+from ZPublisher.pubevents import PubAfterTraversal\n+from ZPublisher.pubevents import PubStart\n+\n+import unittest\n+\n+\n+try:\n+ from Products.CMFCore.interfaces import IShouldAllowAcquiredItemPublication\n+except ImportError:\n+ IShouldAllowAcquiredItemPublication = None\n+\n+\n+@unittest.skipIf(\n+ IShouldAllowAcquiredItemPublication is None,\n+ "Older Plone versions don\'t have CMFCore>=3.2",\n+)\n+class TestExplicitAcquisition(unittest.TestCase):\n+ layer = PLONE_REST_INTEGRATION_TESTING\n+\n+ def setUp(self):\n+ self.portal = self.layer["portal"]\n+ self.request = self.layer["request"]\n+ setRoles(self.portal, TEST_USER_ID, ["Manager"])\n+ self.portal.invokeFactory("Document", id="foo")\n+\n+ def traverse(self, path="/plone", accept="application/json", method="GET"):\n+ request = self.layer["request"]\n+ request.environ["PATH_INFO"] = path\n+ request.environ["PATH_TRANSLATED"] = path\n+ request.environ["HTTP_ACCEPT"] = accept\n+ request.environ["REQUEST_METHOD"] = method\n+ auth = f"{SITE_OWNER_NAME}:{SITE_OWNER_PASSWORD}"\n+ b64auth = b64encode(auth.encode("utf8"))\n+ request._auth = "Basic %s" % b64auth.decode("utf8")\n+ notify(PubStart(request))\n+ return request.traverse(path)\n+\n+ def test_portal_root(self):\n+ self.traverse("/plone")\n+ notify(PubAfterTraversal(self.request))\n+\n+ def test_portal_foo(self):\n+ self.traverse("/plone/foo")\n+ notify(PubAfterTraversal(self.request))\n+\n+ def test_portal_foo_acquired(self):\n+ self.traverse("/plone/foo/foo")\n+ with self.assertRaises(NotFound):\n+ notify(PubAfterTraversal(self.request))\n' +b'diff --git a/news/266.bugfix b/news/266.bugfix\nnew file mode 100644\nindex 0000000..571aae7\n--- /dev/null\n+++ b/news/266.bugfix\n@@ -0,0 +1 @@\n+Fix cut / delete for content with lock created by current user. [laulaz]\ndiff --git a/plone/app/content/browser/actions.py b/plone/app/content/browser/actions.py\nindex e760bf1..b939b34 100644\n--- a/plone/app/content/browser/actions.py\n+++ b/plone/app/content/browser/actions.py\n@@ -5,6 +5,7 @@\n from plone.base import PloneMessageFactory as _\n from plone.base.utils import get_user_friendly_types\n from plone.base.utils import safe_text\n+from plone.locking.interfaces import ILockable\n from Products.CMFCore.utils import getToolByName\n from Products.Five.browser import BrowserView\n from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile\n@@ -76,6 +77,14 @@ def handle_delete(self, action):\n # has the context object been acquired from a place it should not have\n # been?\n if self.context.aq_chain == self.context.aq_inner.aq_chain:\n+ try:\n+ lock_info = self.context.restrictedTraverse("@@plone_lock_info")\n+ except AttributeError:\n+ lock_info = None\n+ if lock_info is not None:\n+ if lock_info.is_locked() and not lock_info.is_locked_for_current_user():\n+ # unlock object as it is locked by current user\n+ ILockable(self.context).unlock()\n parent.manage_delObjects(self.context.getId())\n IStatusMessage(self.request).add(\n _("${title} has been deleted.", mapping={"title": title})\n@@ -219,7 +228,7 @@ def updateActions(self):\n self.actions["Cancel"].addClass("btn-secondary")\n \n \n-class ObjectCutView(LockingBase):\n+class ObjectCutView(BrowserView):\n @property\n def title(self):\n return self.context.Title()\n@@ -251,16 +260,24 @@ def do_redirect(self, url, message=None, message_type="info", raise_exception=No\n raise raise_exception\n \n def do_action(self):\n- if self.is_locked:\n- return self.do_redirect(\n- self.view_url,\n- _(\n- "${title} is locked and cannot be cut.",\n- mapping={\n- "title": self.title,\n- },\n- ),\n- )\n+ try:\n+ lock_info = self.context.restrictedTraverse("@@plone_lock_info")\n+ except AttributeError:\n+ lock_info = None\n+ if lock_info is not None:\n+ if lock_info.is_locked_for_current_user():\n+ return self.do_redirect(\n+ self.view_url,\n+ _(\n+ "${title} is locked and cannot be cut.",\n+ mapping={\n+ "title": self.title,\n+ },\n+ ),\n+ )\n+ elif lock_info.is_locked():\n+ # unlock object as it is locked by current user\n+ ILockable(self.context).unlock()\n \n try:\n cp = self.parent.manage_cutObjects(self.context.getId())\ndiff --git a/plone/app/content/browser/contents/cut.py b/plone/app/content/browser/contents/cut.py\nindex 1e87b45..e785931 100644\n--- a/plone/app/content/browser/contents/cut.py\n+++ b/plone/app/content/browser/contents/cut.py\n@@ -4,6 +4,7 @@\n from plone.app.content.browser.contents import ContentsBaseAction\n from plone.app.content.interfaces import IStructureAction\n from plone.base import PloneMessageFactory as _\n+from plone.locking.interfaces import ILockable\n from zope.i18n import translate\n from zope.interface import implementer\n \n@@ -36,14 +37,23 @@ def action(self, obj):\n def finish(self):\n oblist = []\n for ob in self.oblist:\n- if ob.wl_isLocked():\n- self.errors.append(\n- _(\n- "${title} is being edited and cannot be cut.",\n- mapping={"title": self.objectTitle(ob)},\n+ try:\n+ lock_info = ob.restrictedTraverse("@@plone_lock_info")\n+ except AttributeError:\n+ lock_info = None\n+ if lock_info is not None:\n+ if lock_info.is_locked_for_current_user():\n+ self.errors.append(\n+ _(\n+ "${title} is being edited and cannot be cut.",\n+ mapping={"title": self.objectTitle(ob)},\n+ )\n )\n- )\n- continue\n+ continue\n+ elif lock_info.is_locked():\n+ # unlock object as it is locked by current user\n+ ILockable(ob).unlock()\n+\n if not ob.cb_isMoveable():\n self.errors.append(\n _(\ndiff --git a/plone/app/content/browser/contents/delete.py b/plone/app/content/browser/contents/delete.py\nindex 658e665..5adac14 100644\n--- a/plone/app/content/browser/contents/delete.py\n+++ b/plone/app/content/browser/contents/delete.py\n@@ -3,6 +3,7 @@\n from plone.app.content.browser.contents import ContentsBaseAction\n from plone.app.content.interfaces import IStructureAction\n from plone.base import PloneMessageFactory as _\n+from plone.locking.interfaces import ILockable\n from Products.CMFCore.utils import getToolByName\n from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile\n from zope.component import getMultiAdapter\n@@ -70,19 +71,25 @@ def action(self, obj):\n lock_info = obj.restrictedTraverse("@@plone_lock_info")\n except AttributeError:\n lock_info = None\n-\n- if lock_info is not None and lock_info.is_locked():\n- self.errors.append(\n- _("${title} is locked and cannot be deleted.", mapping={"title": title})\n- )\n- return\n- else:\n- try:\n- parent.manage_delObjects(obj.getId())\n- except Unauthorized:\n+ if lock_info is not None:\n+ if lock_info.is_locked_for_current_user():\n self.errors.append(\n _(\n- "You are not authorized to delete ${title}.",\n- mapping={"title": self.objectTitle(self.dest)},\n+ "${title} is locked and cannot be deleted.",\n+ mapping={"title": title},\n )\n )\n+ return\n+ elif lock_info.is_locked():\n+ # unlock object as it is locked by current user\n+ ILockable(obj).unlock()\n+\n+ try:\n+ parent.manage_delObjects(obj.getId())\n+ except Unauthorized:\n+ self.errors.append(\n+ _(\n+ "You are not authorized to delete ${title}.",\n+ mapping={"title": self.objectTitle(self.dest)},\n+ )\n+ )\ndiff --git a/plone/app/content/tests/test_folder.py b/plone/app/content/tests/test_folder.py\nindex c0e7a3f..01d2764 100644\n--- a/plone/app/content/tests/test_folder.py\n+++ b/plone/app/content/tests/test_folder.py\n@@ -2,9 +2,11 @@\n from plone.app.content.testing import PLONE_APP_CONTENT_DX_FUNCTIONAL_TESTING\n from plone.app.content.testing import PLONE_APP_CONTENT_DX_INTEGRATION_TESTING\n from plone.app.testing import login\n+from plone.app.testing import logout\n from plone.app.testing import setRoles\n from plone.app.testing import TEST_USER_ID\n from plone.app.testing import TEST_USER_NAME\n+from plone.app.testing import TEST_USER_PASSWORD\n from plone.dexterity.fti import DexterityFTI\n from plone.locking.interfaces import IRefreshableLockable\n from plone.protect.authenticator import createToken\n@@ -224,6 +226,10 @@ def setUp(self):\n login(self.portal, TEST_USER_NAME)\n setRoles(self.portal, TEST_USER_ID, ["Manager"])\n \n+ self.portal.acl_users.userFolderAddUser(\n+ "editor", TEST_USER_PASSWORD, ["Editor"], []\n+ )\n+\n self.portal.invokeFactory("Document", id="page", title="page")\n self.portal.page.reindexObject()\n \n@@ -237,14 +243,33 @@ def setUp(self):\n }\n self.request.REQUEST_METHOD = "POST"\n \n- def test_cut_object_when_locked(self):\n+ def test_cut_object_when_locked_by_current_user(self):\n from plone.app.content.browser.contents.cut import CutActionView\n \n+ plone_lock_info = self.portal.page.restrictedTraverse("@@plone_lock_info")\n lockable = IRefreshableLockable(self.portal.page)\n lockable.lock()\n+ self.assertTrue(plone_lock_info.is_locked())\n+ view = CutActionView(self.portal, self.request)\n+ view()\n+ self.assertEqual(len(view.errors), 0)\n+ self.assertFalse(plone_lock_info.is_locked())\n+\n+ def test_cut_object_when_locked_by_other_user(self):\n+ from plone.app.content.browser.contents.cut import CutActionView\n+\n+ plone_lock_info = self.portal.page.restrictedTraverse("@@plone_lock_info")\n+ lockable = IRefreshableLockable(self.portal.page)\n+ lockable.lock()\n+ logout()\n+\n+ login(self.portal, "editor")\n+ self.assertTrue(plone_lock_info.is_locked())\n+ self.request.form["_authenticator"] = createToken()\n view = CutActionView(self.portal, self.request)\n view()\n self.assertEqual(len(view.errors), 1)\n+ self.assertTrue(plone_lock_info.is_locked())\n \n \n class DeleteDXTest(BaseTest):\n@@ -257,6 +282,10 @@ def setUp(self):\n login(self.portal, TEST_USER_NAME)\n setRoles(self.portal, TEST_USER_ID, ["Manager"])\n \n+ self.portal.acl_users.userFolderAddUser(\n+ "editor", TEST_USER_PASSWORD, ["Editor"], []\n+ )\n+\n self.portal.invokeFactory("Document", id="page", title="page")\n self.portal.page.reindexObject()\n \n@@ -285,14 +314,32 @@ def test_delete_object(self):\n view()\n self.assertTrue(page_id not in self.portal)\n \n- def test_delete_object_when_locked(self):\n+ def test_delete_object_when_locked_by_current_user(self):\n from plone.app.content.browser.contents.delete import DeleteActionView\n \n+ page_id = self.portal.page.id\n lockable = IRefreshableLockable(self.portal.page)\n lockable.lock()\n view = DeleteActionView(self.portal, self.request)\n view()\n+ self.assertEqual(len(view.errors), 0)\n+ self.assertTrue(page_id not in self.portal)\n+\n+ def test_delete_object_when_locked_by_other_user(self):\n+ from plone.app.content.browser.contents.delete import DeleteActionView\n+\n+ plone_lock_info = self.portal.page.restrictedTraverse("@@plone_lock_info")\n+ lockable = IRefreshableLockable(self.portal.page)\n+ lockable.lock()\n+ logout()\n+\n+ login(self.portal, "editor")\n+ self.assertTrue(plone_lock_info.is_locked())\n+ self.request.form["_authenticator"] = createToken()\n+ view = DeleteActionView(self.portal, self.request)\n+ view()\n self.assertEqual(len(view.errors), 1)\n+ self.assertTrue(plone_lock_info.is_locked())\n \n def test_delete_wrong_object_by_acquisition(self):\n page_id = self.portal.page.id\n'