From 52f7d3254ef19bd50cafabea626a0601fb761b00 Mon Sep 17 00:00:00 2001 From: MrTango Date: Thu, 13 Feb 2025 11:12:26 +0100 Subject: [PATCH 01/11] move UI part of sharing view here --- setup.py | 1 + .../app/layout/controlpanels/configure.zcml | 11 +- src/plone/app/layout/controlpanels/sharing.pt | 292 ++++++++++++++++++ src/plone/app/layout/controlpanels/sharing.py | 74 +++++ 4 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 src/plone/app/layout/controlpanels/sharing.pt create mode 100644 src/plone/app/layout/controlpanels/sharing.py diff --git a/setup.py b/setup.py index a7424451..4b82ef69 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ "plone.app.dexterity", "plone.app.linkintegrity", "plone.app.relationfield", + "plone.app.workflow", "plone.app.uuid", "plone.app.viewletmanager >=1.2", "plone.base >=4.0.0a1", diff --git a/src/plone/app/layout/controlpanels/configure.zcml b/src/plone/app/layout/controlpanels/configure.zcml index 370d65c1..315131e2 100644 --- a/src/plone/app/layout/controlpanels/configure.zcml +++ b/src/plone/app/layout/controlpanels/configure.zcml @@ -1,7 +1,6 @@ + + + + diff --git a/src/plone/app/layout/controlpanels/sharing.pt b/src/plone/app/layout/controlpanels/sharing.pt new file mode 100644 index 00000000..57d752b0 --- /dev/null +++ b/src/plone/app/layout/controlpanels/sharing.pt @@ -0,0 +1,292 @@ + + + + + + + + + +
+
+ + +

+ Sharing for + title +

