diff --git a/docs/HISTORY.txt b/docs/HISTORY.txt index 96ea5e5..ddcaaae 100644 --- a/docs/HISTORY.txt +++ b/docs/HISTORY.txt @@ -4,7 +4,11 @@ Changelog 2.1.1 (unreleased) ------------------ -- Fixed schema caching. Previously, a non-persistent counter would be +* When pasting into a dexterity container check the FTI for the the pasted + object to see if it is allowed in the new container. + [wichert] + +* Fixed schema caching. Previously, a non-persistent counter would be used as part of the cache key, and changes made to this counter in one process would obviously not propagate to other processes. @@ -26,22 +30,22 @@ Changelog expected effect. [gaudenzius] -- The default attribute accessor now also looks through subtypes +* The default attribute accessor now also looks through subtypes (behaviors) to find a field default. [malthe] -- Added support in the FTI to look up behaviors by utility name when +* Added support in the FTI to look up behaviors by utility name when getting additional schemata (i.e. fields provided by behaviors). This functionality makes it possible to create a behavior where the interface is dynamically generated. [malthe] -- Return early for attributes that begin with two underscores. +* Return early for attributes that begin with two underscores. https://github.com/plone/plone.dexterity/pull/11 [malthe] -- Make it possible to define a SchemaPolicy for the FTI +* Make it possible to define a SchemaPolicy for the FTI [Frédéric Péters] [gbastien] diff --git a/plone/dexterity/content.py b/plone/dexterity/content.py index fef77b1..142b471 100644 --- a/plone/dexterity/content.py +++ b/plone/dexterity/content.py @@ -1,4 +1,4 @@ -from Acquisition import Explicit, aq_parent +from Acquisition import Explicit, aq_base, aq_parent from zExceptions import Unauthorized from copy import deepcopy @@ -32,6 +32,7 @@ from Products.CMFCore.PortalContent import PortalContent from Products.CMFCore.PortalFolder import PortalFolderBase from Products.CMFCore.CMFCatalogAware import CMFCatalogAware from Products.CMFPlone.interfaces import IConstrainTypes +from Products.CMFCore.interfaces import ITypeInformation from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl from Products.CMFDefault.utils import tuplize @@ -152,6 +153,22 @@ class AttributeValidator(Explicit): return None +class PasteBehaviourMixin(object): + def _verifyObjectPaste(self, obj, validate_src=True): + # Extend the paste checks from OFS.CopySupport.CopyContainer + # (permission checks) and + # Products.CMFCore.PortalFolder.PortalFolderBase (permission checks and + # allowed content types) to also ask the FTI if construction is + # allowed. + super(PasteBehaviourMixin, self)._verifyObjectPaste(obj, validate_src) + if validate_src: + portal_type = getattr(aq_base(obj), 'portal_type', None) + if portal_type: + fti = queryUtility(ITypeInformation, name=portal_type) + if fti is not None and not fti.isConstructionAllowed(self): + raise ValueError('You can not add the copied content here.') + + class DexterityContent(DAVResourceMixin, PortalContent, DefaultDublinCoreImpl, Contained): """Base class for Dexterity content """ @@ -246,7 +263,8 @@ class DexterityContent(DAVResourceMixin, PortalContent, DefaultDublinCoreImpl, C s.append(part) return tuple(s) -class Item(BrowserDefaultMixin, DexterityContent): + +class Item(PasteBehaviourMixin, BrowserDefaultMixin, DexterityContent): """A non-containerish, CMFish item """ @@ -275,7 +293,7 @@ class Item(BrowserDefaultMixin, DexterityContent): __getattr__ = DexterityContent.__getattr__ -class Container(DAVCollectionMixin, BrowserDefaultMixin, CMFCatalogAware, CMFOrderedBTreeFolderBase, DexterityContent): +class Container(PasteBehaviourMixin, DAVCollectionMixin, BrowserDefaultMixin, CMFCatalogAware, CMFOrderedBTreeFolderBase, DexterityContent): """Base class for folderish items """ diff --git a/plone/dexterity/tests/test_content.py b/plone/dexterity/tests/test_content.py index c0725c0..2343a6a 100644 --- a/plone/dexterity/tests/test_content.py +++ b/plone/dexterity/tests/test_content.py @@ -705,6 +705,68 @@ class TestContent(MockTestCase): self.assertEquals(folder.allowedContentTypes(), [fti_mock]) self.assertRaises(ValueError, folder.invokeFactory, u"disallowed_type", id="test") + def test_verifyObjectPaste_paste_without_portal_type(self): + original_container = Container(id='parent') + original_container.manage_permission('View', ('Anonymous',)) + content = Item(id='test') + content.__factory_meta_type__ = 'document' + container = Container(id='container') + container.all_meta_types = [{'name': 'document', + 'action': None, + 'permission': 'View'}] + container.manage_permission('View', ('Anonymous',)) + container['test'] = content + content = container['test'] + container._verifyObjectPaste(content, True) + + def test_verifyObjectPaste_fti_does_not_allow_content(self): + from Products.CMFCore.interfaces import ITypeInformation + original_container = Container(id='parent') + original_container.manage_permission('View', ('Anonymous',)) + content = Item(id='test') + content.__factory_meta_type__ = 'document' + content.portal_type = 'document' + container = Container(id='container') + container.all_meta_types = [{'name': 'document', + 'action': None, + 'permission': 'View'}] + container.manage_permission('View', ('Anonymous',)) + container['test'] = content + content = container['test'] + fti = self.mocker.mock() + self.expect(fti.isConstructionAllowed(container)).result(False) + self.mock_utility(fti, ITypeInformation, name='document') + pt = self.mocker.mock() + self.expect(pt.getTypeInfo('document')).result(None) + self.expect(pt.getTypeInfo(container)).result(None) + self.mock_tool(pt, 'portal_types') + self.replay() + self.assertRaises(ValueError, container._verifyObjectPaste, content, True) + + def test_verifyObjectPaste_fti_does_allow_content(self): + from Products.CMFCore.interfaces import ITypeInformation + original_container = Container(id='parent') + original_container.manage_permission('View', ('Anonymous',)) + content = Item(id='test') + content.__factory_meta_type__ = 'document' + content.portal_type = 'document' + container = Container(id='container') + container.all_meta_types = [{'name': 'document', + 'action': None, + 'permission': 'View'}] + container.manage_permission('View', ('Anonymous',)) + container['test'] = content + content = container['test'] + fti = self.mocker.mock() + self.expect(fti.isConstructionAllowed(container)).result(True) + self.mock_utility(fti, ITypeInformation, name='document') + pt = self.mocker.mock() + self.expect(pt.getTypeInfo('document')).result(None) + self.expect(pt.getTypeInfo(container)).result(None) + self.mock_tool(pt, 'portal_types') + self.replay() + container._verifyObjectPaste(content, True) + def test_suite(): return unittest.defaultTestLoader.loadTestsFromName(__name__)