diff --git a/Products/CMFPlone/Portal.py b/Products/CMFPlone/Portal.py index 60477253cb..f1df7afe94 100644 --- a/Products/CMFPlone/Portal.py +++ b/Products/CMFPlone/Portal.py @@ -1,20 +1,30 @@ from AccessControl import ClassSecurityInfo -from AccessControl import Permissions from AccessControl import Unauthorized from AccessControl.class_init import InitializeClass from Acquisition import aq_base from ComputedAttribute import ComputedAttribute +from five.localsitemanager.registry import PersistentComponents from OFS.ObjectManager import REPLACEABLE +from plone.dexterity.content import Container +from plone.i18n.locales.interfaces import IMetadataLanguageAvailability from Products.CMFCore import permissions +from Products.CMFCore.interfaces import IContentish +from Products.CMFCore.interfaces import ISiteRoot +from Products.CMFCore.permissions import AccessContentsInformation +from Products.CMFCore.permissions import AddPortalMember +from Products.CMFCore.permissions import MailForgottenPassword +from Products.CMFCore.permissions import RequestReview +from Products.CMFCore.permissions import ReviewPortalContent +from Products.CMFCore.permissions import SetOwnPassword +from Products.CMFCore.permissions import SetOwnProperties +from Products.CMFCore.PortalFolder import PortalFolderBase from Products.CMFCore.PortalObject import PortalObjectBase -from Products.CMFCore.utils import UniqueObject +from Products.CMFCore.Skinnable import SkinnableObjectManager from Products.CMFCore.utils import _checkPermission from Products.CMFCore.utils import getToolByName -from Products.CMFDynamicViewFTI.browserdefault import BrowserDefaultMixin -from Products.CMFPlone import PloneMessageFactory as _ +from Products.CMFCore.utils import UniqueObject from Products.CMFPlone import bbb -from Products.CMFPlone.DublinCore import DefaultDublinCoreImpl -from Products.CMFPlone.PloneFolder import OrderedContainer +from Products.CMFPlone import PloneMessageFactory as _ from Products.CMFPlone.interfaces.siteroot import IPloneSiteRoot from Products.CMFPlone.interfaces.syndication import ISyndicatable from Products.CMFPlone.permissions import AddPortalContent @@ -23,27 +33,67 @@ from Products.CMFPlone.permissions import ModifyPortalContent from Products.CMFPlone.permissions import ReplyToItem from Products.CMFPlone.permissions import View -from plone.i18n.locales.interfaces import IMetadataLanguageAvailability +from Products.Five.component.interfaces import IObjectManagerSite from zope.component import queryUtility +from zope.component.interfaces import ComponentLookupError +from zope.event import notify +from zope.interface import classImplementsOnly +from zope.interface import implementedBy from zope.interface import implementer +from zope.traversing.interfaces import BeforeTraverseEvent + if bbb.HAS_ZSERVER: from webdav.NullResource import NullResource -@implementer(IPloneSiteRoot, ISyndicatable) -class PloneSite(PortalObjectBase, DefaultDublinCoreImpl, OrderedContainer, - BrowserDefaultMixin, UniqueObject): +@implementer(IPloneSiteRoot, ISiteRoot, ISyndicatable, IObjectManagerSite) +class PloneSite(Container, SkinnableObjectManager, UniqueObject): """ The Plone site object. """ security = ClassSecurityInfo() meta_type = portal_type = 'Plone Site' + # Ensure certain attributes come from the correct base class. + _checkId = SkinnableObjectManager._checkId + manage_main = PortalFolderBase.manage_main + + def __getattr__(self, name): + try: + # Try DX + return super().__getattr__(name) + except AttributeError: + # Check portal_skins + return SkinnableObjectManager.__getattr__(self, name) + + def __setattr__(self, name, obj): + # handle re setting an item as an attribute + if self._tree is not None and name in self: + del self[name] + self[name] = obj + else: + super().__setattr__(name, obj) + + def __delattr__(self, name): + try: + return super().__delattr__(name) + except AttributeError: + return self.__delitem__(name) + + # Removes the 'Components Folder' + manage_options = ( - PortalObjectBase.manage_options[:2] + - PortalObjectBase.manage_options[3:]) + Container.manage_options[:2] + + Container.manage_options[3:]) __ac_permissions__ = ( + (AccessContentsInformation, ()), + (AddPortalMember, ()), + (SetOwnPassword, ()), + (SetOwnProperties, ()), + (MailForgottenPassword, ()), + (RequestReview, ()), + (ReviewPortalContent, ()), (AddPortalContent, ()), (AddPortalFolders, ()), (ListPortalMembers, ()), @@ -53,13 +103,6 @@ class PloneSite(PortalObjectBase, DefaultDublinCoreImpl, OrderedContainer, 'manage_renameForm', 'manage_renameObject', 'manage_renameObjects'))) - security.declareProtected(Permissions.copy_or_move, 'manage_copyObjects') - - manage_renameObject = OrderedContainer.manage_renameObject - - moveObject = OrderedContainer.moveObject - moveObjectsByDelta = OrderedContainer.moveObjectsByDelta - # Switch off ZMI ordering interface as it assumes a slightly # different functionality has_order_support = 0 @@ -73,9 +116,28 @@ class PloneSite(PortalObjectBase, DefaultDublinCoreImpl, OrderedContainer, description = '' icon = 'misc_/CMFPlone/tool.gif' + # From PortalObjectBase def __init__(self, id, title=''): - PortalObjectBase.__init__(self, id, title) - DefaultDublinCoreImpl.__init__(self) + super(PloneSite, self).__init__(id, title=title) + components = PersistentComponents('++etc++site') + components.__parent__ = self + self.setSiteManager(components) + + # From PortalObjectBase + def __before_publishing_traverse__(self, arg1, arg2=None): + """ Pre-traversal hook. + """ + # XXX hack around a bug(?) in BeforeTraverse.MultiHook + REQUEST = arg2 or arg1 + + try: + notify(BeforeTraverseEvent(self, REQUEST)) + except ComponentLookupError: + # allow ZMI access, even if the portal's site manager is missing + pass + self.setupCurrentSkin(REQUEST) + + super(PloneSite, self).__before_publishing_traverse__(arg1, arg2) def __browser_default__(self, request): """ Set default so we can return whatever we want instead @@ -173,4 +235,9 @@ def reindexObject(self, idxs=None): def reindexObjectSecurity(self, skip_self=False): pass + +# Remove the IContentish interface so we don't listen to events that won't +# apply to the site root, ie handleUidAnnotationEvent +classImplementsOnly(PloneSite, implementedBy(PloneSite) - IContentish) + InitializeClass(PloneSite) diff --git a/Products/CMFPlone/browser/admin.py b/Products/CMFPlone/browser/admin.py index 1c149370a8..62301e605a 100644 --- a/Products/CMFPlone/browser/admin.py +++ b/Products/CMFPlone/browser/admin.py @@ -70,7 +70,12 @@ def sites(self, root=None): return result def outdated(self, obj): - mig = obj.get('portal_migration', None) + # Try to pick the portal_migration as an attribute + # (Plone 5 unmigrated site root) or as an item + mig = ( + getattr(obj, "portal_migration", None) + or obj.get('portal_migration', None) + ) if mig is not None: return mig.needUpgrading() return False diff --git a/Products/CMFPlone/configure.zcml b/Products/CMFPlone/configure.zcml index d2cdac1e8c..4594586eb8 100644 --- a/Products/CMFPlone/configure.zcml +++ b/Products/CMFPlone/configure.zcml @@ -29,6 +29,7 @@ + diff --git a/Products/CMFPlone/factory.py b/Products/CMFPlone/factory.py index 5336f3f493..41f47a1c81 100644 --- a/Products/CMFPlone/factory.py +++ b/Products/CMFPlone/factory.py @@ -1,5 +1,7 @@ from logging import getLogger from plone.registry.interfaces import IRegistry +from plone.uuid.handlers import addAttributeUUID +from Products.CMFCore.interfaces import ISiteRoot from Products.CMFPlone import PloneMessageFactory as _ from Products.CMFPlone.events import SiteManagerCreatedEvent from Products.CMFPlone.interfaces import INonInstallable @@ -10,6 +12,7 @@ from zope.component.hooks import setSite from zope.event import notify from zope.interface import implementer +from zope.lifecycleevent import ObjectCreatedEvent _TOOL_ID = 'portal_setup' _DEFAULT_PROFILE = 'Products.CMFPlone:plone' @@ -120,8 +123,12 @@ def addPloneSite(context, site_id, title='Plone site', description='', extension_ids=(), setup_content=True, default_language='en', portal_timezone='UTC'): """Add a PloneSite to the context.""" - context._setObject(site_id, PloneSite(site_id)) - site = context._getOb(site_id) + + site = PloneSite(site_id) + notify(ObjectCreatedEvent(site)) + context[site_id] = site + + site = context[site_id] site.setLanguage(default_language) # Set the accepted language for the rest of the request. This makes sure # the front-page text gets the correct translation also when your browser diff --git a/Products/CMFPlone/profiles/default/types.xml b/Products/CMFPlone/profiles/default/types.xml index 93d91d8054..324c5fa186 100644 --- a/Products/CMFPlone/profiles/default/types.xml +++ b/Products/CMFPlone/profiles/default/types.xml @@ -3,7 +3,6 @@ Controls the available content types in your portal - + diff --git a/Products/CMFPlone/profiles/default/types/Plone_Site.xml b/Products/CMFPlone/profiles/default/types/Plone_Site.xml index 8cdc3035c2..4527802170 100644 --- a/Products/CMFPlone/profiles/default/types/Plone_Site.xml +++ b/Products/CMFPlone/profiles/default/types/Plone_Site.xml @@ -1,29 +1,63 @@ - - - The root object in a Plone site. - - Plone Site - CMFPlone - manage_addSite - folder_listing - False - False - - False - folder_listing - - - - - - - + + + + Plone Site + + + False + manage_addSite + + + + + + False + False + + + cmf.AddPortalContent + Products.CMFPlone.Portal.PloneSite + plone.app.contenttypes.schema:folder.xml + + + + + + + + + + + + + + + + string:${folder_url}/addPlone Site + listing_view + False + view + + + + + + + + + + - + + + + + diff --git a/Products/CMFPlone/skins/plone_templates/index_html.pt b/Products/CMFPlone/skins/plone_templates/index_html.pt deleted file mode 100644 index bceba801f3..0000000000 --- a/Products/CMFPlone/skins/plone_templates/index_html.pt +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - -

Welcome to - Portal title -

- -

- Portal description -

- -
- - - diff --git a/Products/CMFPlone/skins/plone_templates/index_html.pt.metadata b/Products/CMFPlone/skins/plone_templates/index_html.pt.metadata deleted file mode 100644 index 6660362fbc..0000000000 --- a/Products/CMFPlone/skins/plone_templates/index_html.pt.metadata +++ /dev/null @@ -1,2 +0,0 @@ -[default] -title=Default frontpage diff --git a/Products/CMFPlone/tests/testActionsTool.py b/Products/CMFPlone/tests/testActionsTool.py index 1b304e438b..8bcaed7d3a 100644 --- a/Products/CMFPlone/tests/testActionsTool.py +++ b/Products/CMFPlone/tests/testActionsTool.py @@ -64,7 +64,7 @@ def testMissingActionProvider(self): self.fail_tb('Should not bomb out if a provider is missing') def testBrokenActionProvider(self): - self.portal.portal_types = None + self.portal.portal_types = self.portal.portal_catalog try: self.actions.listFilteredActionsFor(self.portal) except: diff --git a/Products/CMFPlone/tests/testCatalogTool.py b/Products/CMFPlone/tests/testCatalogTool.py index 87cb05456b..2dd3fe05f2 100644 --- a/Products/CMFPlone/tests/testCatalogTool.py +++ b/Products/CMFPlone/tests/testCatalogTool.py @@ -31,7 +31,7 @@ group2 = 'g2' base_content = ['Members', 'aggregator', 'aggregator', - 'events', 'news', TEST_USER_ID, 'front-page', 'doc'] + 'events', 'news', TEST_USER_ID, 'doc'] class TestCatalogSetup(PloneTestCase): diff --git a/Products/CMFPlone/tests/testPloneFolder.py b/Products/CMFPlone/tests/testPloneFolder.py index 3ccbc81431..757b6129f6 100644 --- a/Products/CMFPlone/tests/testPloneFolder.py +++ b/Products/CMFPlone/tests/testPloneFolder.py @@ -3,7 +3,7 @@ from Products.CMFPlone.utils import _createObjectByType -from AccessControl import Unauthorized +from zExceptions import Unauthorized from Products.CMFCore.permissions import DeleteObjects from zExceptions import BadRequest diff --git a/Products/CMFPlone/tests/testPortalCreation.py b/Products/CMFPlone/tests/testPortalCreation.py index fca5d5358a..327f1f4bdd 100644 --- a/Products/CMFPlone/tests/testPortalCreation.py +++ b/Products/CMFPlone/tests/testPortalCreation.py @@ -419,18 +419,16 @@ def testTTWLockableProperty(self): self.assertEqual(True, registry['plone.lock_on_ttw_edit']) def testPortalFTIIsDynamicFTI(self): - # Plone Site FTI should be a DynamicView FTI + # Plone Site FTI should be a Dexterity FTI fti = self.portal.getTypeInfo() - self.assertEqual( - fti.meta_type, 'Factory-based Type Information with dynamic views' - ) + self.assertEqual(fti.meta_type, 'Dexterity FTI') def testPloneSiteFTIHasMethodAliases(self): # Should add method aliases to the Plone Site FTI expected_aliases = { '(Default)': '(dynamic view)', 'view': '(selected layout)', - 'edit': '@@site-controlpanel', + 'edit': '@@edit', 'sharing': '@@sharing', } fti = self.portal.getTypeInfo() @@ -532,13 +530,6 @@ def testSyndicationTabDisabled(self): "Actions tool still has visible 'syndication' action" ) - def testObjectButtonActionsInvisibleOnPortalRoot(self): - # only a manager would have proper permissions - self.setRoles(['Manager', 'Member']) - acts = self.actions.listFilteredActionsFor(self.portal) - buttons = acts.get('object_buttons', []) - self.assertEqual(0, len(buttons)) - def testObjectButtonActionsInvisibleOnPortalDefaultDocument(self): # only a manager would have proper permissions self.setRoles(['Manager', 'Member']) @@ -1023,8 +1014,11 @@ def test_addsite_en_as_nl(self): self.request.form['default_language'] = 'en' self.addsite() plonesite = self.app.plonesite1 - fp = plonesite['front-page'] # Unfortunately, the next test passes even without the fix (overriding # HTTP_ACCEPT_LANGUAGE on the request in factory.py). This seems to be # because translations are not available in the tests. - self.assertIn('Learn more about Plone', fp.text.raw) + self.assertIn('Learn more about Plone', plonesite.text.raw) + + # XXX maybe it is better to reset the sire in the @@plone-addsite view + # or somewhere else? + setSite(None) diff --git a/Products/CMFPlone/tests/testWebDAV.py b/Products/CMFPlone/tests/testWebDAV.py index 63ca592faa..7ab89b0644 100644 --- a/Products/CMFPlone/tests/testWebDAV.py +++ b/Products/CMFPlone/tests/testWebDAV.py @@ -303,8 +303,6 @@ def testPUTIndexHtml(self): def testPUTIndexHtmlIntoPortal(self): # Create an index_html document in the portal via FTP/DAV self.assertFalse('index_html' in self.portal) - self.assertEqual(self.portal.index_html.meta_type, - 'Filesystem Page Template') self.setRoles(['Manager']) response = self.publish( diff --git a/Products/CMFPlone/tests/test_safe_formatter.py b/Products/CMFPlone/tests/test_safe_formatter.py index bb90285cdd..25a6e687e3 100644 --- a/Products/CMFPlone/tests/test_safe_formatter.py +++ b/Products/CMFPlone/tests/test_safe_formatter.py @@ -118,7 +118,7 @@ def test_cook_zope2_page_templates_good_format_attr_str(self): hack_pt(pt, self.portal) self.assertEqual( pt.pt_render().strip(), - '

title of <PloneSite at plone> is Plone site

') + '

title of <PloneSite at plone> is Welcome to Plone

') def test_cook_zope2_page_templates_good_format_attr_unicode(self): from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate @@ -126,7 +126,7 @@ def test_cook_zope2_page_templates_good_format_attr_unicode(self): hack_pt(pt, self.portal) self.assertEqual( pt.pt_render().strip(), - '

title of <PloneSite at plone> is Plone site

') + '

title of <PloneSite at plone> is Welcome to Plone

') def test_access_to_private_content_not_allowed_via_rich_text(self): try: diff --git a/news/2454.breaking b/news/2454.breaking new file mode 100644 index 0000000000..2dad0ff727 --- /dev/null +++ b/news/2454.breaking @@ -0,0 +1,3 @@ +Use Dexterity for the Plone Site root object. +This is `PLIP 2454 `_. +[jaroel, ale-rt]