+ +
+ You can control who can view and edit your item using the list below. +
+ +
+ +
+
+ + + +
+ + +
+
+
+ + +
+ + + + + + + + + + + + + + + + + +
Name
+ + + + + + + + + + + (${entry/id}) + + + + + + + + + + + + + + +
+
+ +
+
+ + + + By default, permissions from the container of this item are inherited. + If you disable this, only the explicitly defined sharing permissions will + be valid. + In the overview, the symbol + + indicates an inherited value. Similarly, the symbol + + indicates a global role, which is managed by the site administrator. + +
+
+ + + +
+
+
+
+
+ + + + diff --git a/src/plone/app/layout/controlpanels/sharing.py b/src/plone/app/layout/controlpanels/sharing.py new file mode 100644 index 00000000..8ec1ea93 --- /dev/null +++ b/src/plone/app/layout/controlpanels/sharing.py @@ -0,0 +1,74 @@ +from plone.app.workflow.browser.sharing import SharingView as ApiSharingView +from plone.app.workflow.events import LocalrolesModifiedEvent +from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from Products.statusmessages.interfaces import IStatusMessage +from zExceptions import Forbidden +from zope.event import notify + + +class SharingView(ApiSharingView): + # Actions + template = ViewPageTemplateFile("sharing.pt") + + def __call__(self): + """Perform the update and redirect if necessary, or render the page""" + postback = self.handle_form() + if postback: + return self.template() + else: + context_state = self.context.restrictedTraverse("@@plone_context_state") + url = context_state.view_url() + self.request.response.redirect(url) + + def handle_form(self): + """ + We split this out so we can reuse this for ajax. + Will return a boolean if it was a post or not + """ + postback = True + + form = self.request.form + submitted = form.get("form.submitted", False) + save_button = form.get("form.button.Save", None) is not None + cancel_button = form.get("form.button.Cancel", None) is not None + if submitted and save_button and not cancel_button: + if not self.request.get("REQUEST_METHOD", "GET") == "POST": + raise Forbidden + + authenticator = self.context.restrictedTraverse("@@authenticator", None) + if not authenticator.verify(): + raise Forbidden + + # Update the acquire-roles setting + if self.can_edit_inherit(): + inherit = bool(form.get("inherit", False)) + reindex = self.update_inherit(inherit, reindex=False) + else: + reindex = False + + # Update settings for users and groups + entries = form.get("entries", []) + roles = [r["id"] for r in self.roles()] + settings = [] + for entry in entries: + settings.append( + dict( + id=entry["id"], + type=entry["type"], + roles=[r for r in roles if entry.get("role_%s" % r, False)], + ) + ) + if settings: + reindex = self.update_role_settings(settings, reindex=False) or reindex + if reindex: + self.context.reindexObjectSecurity() + notify(LocalrolesModifiedEvent(self.context, self.request)) + IStatusMessage(self.request).addStatusMessage( + _("Changes saved."), type="info" + ) + + # Other buttons return to the sharing page + if cancel_button: + postback = False + + return postback From d5756f553eed5e7f87fc5fbb2549009c20cef3c1 Mon Sep 17 00:00:00 2001 From: MrTango Date: Thu, 13 Feb 2025 11:26:52 +0100 Subject: [PATCH 02/11] fix linting --- src/plone/app/layout/controlpanels/configure.zcml | 5 ++++- src/plone/app/layout/controlpanels/sharing.py | 1 + src/plone/app/layout/profiles/default/browserlayer.xml | 3 +-- src/plone/app/layout/profiles/uninstall/browserlayer.xml | 6 +++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/plone/app/layout/controlpanels/configure.zcml b/src/plone/app/layout/controlpanels/configure.zcml index 315131e2..8f7b03b4 100644 --- a/src/plone/app/layout/controlpanels/configure.zcml +++ b/src/plone/app/layout/controlpanels/configure.zcml @@ -20,7 +20,10 @@ layer="plone.app.layout.interfaces.IPloneAppLayoutLayer" /> - + + name="plone.app.layout" /> diff --git a/src/plone/app/layout/profiles/uninstall/browserlayer.xml b/src/plone/app/layout/profiles/uninstall/browserlayer.xml index 0f615d02..2f9ea79a 100644 --- a/src/plone/app/layout/profiles/uninstall/browserlayer.xml +++ b/src/plone/app/layout/profiles/uninstall/browserlayer.xml @@ -1,6 +1,6 @@ - + From 0c83517f34763858cc2a5881eadf6aa22f968802 Mon Sep 17 00:00:00 2001 From: MrTango Date: Thu, 13 Feb 2025 11:50:56 +0100 Subject: [PATCH 03/11] add changelog entry --- news/390.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/390.feature diff --git a/news/390.feature b/news/390.feature new file mode 100644 index 00000000..95f0ac20 --- /dev/null +++ b/news/390.feature @@ -0,0 +1,2 @@ +move UI related code and template of sharing view from plone.app.workflow to plone.app.layout +[MrTango] From 9ad5bcd1a92c3bff1c20a6d19eeff3709a5b8b23 Mon Sep 17 00:00:00 2001 From: MrTango Date: Thu, 13 Feb 2025 20:42:16 +0100 Subject: [PATCH 04/11] add test_sharing_view --- .../app/layout/tests/test_sharing_view.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/plone/app/layout/tests/test_sharing_view.py diff --git a/src/plone/app/layout/tests/test_sharing_view.py b/src/plone/app/layout/tests/test_sharing_view.py new file mode 100644 index 00000000..73fe4b89 --- /dev/null +++ b/src/plone/app/layout/tests/test_sharing_view.py @@ -0,0 +1,58 @@ +from plone.app.testing import login +from plone.app.layout.testing import INTEGRATION_TESTING +from zope.component import getMultiAdapter + +import unittest + + +class TestSharingView(unittest.TestCase): + layer = INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer["portal"] + self.request = self.layer["request"] + + self.portal.acl_users._doAddUser("testuser", "secret", ["Member"], []) + self.portal.acl_users._doAddUser("testreviewer", "secret", ["Reviewer"], []) + self.portal.acl_users._doAddUser("nonasciiuser", "secret", ["Member"], []) + self.portal.acl_users._doAddGroup( + "testgroup", [], title="Some meaningful title" + ) + testuser = self.portal.portal_membership.getMemberById("testuser") + testuser.setMemberProperties(dict(email="testuser@plone.org")) + nonasciiuser = self.portal.portal_membership.getMemberById("nonasciiuser") + nonasciiuser.setMemberProperties(dict(fullname="\xc4\xdc\xdf")) + login(self.portal, "manager") + + def test_group_name_links_to_prefs_for_admin(self): + """Make sure that for admins group name links to group prefs""" + self.request.form["search_term"] = "testgroup" + view = getMultiAdapter((self.portal, self.request), name="sharing") + self.assertIn( + '', + view(), + msg="Group name was not linked to group prefs.", + ) + + def test_group_name_links_not_include_authusers(self): + """Make sure that for admins group name links to group prefs""" + self.request.form["search_term"] = "testgroup" + view = getMultiAdapter((self.portal, self.request), name="sharing") + self.assertNotIn( + '', + view(), + msg="AuthenticatedUsers was linked to group prefs.", + ) + + def test_group_name_doesnt_link_to_prefs_for_reviewer(self): + """Make sure that for admins group name links to group prefs""" + login(self.portal, "testreviewer") + self.request.form["search_term"] = "testgroup" + view = getMultiAdapter((self.portal, self.request), name="sharing") + self.assertNotIn( + '', + view(), + msg="Group name link was unexpectedly shown to reviewer.", + ) From 57e454933c20edfb338a8d32e2213737eb78d438 Mon Sep 17 00:00:00 2001 From: MrTango Date: Thu, 13 Feb 2025 20:54:13 +0100 Subject: [PATCH 05/11] fix linting --- src/plone/app/layout/profiles/default/browserlayer.xml | 3 ++- src/plone/app/layout/profiles/uninstall/browserlayer.xml | 5 +++-- src/plone/app/layout/tests/test_sharing_view.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/plone/app/layout/profiles/default/browserlayer.xml b/src/plone/app/layout/profiles/default/browserlayer.xml index 12381da3..4745f5cb 100644 --- a/src/plone/app/layout/profiles/default/browserlayer.xml +++ b/src/plone/app/layout/profiles/default/browserlayer.xml @@ -1,5 +1,6 @@ + name="plone.app.layout" + /> diff --git a/src/plone/app/layout/profiles/uninstall/browserlayer.xml b/src/plone/app/layout/profiles/uninstall/browserlayer.xml index 2f9ea79a..0a13c434 100644 --- a/src/plone/app/layout/profiles/uninstall/browserlayer.xml +++ b/src/plone/app/layout/profiles/uninstall/browserlayer.xml @@ -1,6 +1,7 @@ + name="plone.app.layout" + remove="true" + /> diff --git a/src/plone/app/layout/tests/test_sharing_view.py b/src/plone/app/layout/tests/test_sharing_view.py index 73fe4b89..ab38bf51 100644 --- a/src/plone/app/layout/tests/test_sharing_view.py +++ b/src/plone/app/layout/tests/test_sharing_view.py @@ -1,5 +1,5 @@ -from plone.app.testing import login from plone.app.layout.testing import INTEGRATION_TESTING +from plone.app.testing import login from zope.component import getMultiAdapter import unittest From bbf6c7d4c510d735d63563b4f5689b73baf2f8d2 Mon Sep 17 00:00:00 2001 From: MrTango Date: Fri, 14 Feb 2025 10:59:03 +0100 Subject: [PATCH 06/11] fix import of message factory --- src/plone/app/layout/controlpanels/sharing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/app/layout/controlpanels/sharing.py b/src/plone/app/layout/controlpanels/sharing.py index 7644e6fb..5217f68c 100644 --- a/src/plone/app/layout/controlpanels/sharing.py +++ b/src/plone/app/layout/controlpanels/sharing.py @@ -1,4 +1,4 @@ -from plone.app.layout import _ +from plone.app.layout import PloneMessageFactory as _ from plone.app.workflow.browser.sharing import SharingView as ApiSharingView from plone.app.workflow.events import LocalrolesModifiedEvent from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile From ecce9c796b471d2c9c37483fc32b0843d8c57f74 Mon Sep 17 00:00:00 2001 From: MrTango Date: Fri, 14 Feb 2025 11:03:41 +0100 Subject: [PATCH 07/11] fix BrowserLayer name --- src/plone/app/layout/controlpanels/configure.zcml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/app/layout/controlpanels/configure.zcml b/src/plone/app/layout/controlpanels/configure.zcml index 8f7b03b4..00d1d512 100644 --- a/src/plone/app/layout/controlpanels/configure.zcml +++ b/src/plone/app/layout/controlpanels/configure.zcml @@ -30,7 +30,7 @@ for="*" class=".sharing.SharingView" permission="plone.DelegateRoles" - layer="plone.app.layout.interfaces.IClassicUILayer" + layer="plone.app.layout.interfaces.IPloneAppLayoutLayer" /> From b7b8d5ff5abdd29b6292195ec1480222953ad458 Mon Sep 17 00:00:00 2001 From: MrTango Date: Fri, 14 Feb 2025 11:03:49 +0100 Subject: [PATCH 08/11] fix test setup --- src/plone/app/layout/testing.py | 86 +++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/plone/app/layout/testing.py b/src/plone/app/layout/testing.py index 6101bb85..76bcfc2f 100644 --- a/src/plone/app/layout/testing.py +++ b/src/plone/app/layout/testing.py @@ -7,6 +7,7 @@ from plone.app.testing import TEST_USER_ID from plone.base.utils import unrestricted_construct_instance from plone.testing import zope +from Products.CMFCore.utils import getToolByName class PloneAppLayoutFixture(PloneSandboxLayer): @@ -26,6 +27,91 @@ def setUpPloneSite(self, portal): mtool.createMemberArea(TEST_USER_ID) if mtool.getMemberareaCreationFlag(): mtool.setMemberareaCreationFlag() + applyProfile(portal, "plone.app.contenttypes:default") + + acl_users = getToolByName(portal, "acl_users") + + acl_users.userFolderAddUser( + "manager", + "secret", + [ + "Manager", + ], + [], + ) + acl_users.userFolderAddUser( + "member", + "secret", + [ + "Member", + ], + [], + ) + acl_users.userFolderAddUser( + "owner", + "secret", + [ + "Owner", + ], + [], + ) + acl_users.userFolderAddUser( + "reviewer", + "secret", + [ + "Reviewer", + ], + [], + ) + acl_users.userFolderAddUser( + "editor", + "secret", + [ + "Editor", + ], + [], + ) + acl_users.userFolderAddUser( + "reader", + "secret", + [ + "Reader", + ], + [], + ) + + acl_users.userFolderAddUser( + "delegate_reader", + "secret", + [ + "Member", + ], + [], + ) + acl_users.userFolderAddUser( + "delegate_editor", + "secret", + [ + "Member", + ], + [], + ) + acl_users.userFolderAddUser( + "delegate_contributor", + "secret", + [ + "Member", + ], + [], + ) + acl_users.userFolderAddUser( + "delegate_reviewer", + "secret", + [ + "Member", + ], + [], + ) applyProfile(portal, "plone.app.layout:default") From cc4f24a630117b99e94d541b8226bc75efaac2c4 Mon Sep 17 00:00:00 2001 From: MrTango Date: Sun, 31 May 2026 13:40:13 +0200 Subject: [PATCH 09/11] rm IPloneAppLayoutLayer as suggested by ale --- src/plone/app/layout/profiles/uninstall/browserlayer.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plone/app/layout/profiles/uninstall/browserlayer.xml b/src/plone/app/layout/profiles/uninstall/browserlayer.xml index 0a13c434..0f615d02 100644 --- a/src/plone/app/layout/profiles/uninstall/browserlayer.xml +++ b/src/plone/app/layout/profiles/uninstall/browserlayer.xml @@ -1,7 +1,6 @@ - From 2c83337c3137c9ec22cd7856467671f91340e763 Mon Sep 17 00:00:00 2001 From: MrTango Date: Sun, 31 May 2026 16:08:55 +0200 Subject: [PATCH 10/11] Add sharing view with CSRF doctest moved from Products.CMFPlone - Fix PloneMessageFactory import in controlpanels/sharing.py (use plone.base). - Fix testing-layer name in test_sharing_view.py. - Add sharing_csrf.txt functional doctest under the layout functional layer, which installs plone.app.layout:default so IPloneAppLayoutLayer is active. --- news/3953.tests.1 | 2 + src/plone/app/layout/controlpanels/sharing.py | 2 +- src/plone/app/layout/tests/sharing_csrf.txt | 65 +++++++++++++++++++ .../app/layout/tests/test_sharing_csrf.py | 23 +++++++ .../app/layout/tests/test_sharing_view.py | 4 +- 5 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 news/3953.tests.1 create mode 100644 src/plone/app/layout/tests/sharing_csrf.txt create mode 100644 src/plone/app/layout/tests/test_sharing_csrf.py diff --git a/news/3953.tests.1 b/news/3953.tests.1 new file mode 100644 index 00000000..cd1db7a5 --- /dev/null +++ b/news/3953.tests.1 @@ -0,0 +1,2 @@ +Add ``sharing_csrf.txt`` functional doctest, moved here from ``Products.CMFPlone`` together with the Classic-UI ``@@sharing`` view. +[MrTango] diff --git a/src/plone/app/layout/controlpanels/sharing.py b/src/plone/app/layout/controlpanels/sharing.py index 5217f68c..9479b2b1 100644 --- a/src/plone/app/layout/controlpanels/sharing.py +++ b/src/plone/app/layout/controlpanels/sharing.py @@ -1,4 +1,4 @@ -from plone.app.layout import PloneMessageFactory as _ +from plone.base import PloneMessageFactory as _ from plone.app.workflow.browser.sharing import SharingView as ApiSharingView from plone.app.workflow.events import LocalrolesModifiedEvent from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile diff --git a/src/plone/app/layout/tests/sharing_csrf.txt b/src/plone/app/layout/tests/sharing_csrf.txt new file mode 100644 index 00000000..6344364d --- /dev/null +++ b/src/plone/app/layout/tests/sharing_csrf.txt @@ -0,0 +1,65 @@ +Sharing view CSRF protection +============================ + +The Classic-UI ``@@sharing`` view lives in plone.app.layout and is registered +on the ``IPloneAppLayoutLayer`` browser layer (installed by the +``plone.app.layout:default`` profile). Like the other write-enabled forms it +must be protected against CSRF attacks. This test used to live in +``Products.CMFPlone/tests/csrf.txt`` and moved here together with the view. + +First we need a logged in user with manager rights: + + >>> import transaction; transaction.commit() + >>> from plone.app.testing import SITE_OWNER_NAME + >>> from plone.app.testing import SITE_OWNER_PASSWORD + >>> from plone.testing.zope import Browser + >>> app = layer['app'] + >>> portal = layer['portal'] + >>> browser = Browser(app) + >>> browser.handleErrors = False + >>> browser.open('http://nohost/plone/login_form') + >>> browser.getControl(name='__ac_name').value = SITE_OWNER_NAME + >>> browser.getControl(name='__ac_password').value = SITE_OWNER_PASSWORD + >>> browser.getControl('Log in').click() + +Now we add a folder to share: + + >>> browser.open('http://nohost/plone/') + >>> browser.getLink(url='++add++Folder').click() + >>> browser.getControl('Title').value = 'a folder' + >>> browser.getControl('Save').click() + >>> browser.url + 'http://nohost/plone/a-folder/view' + +Reopen URL to clean up HTTP_REFERRER + + >>> browser.open('http://nohost/plone/a-folder/') + +"Sharing" the item via the toolbar link: + + >>> browser.getLink('Sharing').click() + >>> browser.url + 'http://nohost/plone/a-folder/@@sharing?_auth...' + >>> browser.getControl(name='entries.role_Editor:records').value + [] + +Change the value of the second _authenticator and check for Exception + + >>> browser.getControl(name='_authenticator', index=1).value = 'invalid!' + >>> browser.getControl(name='entries.role_Editor:records').value = ['True'] + >>> browser.getControl('Save').click() + Traceback (most recent call last): + ... + zExceptions.Forbidden + +With a valid authenticator the change goes through: + + >>> browser.getLink('Sharing').click() + >>> browser.getControl(name='entries.role_Editor:records').value = ['True'] + >>> browser.getControl('Save').click() + >>> browser.url + 'http://nohost/plone/a-folder/@@sharing' + >>> browser.contents + '...Info...Changes saved...' + >>> browser.getControl(name='entries.role_Editor:records').value + ['True'] diff --git a/src/plone/app/layout/tests/test_sharing_csrf.py b/src/plone/app/layout/tests/test_sharing_csrf.py new file mode 100644 index 00000000..f57413ba --- /dev/null +++ b/src/plone/app/layout/tests/test_sharing_csrf.py @@ -0,0 +1,23 @@ +from plone.app.layout.testing import PLONE_APP_LAYOUT_FUNCTIONAL_TESTING +from plone.testing import layered + +import doctest +import unittest + + +OPTIONFLAGS = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest( + layered( + doctest.DocFileSuite( + "sharing_csrf.txt", + optionflags=OPTIONFLAGS, + package="plone.app.layout.tests", + ), + layer=PLONE_APP_LAYOUT_FUNCTIONAL_TESTING, + ) + ) + return suite diff --git a/src/plone/app/layout/tests/test_sharing_view.py b/src/plone/app/layout/tests/test_sharing_view.py index ab38bf51..4e64fa1e 100644 --- a/src/plone/app/layout/tests/test_sharing_view.py +++ b/src/plone/app/layout/tests/test_sharing_view.py @@ -1,4 +1,4 @@ -from plone.app.layout.testing import INTEGRATION_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_INTEGRATION_TESTING from plone.app.testing import login from zope.component import getMultiAdapter @@ -6,7 +6,7 @@ class TestSharingView(unittest.TestCase): - layer = INTEGRATION_TESTING + layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING def setUp(self): self.portal = self.layer["portal"] From 3fe7b1877d38c0d93eaf9cf3fcf15600f234ab2f Mon Sep 17 00:00:00 2001 From: MrTango Date: Wed, 3 Jun 2026 18:16:33 +0300 Subject: [PATCH 11/11] Fix isort import ordering in sharing controlpanel and CSRF test --- src/plone/app/layout/controlpanels/sharing.py | 2 +- src/plone/app/layout/tests/test_sharing_csrf.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plone/app/layout/controlpanels/sharing.py b/src/plone/app/layout/controlpanels/sharing.py index 9479b2b1..23b3a847 100644 --- a/src/plone/app/layout/controlpanels/sharing.py +++ b/src/plone/app/layout/controlpanels/sharing.py @@ -1,6 +1,6 @@ -from plone.base import PloneMessageFactory as _ from plone.app.workflow.browser.sharing import SharingView as ApiSharingView from plone.app.workflow.events import LocalrolesModifiedEvent +from plone.base import PloneMessageFactory as _ from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from Products.statusmessages.interfaces import IStatusMessage from zExceptions import Forbidden diff --git a/src/plone/app/layout/tests/test_sharing_csrf.py b/src/plone/app/layout/tests/test_sharing_csrf.py index f57413ba..99e66ca6 100644 --- a/src/plone/app/layout/tests/test_sharing_csrf.py +++ b/src/plone/app/layout/tests/test_sharing_csrf.py @@ -4,7 +4,6 @@ import doctest import unittest - OPTIONFLAGS = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE