vendor/CMF/1.6.3/CMFCore

changeset 0:587011552858 CMFCore

import CMF 1.6.3
author bdelbosc
date Mon, 23 Apr 2007 13:58:01 +0000
parents
children 1babb9d61518
files ActionInformation.py ActionProviderBase.py ActionsTool.py CMFBTreeFolder.py CMFCatalogAware.py CMFCorePermissions.py CachingPolicyManager.py CatalogTool.py ContentTypeRegistry.py CookieCrumbler.py DEPENDENCIES.txt DirectoryView.py DiscussionTool.py DynamicType.py Expression.py Extensions/TestRecord.py FSDTMLMethod.py FSFile.py FSImage.py FSMetadata.py FSObject.py FSPageTemplate.py FSPropertiesObject.py FSPythonScript.py FSSTXMethod.py FSZSQLMethod.py MemberDataTool.py MembershipTool.py PortalContent.py PortalFolder.py PortalObject.py README.txt RegistrationTool.py Skinnable.py SkinsContainer.py SkinsTool.py TypesTool.py URLTool.py UndoTool.py WorkflowCore.py WorkflowTool.py __init__.py browser/__init__.py browser/configure.zcml browser/typeinfo.py configure.zcml dtml/addCC.dtml dtml/addFSDirView.dtml dtml/addInstance.dtml dtml/addTypeInfo.dtml dtml/addWorkflow.dtml dtml/cachingPolicies.dtml dtml/catalogFind.dtml dtml/configureRegistrationTool.dtml dtml/custdtml.dtml dtml/custfile.dtml dtml/custimage.dtml dtml/custprops.dtml dtml/custpt.dtml dtml/custpy.dtml dtml/custstx.dtml dtml/custzsql.dtml dtml/dirview_properties.dtml dtml/editToolsActions.dtml dtml/explainActionsTool.dtml dtml/explainCatalogTool.dtml dtml/explainDiscussionTool.dtml dtml/explainMemberDataTool.dtml dtml/explainMembershipTool.dtml dtml/explainRegistrationTool.dtml dtml/explainSkinsTool.dtml dtml/explainTypesTool.dtml dtml/explainURLTool.dtml dtml/explainUndoTool.dtml dtml/explainWorkflowTool.dtml dtml/extensionWidget.dtml dtml/findForm.dtml dtml/findResult.dtml dtml/majorMinorWidget.dtml dtml/manageActionProviders.dtml dtml/memberdataContents.dtml dtml/membershipRolemapping.dtml dtml/mimetypePredEdit.dtml dtml/patternWidget.dtml dtml/registryPredList.dtml dtml/registryTest.dtml dtml/selectWorkflows.dtml dtml/skinProps.dtml dtml/zmi_workflows.dtml exceptions.py exportimport/__init__.py exportimport/actions.py exportimport/cachingpolicymgr.py exportimport/catalog.py exportimport/configure.zcml exportimport/content.py exportimport/contenttyperegistry.py exportimport/cookieauth.py exportimport/mailhost.py exportimport/properties.py exportimport/skins.py exportimport/tests/__init__.py exportimport/tests/conformance.py exportimport/tests/four/placeholder.txt exportimport/tests/one/placeholder.txt exportimport/tests/test_actions.py exportimport/tests/test_cachingpolicymgr.py exportimport/tests/test_catalog.py exportimport/tests/test_content.py exportimport/tests/test_contenttyperegistry.py exportimport/tests/test_cookieauth.py exportimport/tests/test_mailhost.py exportimport/tests/test_properties.py exportimport/tests/test_skins.py exportimport/tests/test_typeinfo.py exportimport/tests/test_workflow.py exportimport/tests/three/placeholder.txt exportimport/tests/two/placeholder.txt exportimport/typeinfo.py exportimport/workflow.py folderAdd.dtml help/Actions.stx help/CPMPolicies.stx images/cookie.gif images/dirview.gif images/fsdtml.gif images/fsfile.gif images/fsimage.gif images/fsprops.gif images/fspt.gif images/fspy.gif images/fssqlmethod.gif images/registry.gif images/typeinfo.gif implements.zcml interfaces/CachingPolicyManager.py interfaces/ContentTypeRegistry.py interfaces/Contentish.py interfaces/Discussions.py interfaces/DublinCore.py interfaces/Dynamic.py interfaces/Folderish.py interfaces/IOpaqueItems.py interfaces/Syndicatable.py interfaces/__init__.py interfaces/_content.py interfaces/_tools.py interfaces/portal_actions.py interfaces/portal_catalog.py interfaces/portal_discussion.py interfaces/portal_memberdata.py interfaces/portal_membership.py interfaces/portal_metadata.py interfaces/portal_properties.py interfaces/portal_registration.py interfaces/portal_skins.py interfaces/portal_types.py interfaces/portal_undo.py interfaces/portal_url.py interfaces/portal_workflow.py permissions.py tests/__init__.py tests/base/__init__.py tests/base/content.py tests/base/dummy.py tests/base/security.py tests/base/testcase.py tests/base/tidata.py tests/base/utils.py tests/fake_skins/fake_skin/#test1.py tests/fake_skins/fake_skin/.test1.py tests/fake_skins/fake_skin/test1.py tests/fake_skins/fake_skin/test4.py tests/fake_skins/fake_skin/test4.py.security tests/fake_skins/fake_skin/test5.py tests/fake_skins/fake_skin/test6.py tests/fake_skins/fake_skin/test6.py.metadata tests/fake_skins/fake_skin/testDTML.dtml tests/fake_skins/fake_skin/testDTML.dtml.metadata tests/fake_skins/fake_skin/testPT.pt tests/fake_skins/fake_skin/testPT.pt.properties tests/fake_skins/fake_skin/testPT2.pt tests/fake_skins/fake_skin/testPT2.pt.metadata tests/fake_skins/fake_skin/testPT_multiline_python_dos.pt tests/fake_skins/fake_skin/testPT_multiline_python_mac.pt tests/fake_skins/fake_skin/testPT_multiline_python_unix.pt tests/fake_skins/fake_skin/testPT_utf8.pt tests/fake_skins/fake_skin/testPTbad.pt tests/fake_skins/fake_skin/testUtf8.js tests/fake_skins/fake_skin/testXMLPT.pt tests/fake_skins/fake_skin/test_dos.py tests/fake_skins/fake_skin/test_dtml.dtml tests/fake_skins/fake_skin/test_dtml.dtml.metadata tests/fake_skins/fake_skin/test_file.swf tests/fake_skins/fake_skin/test_file.swf.metadata tests/fake_skins/fake_skin/test_file_two.swf tests/fake_skins/fake_skin/test_file_two.swf.metadata tests/fake_skins/fake_skin/test_image.gif tests/fake_skins/fake_skin/test_image.gif.metadata tests/fake_skins/fake_skin/test_mac.py tests/fake_skins/fake_skin/test_manual_ignore.py tests/fake_skins/fake_skin/test_props.props tests/fake_skins/fake_skin/test_unix.py tests/fake_skins/fake_skin/test_warn.py tests/fake_skins/fake_skin/testsql.zsql tests/testCookieCrumbler.py tests/test_ActionInformation.py tests/test_ActionProviderBase.py tests/test_ActionsTool.py tests/test_CMFBTreeFolder.py tests/test_CMFCatalogAware.py tests/test_CachingPolicyManager.py tests/test_CatalogTool.py tests/test_ContentTypeRegistry.py tests/test_DirectoryView.py tests/test_DiscussionTool.py tests/test_DynamicType.py tests/test_Expression.py tests/test_FSDTMLMethod.py tests/test_FSFile.py tests/test_FSImage.py tests/test_FSMetadata.py tests/test_FSPageTemplate.py tests/test_FSPropertiesObject.py tests/test_FSPythonScript.py tests/test_FSSecurity.py tests/test_FSZSQLMethod.py tests/test_MemberDataTool.py tests/test_MembershipTool.py tests/test_OpaqueItems.py tests/test_PortalContent.py tests/test_PortalFolder.py tests/test_RegistrationTool.py tests/test_SkinsTool.py tests/test_TypesTool.py tests/test_URLTool.py tests/test_UndoTool.py tests/test_WorkflowTool.py tests/test_utils.py tool.gif utils.py version.txt www/typeinfoAliases.zpt www/typesAliases.zpt
diffstat 244 files changed, 35362 insertions(+), 0 deletions(-) [+]
line diff
     1.1 new file mode 100644
     1.2 --- /dev/null
     1.3 +++ b/ActionInformation.py
     1.4 @@ -0,0 +1,400 @@
     1.5 +##############################################################################
     1.6 +#
     1.7 +# Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved.
     1.8 +#
     1.9 +# This software is subject to the provisions of the Zope Public License,
    1.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    1.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    1.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    1.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    1.14 +# FOR A PARTICULAR PURPOSE.
    1.15 +#
    1.16 +##############################################################################
    1.17 +""" Information about customizable actions.
    1.18 +
    1.19 +$Id$
    1.20 +"""
    1.21 +
    1.22 +from UserDict import UserDict
    1.23 +
    1.24 +from AccessControl import ClassSecurityInfo
    1.25 +from Acquisition import aq_base, aq_inner, aq_parent
    1.26 +from Globals import InitializeClass
    1.27 +from OFS.SimpleItem import SimpleItem
    1.28 +
    1.29 +from Expression import Expression
    1.30 +from interfaces.portal_actions import ActionInfo as IActionInfo
    1.31 +from permissions import View
    1.32 +from utils import _checkPermission
    1.33 +from utils import getToolByName
    1.34 +
    1.35 +
    1.36 +_unchanged = [] # marker
    1.37 +
    1.38 +class ActionInfo(UserDict):
    1.39 +    """ A lazy dictionary for Action infos.
    1.40 +    """
    1.41 +
    1.42 +    __implements__ = IActionInfo
    1.43 +
    1.44 +    __allow_access_to_unprotected_subobjects__ = 1
    1.45 +
    1.46 +    def __init__(self, action, ec):
    1.47 +        lazy_keys = []
    1.48 +
    1.49 +        if isinstance(action, dict):
    1.50 +            UserDict.__init__(self, action)
    1.51 +            self.data.setdefault( 'id', self.data['name'].lower() )
    1.52 +            self.data.setdefault( 'title', self.data['name'] )
    1.53 +            self.data.setdefault( 'url', '' )
    1.54 +            self.data.setdefault( 'permissions', () )
    1.55 +            self.data.setdefault( 'category', 'object' )
    1.56 +            self.data.setdefault( 'visible', True )
    1.57 +            self.data['available'] = True
    1.58 +
    1.59 +        else:
    1.60 +            self._action = action
    1.61 +            UserDict.__init__( self, action.getMapping() )
    1.62 +            self.data['name'] = self.data['title']
    1.63 +            del self.data['description']
    1.64 +
    1.65 +            if self.data['action']:
    1.66 +                self.data['url'] = self._getURL
    1.67 +                lazy_keys.append('url')
    1.68 +            else:
    1.69 +                self.data['url'] = ''
    1.70 +            del self.data['action']
    1.71 +
    1.72 +            if self.data['condition']:
    1.73 +                self.data['available'] = self._checkCondition
    1.74 +                lazy_keys.append('available')
    1.75 +            else:
    1.76 +                self.data['available'] = True
    1.77 +            del self.data['condition']
    1.78 +
    1.79 +        if self.data['permissions']:
    1.80 +            self.data['allowed'] = self._checkPermissions
    1.81 +            lazy_keys.append('allowed')
    1.82 +        else:
    1.83 +            self.data['allowed'] = True
    1.84 +
    1.85 +        self._ec = ec
    1.86 +        self._lazy_keys = lazy_keys
    1.87 +
    1.88 +    def __getitem__(self, key):
    1.89 +        value = UserDict.__getitem__(self, key)
    1.90 +        if key in self._lazy_keys:
    1.91 +            value = self.data[key] = value()
    1.92 +            self._lazy_keys.remove(key)
    1.93 +        return value
    1.94 +
    1.95 +    def __eq__(self, other):
    1.96 +        # this is expensive, use it with care
    1.97 +        [ self.__getitem__(key) for key in self._lazy_keys ]
    1.98 +
    1.99 +        if isinstance(other, self.__class__):
   1.100 +            [ other[key] for key in other._lazy_keys ]
   1.101 +            return self.data == other.data
   1.102 +        elif isinstance(other, UserDict):
   1.103 +            return self.data == other.data
   1.104 +        else:
   1.105 +            return self.data == other
   1.106 +
   1.107 +    def copy(self):
   1.108 +        c = UserDict.copy(self)
   1.109 +        c._lazy_keys = self._lazy_keys[:]
   1.110 +        return c
   1.111 +
   1.112 +    def _getURL(self):
   1.113 +        """ Get the result of the URL expression in the current context.
   1.114 +        """
   1.115 +        return self._action._getActionObject()(self._ec)
   1.116 +
   1.117 +    def _checkCondition(self):
   1.118 +        """ Check condition expression in the current context.
   1.119 +        """
   1.120 +        return self._action.testCondition(self._ec)
   1.121 +
   1.122 +    def _checkPermissions(self):
   1.123 +        """ Check permissions in the current context.
   1.124 +        """
   1.125 +        category = self['category']
   1.126 +        object = self._ec.contexts['object']
   1.127 +        if object is not None and ( category.startswith('object') or
   1.128 +                                    category.startswith('workflow') or
   1.129 +                                    category.startswith('document') ):
   1.130 +            context = object
   1.131 +        else:
   1.132 +            folder = self._ec.contexts['folder']
   1.133 +            if folder is not None and category.startswith('folder'):
   1.134 +                context = folder
   1.135 +            else:
   1.136 +                context = self._ec.contexts['portal']
   1.137 +
   1.138 +        for permission in self['permissions']:
   1.139 +            if _checkPermission(permission, context):
   1.140 +                return True
   1.141 +        return False
   1.142 +
   1.143 +
   1.144 +class ActionInformation( SimpleItem ):
   1.145 +
   1.146 +    """ Represent a single selectable action.
   1.147 +
   1.148 +    Actions generate links to views of content, or to specific methods
   1.149 +    of the site.  They can be filtered via their conditions.
   1.150 +    """
   1.151 +    _isActionInformation = 1
   1.152 +    __allow_access_to_unprotected_subobjects__ = 1
   1.153 +
   1.154 +    security = ClassSecurityInfo()
   1.155 +
   1.156 +    def __init__( self
   1.157 +                , id
   1.158 +                , title=''
   1.159 +                , description=''
   1.160 +                , category='object'
   1.161 +                , condition=''
   1.162 +                , permissions=()
   1.163 +                , priority=10
   1.164 +                , visible=True
   1.165 +                , action=''
   1.166 +                ):
   1.167 +        """ Set up an instance.
   1.168 +        """
   1.169 +        self.edit( id
   1.170 +                 , title
   1.171 +                 , description
   1.172 +                 , category
   1.173 +                 , condition
   1.174 +                 , permissions
   1.175 +                 , priority
   1.176 +                 , visible
   1.177 +                 , action
   1.178 +                 )
   1.179 +
   1.180 +    security.declarePrivate('edit')
   1.181 +    def edit( self
   1.182 +            , id=_unchanged
   1.183 +            , title=_unchanged
   1.184 +            , description=_unchanged
   1.185 +            , category=_unchanged
   1.186 +            , condition=_unchanged
   1.187 +            , permissions=_unchanged
   1.188 +            , priority=_unchanged
   1.189 +            , visible=_unchanged
   1.190 +            , action=_unchanged
   1.191 +            ):
   1.192 +        """Edit the specified properties.
   1.193 +        """
   1.194 +
   1.195 +        if id is not _unchanged:
   1.196 +            self.id = id
   1.197 +        if title is not _unchanged:
   1.198 +            self.title = title
   1.199 +        if description is not _unchanged:
   1.200 +            self.description = description
   1.201 +        if category is not _unchanged:
   1.202 +            self.category = category
   1.203 +        if condition is not _unchanged:
   1.204 +            if condition and isinstance(condition, basestring):
   1.205 +                condition = Expression(condition)
   1.206 +            self.condition = condition
   1.207 +        if permissions is not _unchanged:
   1.208 +            if permissions == ('',):
   1.209 +                permissions = ()
   1.210 +            self.permissions = permissions
   1.211 +        if priority is not _unchanged:
   1.212 +            self.priority = priority
   1.213 +        if visible is not _unchanged:
   1.214 +            self.visible = visible
   1.215 +        if action is not _unchanged:
   1.216 +            if action and isinstance(action, basestring):
   1.217 +                action = Expression(action)
   1.218 +            self.setActionExpression(action)
   1.219 +
   1.220 +    security.declareProtected( View, 'Title' )
   1.221 +    def Title(self):
   1.222 +
   1.223 +        """ Return the Action title.
   1.224 +        """
   1.225 +        return self.title or self.getId()
   1.226 +
   1.227 +    security.declareProtected( View, 'Description' )
   1.228 +    def Description( self ):
   1.229 +
   1.230 +        """ Return a description of the action.
   1.231 +        """
   1.232 +        return self.description
   1.233 +
   1.234 +    security.declarePrivate( 'testCondition' )
   1.235 +    def testCondition( self, ec ):
   1.236 +
   1.237 +        """ Evaluate condition using context, 'ec', and return 0 or 1.
   1.238 +        """
   1.239 +        if self.condition:
   1.240 +            return bool( self.condition(ec) )
   1.241 +        else:
   1.242 +            return True
   1.243 +
   1.244 +    security.declarePublic( 'getAction' )
   1.245 +    def getAction( self, ec ):
   1.246 +
   1.247 +        """ Compute the action using context, 'ec'; return a mapping of
   1.248 +            info about the action.
   1.249 +        """
   1.250 +        return ActionInfo(self, ec)
   1.251 +
   1.252 +    security.declarePrivate( '_getActionObject' )
   1.253 +    def _getActionObject( self ):
   1.254 +
   1.255 +        """ Find the action object, working around name changes.
   1.256 +        """
   1.257 +        action = getattr( self, 'action', None )
   1.258 +
   1.259 +        if action is None:  # Forward compatibility, used to be '_action'
   1.260 +            action = getattr( self, '_action', None )
   1.261 +            if action is not None:
   1.262 +                self.action = self._action
   1.263 +                del self._action
   1.264 +
   1.265 +        return action
   1.266 +
   1.267 +    security.declarePublic( 'getActionExpression' )
   1.268 +    def getActionExpression( self ):
   1.269 +
   1.270 +        """ Return the text of the TALES expression for our URL.
   1.271 +        """
   1.272 +        action = self._getActionObject()
   1.273 +        expr = action and action.text or ''
   1.274 +        if expr and isinstance(expr, basestring):
   1.275 +            if ( not expr.startswith('string:')
   1.276 +                 and not expr.startswith('python:') ):
   1.277 +                expr = 'string:${object_url}/%s' % expr
   1.278 +                self.action = Expression( expr )
   1.279 +        return expr
   1.280 +
   1.281 +    security.declarePrivate( 'setActionExpression' )
   1.282 +    def setActionExpression(self, action):
   1.283 +        if action and isinstance(action, basestring):
   1.284 +            if ( not action.startswith('string:')
   1.285 +                 and not action.startswith('python:') ):
   1.286 +                action = 'string:${object_url}/%s' % action
   1.287 +            action = Expression( action )
   1.288 +        self.action = action
   1.289 +
   1.290 +    security.declarePublic( 'getCondition' )
   1.291 +    def getCondition(self):
   1.292 +
   1.293 +        """ Return the text of the TALES expression for our condition.
   1.294 +        """
   1.295 +        return getattr( self, 'condition', None ) and self.condition.text or ''
   1.296 +
   1.297 +    security.declarePublic( 'getPermissions' )
   1.298 +    def getPermissions( self ):
   1.299 +
   1.300 +        """ Return the permission, if any, required to execute the action.
   1.301 +
   1.302 +        Return an empty tuple if no permission is required.
   1.303 +        """
   1.304 +        return self.permissions
   1.305 +
   1.306 +    security.declarePublic( 'getCategory' )
   1.307 +    def getCategory( self ):
   1.308 +
   1.309 +        """ Return the category in which the action should be grouped.
   1.310 +        """
   1.311 +        return self.category or 'object'
   1.312 +
   1.313 +    security.declarePublic( 'getVisibility' )
   1.314 +    def getVisibility( self ):
   1.315 +
   1.316 +        """ Return whether the action should be visible in the CMF UI.
   1.317 +        """
   1.318 +        return bool(self.visible)
   1.319 +
   1.320 +    security.declarePrivate('getMapping')
   1.321 +    def getMapping(self):
   1.322 +        """ Get a mapping of this object's data.
   1.323 +        """
   1.324 +        return { 'id': self.id,
   1.325 +                 'title': self.title or self.id,
   1.326 +                 'description': self.description,
   1.327 +                 'category': self.category or 'object',
   1.328 +                 'condition': getattr(self, 'condition', None)
   1.329 +                              and self.condition.text or '',
   1.330 +                 'permissions': self.permissions,
   1.331 +                 'visible': bool(self.visible),
   1.332 +                 'action': self.getActionExpression() }
   1.333 +
   1.334 +    security.declarePrivate('clone')
   1.335 +    def clone( self ):
   1.336 +        """ Get a newly-created AI just like us.
   1.337 +        """
   1.338 +        return self.__class__( priority=self.priority, **self.getMapping() )
   1.339 +
   1.340 +InitializeClass( ActionInformation )
   1.341 +
   1.342 +
   1.343 +def getOAI(context, object=None):
   1.344 +    request = getattr(context, 'REQUEST', None)
   1.345 +    if request:
   1.346 +        cache = request.get('_oai_cache', None)
   1.347 +        if cache is None:
   1.348 +            request['_oai_cache'] = cache = {}
   1.349 +        info = cache.get( id(object), None )
   1.350 +    else:
   1.351 +        info = None
   1.352 +    if info is None:
   1.353 +        if object is None or not hasattr(object, 'aq_base'):
   1.354 +            folder = None
   1.355 +        else:
   1.356 +            folder = object
   1.357 +            # Search up the containment hierarchy until we find an
   1.358 +            # object that claims it's a folder.
   1.359 +            while folder is not None:
   1.360 +                if getattr(aq_base(folder), 'isPrincipiaFolderish', 0):
   1.361 +                    # found it.
   1.362 +                    break
   1.363 +                else:
   1.364 +                    folder = aq_parent(aq_inner(folder))
   1.365 +        info = oai(context, folder, object)
   1.366 +        if request:
   1.367 +            cache[ id(object) ] = info
   1.368 +    return info
   1.369 +
   1.370 +
   1.371 +class oai:
   1.372 +    #Provided for backwards compatability
   1.373 +    # Provides information that may be needed when constructing the list of
   1.374 +    # available actions.
   1.375 +    __allow_access_to_unprotected_subobjects__ = 1
   1.376 +
   1.377 +    def __init__( self, tool, folder, object=None ):
   1.378 +        self.portal = portal = aq_parent(aq_inner(tool))
   1.379 +        membership = getToolByName(tool, 'portal_membership')
   1.380 +        self.isAnonymous = membership.isAnonymousUser()
   1.381 +        self.user_id = membership.getAuthenticatedMember().getId()
   1.382 +        self.portal_url = portal.absolute_url()
   1.383 +        if folder is not None:
   1.384 +            self.folder_url = folder.absolute_url()
   1.385 +            self.folder = folder
   1.386 +        else:
   1.387 +            self.folder_url = self.portal_url
   1.388 +            self.folder = portal
   1.389 +
   1.390 +        # The name "content" is deprecated and will go away in CMF 2.0!    
   1.391 +        self.object = self.content = object
   1.392 +        if object is not None:
   1.393 +            self.object_url = self.content_url = object.absolute_url()
   1.394 +        else:
   1.395 +            self.object_url = self.content_url = None
   1.396 +
   1.397 +    def __getitem__(self, name):
   1.398 +        # Mapping interface for easy string formatting.
   1.399 +        if name[:1] == '_':
   1.400 +            raise KeyError, name
   1.401 +        if hasattr(self, name):
   1.402 +            return getattr(self, name)
   1.403 +        raise KeyError, name
   1.404 +
     2.1 new file mode 100644
     2.2 --- /dev/null
     2.3 +++ b/ActionProviderBase.py
     2.4 @@ -0,0 +1,339 @@
     2.5 +##############################################################################
     2.6 +#
     2.7 +# Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved.
     2.8 +#
     2.9 +# This software is subject to the provisions of the Zope Public License,
    2.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    2.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    2.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    2.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    2.14 +# FOR A PARTICULAR PURPOSE.
    2.15 +#
    2.16 +##############################################################################
    2.17 +""" Implement a shared base for tools which provide actions.
    2.18 +
    2.19 +$Id$
    2.20 +"""
    2.21 +
    2.22 +from AccessControl import ClassSecurityInfo
    2.23 +from Globals import DTMLFile
    2.24 +from Globals import InitializeClass
    2.25 +
    2.26 +from ActionInformation import ActionInfo
    2.27 +from ActionInformation import ActionInformation
    2.28 +from ActionInformation import getOAI
    2.29 +from exceptions import AccessControl_Unauthorized
    2.30 +from Expression import getExprContext
    2.31 +from interfaces.portal_actions import ActionProvider as IActionProvider
    2.32 +from permissions import ManagePortal
    2.33 +from utils import _dtmldir
    2.34 +
    2.35 +
    2.36 +class ActionProviderBase:
    2.37 +    """ Provide ActionTabs and management methods for ActionProviders
    2.38 +    """
    2.39 +
    2.40 +    __implements__ = IActionProvider
    2.41 +
    2.42 +    security = ClassSecurityInfo()
    2.43 +
    2.44 +    _actions = ()
    2.45 +
    2.46 +    _actions_form = DTMLFile( 'editToolsActions', _dtmldir )
    2.47 +
    2.48 +    manage_options = ( { 'label'  : 'Actions'
    2.49 +                       , 'action' : 'manage_editActionsForm'
    2.50 +                       , 'help'   : ('CMFCore', 'Actions.stx')
    2.51 +                       }
    2.52 +                     ,
    2.53 +                     )
    2.54 +
    2.55 +    #
    2.56 +    #   ActionProvider interface
    2.57 +    #
    2.58 +    security.declarePrivate('listActions')
    2.59 +    def listActions(self, info=None, object=None):
    2.60 +        """ List all the actions defined by a provider.
    2.61 +        """
    2.62 +        return self._actions or ()
    2.63 +
    2.64 +    security.declarePrivate('getActionObject')
    2.65 +    def getActionObject(self, action):
    2.66 +        """Return the actions object or None if action doesn't exist.
    2.67 +        """
    2.68 +        # separate cataegory and id from action
    2.69 +        sep = action.rfind('/')
    2.70 +        if sep == -1:
    2.71 +            raise ValueError('Actions must have the format <category>/<id>.')
    2.72 +        category, id = action[:sep], action[sep+1:]
    2.73 +
    2.74 +        # search for action and return first one found
    2.75 +        for ai in self.listActions():
    2.76 +            if id == ai.getId() and category == ai.getCategory():
    2.77 +                return ai
    2.78 +
    2.79 +        # no action found
    2.80 +        return None
    2.81 +
    2.82 +    security.declarePublic('listActionInfos')
    2.83 +    def listActionInfos(self, action_chain=None, object=None,
    2.84 +                        check_visibility=1, check_permissions=1,
    2.85 +                        check_condition=1, max=-1):
    2.86 +        # List ActionInfo objects.
    2.87 +        # (method is without docstring to disable publishing)
    2.88 +        #
    2.89 +        ec = self._getExprContext(object)
    2.90 +        actions = self.listActions(object=object)
    2.91 +        actions = [ ActionInfo(action, ec) for action in actions ]
    2.92 +
    2.93 +        if action_chain:
    2.94 +            filtered_actions = []
    2.95 +            if isinstance(action_chain, basestring):
    2.96 +                action_chain = (action_chain,)
    2.97 +            for action_ident in action_chain:
    2.98 +                sep = action_ident.rfind('/')
    2.99 +                category, id = action_ident[:sep], action_ident[sep+1:]
   2.100 +                for ai in actions:
   2.101 +                    if id == ai['id'] and category == ai['category']:
   2.102 +                        filtered_actions.append(ai)
   2.103 +            actions = filtered_actions
   2.104 +
   2.105 +        action_infos = []
   2.106 +        for ai in actions:
   2.107 +            if check_visibility and not ai['visible']:
   2.108 +                continue
   2.109 +            if check_permissions and not ai['allowed']:
   2.110 +                continue
   2.111 +            if check_condition and not ai['available']:
   2.112 +                continue
   2.113 +            action_infos.append(ai)
   2.114 +            if max + 1 and len(action_infos) >= max:
   2.115 +                break
   2.116 +        return action_infos
   2.117 +
   2.118 +    security.declarePublic('getActionInfo')
   2.119 +    def getActionInfo(self, action_chain, object=None, check_visibility=0,
   2.120 +                      check_condition=0):
   2.121 +        """ Get an ActionInfo object specified by a chain of actions.
   2.122 +        """
   2.123 +        action_infos = self.listActionInfos(action_chain, object,
   2.124 +                                            check_visibility=check_visibility,
   2.125 +                                            check_permissions=False,
   2.126 +                                            check_condition=check_condition)
   2.127 +        if not action_infos:
   2.128 +            if object is None:
   2.129 +                provider = self
   2.130 +            else:
   2.131 +                provider = object
   2.132 +            msg = 'Action "%s" not available for %s' % (
   2.133 +                        action_chain, '/'.join(provider.getPhysicalPath()))
   2.134 +            raise ValueError(msg)
   2.135 +        for ai in action_infos:
   2.136 +            if ai['allowed']:
   2.137 +                return ai
   2.138 +        raise AccessControl_Unauthorized('You are not allowed to access any '
   2.139 +                                         'of the specified Actions.')
   2.140 +
   2.141 +    #
   2.142 +    #   ZMI methods
   2.143 +    #
   2.144 +    security.declareProtected( ManagePortal, 'manage_editActionsForm' )
   2.145 +    def manage_editActionsForm( self, REQUEST, manage_tabs_message=None ):
   2.146 +
   2.147 +        """ Show the 'Actions' management tab.
   2.148 +        """
   2.149 +        actions = [ ai.getMapping() for ai in self.listActions() ]
   2.150 +
   2.151 +        # possible_permissions is in AccessControl.Role.RoleManager.
   2.152 +        pp = self.possible_permissions()
   2.153 +        return self._actions_form( self
   2.154 +                                 , REQUEST
   2.155 +                                 , actions=actions
   2.156 +                                 , possible_permissions=pp
   2.157 +                                 , management_view='Actions'
   2.158 +                                 , manage_tabs_message=manage_tabs_message
   2.159 +                                 )
   2.160 +
   2.161 +    security.declareProtected( ManagePortal, 'addAction' )
   2.162 +    def addAction( self
   2.163 +                 , id
   2.164 +                 , name
   2.165 +                 , action
   2.166 +                 , condition
   2.167 +                 , permission
   2.168 +                 , category
   2.169 +                 , visible=1
   2.170 +                 , REQUEST=None
   2.171 +                 ):
   2.172 +        """ Add an action to our list.
   2.173 +        """
   2.174 +        if not name:
   2.175 +            raise ValueError('A name is required.')
   2.176 +
   2.177 +        action = action and str(action) or ''
   2.178 +        condition = condition and str(condition) or ''
   2.179 +
   2.180 +        if not isinstance(permission, tuple):
   2.181 +            permission = (str(permission),)
   2.182 +
   2.183 +        new_actions = self._cloneActions()
   2.184 +
   2.185 +        new_action = ActionInformation( id=str(id)
   2.186 +                                      , title=str(name)
   2.187 +                                      , category=str(category)
   2.188 +                                      , condition=condition
   2.189 +                                      , permissions=permission
   2.190 +                                      , visible=bool(visible)
   2.191 +                                      , action=action
   2.192 +                                      )
   2.193 +
   2.194 +        new_actions.append( new_action )
   2.195 +        self._actions = tuple( new_actions )
   2.196 +
   2.197 +        if REQUEST is not None:
   2.198 +            return self.manage_editActionsForm(
   2.199 +                REQUEST, manage_tabs_message='Added.')
   2.200 +
   2.201 +    security.declareProtected( ManagePortal, 'changeActions' )
   2.202 +    def changeActions( self, properties=None, REQUEST=None ):
   2.203 +
   2.204 +        """ Update our list of actions.
   2.205 +        """
   2.206 +        if properties is None:
   2.207 +            properties = REQUEST
   2.208 +
   2.209 +        actions = []
   2.210 +
   2.211 +        for index in range( len( self._actions ) ):
   2.212 +            actions.append( self._extractAction( properties, index ) )
   2.213 +
   2.214 +        self._actions = tuple( actions )
   2.215 +
   2.216 +        if REQUEST is not None:
   2.217 +            return self.manage_editActionsForm(REQUEST, manage_tabs_message=
   2.218 +                                               'Actions changed.')
   2.219 +
   2.220 +    security.declareProtected( ManagePortal, 'deleteActions' )
   2.221 +    def deleteActions( self, selections=(), REQUEST=None ):
   2.222 +
   2.223 +        """ Delete actions indicated by indexes in 'selections'.
   2.224 +        """
   2.225 +        sels = list( map( int, selections ) )  # Convert to a list of integers.
   2.226 +
   2.227 +        old_actions = self._cloneActions()
   2.228 +        new_actions = []
   2.229 +
   2.230 +        for index in range( len( old_actions ) ):
   2.231 +            if index not in sels:
   2.232 +                new_actions.append( old_actions[ index ] )
   2.233 +
   2.234 +        self._actions = tuple( new_actions )
   2.235 +
   2.236 +        if REQUEST is not None:
   2.237 +            return self.manage_editActionsForm(
   2.238 +                REQUEST, manage_tabs_message=(
   2.239 +                'Deleted %d action(s).' % len(sels)))
   2.240 +
   2.241 +    security.declareProtected( ManagePortal, 'moveUpActions' )
   2.242 +    def moveUpActions( self, selections=(), REQUEST=None ):
   2.243 +
   2.244 +        """ Move the specified actions up one slot in our list.
   2.245 +        """
   2.246 +        sels = list( map( int, selections ) )  # Convert to a list of integers.
   2.247 +        sels.sort()
   2.248 +
   2.249 +        new_actions = self._cloneActions()
   2.250 +
   2.251 +        for idx in sels:
   2.252 +            idx2 = idx - 1
   2.253 +            if idx2 < 0:
   2.254 +                # Wrap to the bottom.
   2.255 +                idx2 = len(new_actions) - 1
   2.256 +            # Swap.
   2.257 +            a = new_actions[idx2]
   2.258 +            new_actions[idx2] = new_actions[idx]
   2.259 +            new_actions[idx] = a
   2.260 +
   2.261 +        self._actions = tuple( new_actions )
   2.262 +
   2.263 +        if REQUEST is not None:
   2.264 +            return self.manage_editActionsForm(
   2.265 +                REQUEST, manage_tabs_message=(
   2.266 +                'Moved up %d action(s).' % len(sels)))
   2.267 +
   2.268 +    security.declareProtected( ManagePortal, 'moveDownActions' )
   2.269 +    def moveDownActions( self, selections=(), REQUEST=None ):
   2.270 +
   2.271 +        """ Move the specified actions down one slot in our list.
   2.272 +        """
   2.273 +        sels = list( map( int, selections ) )  # Convert to a list of integers.
   2.274 +        sels.sort()
   2.275 +        sels.reverse()
   2.276 +
   2.277 +        new_actions = self._cloneActions()
   2.278 +
   2.279 +        for idx in sels:
   2.280 +            idx2 = idx + 1
   2.281 +            if idx2 >= len(new_actions):
   2.282 +                # Wrap to the top.
   2.283 +                idx2 = 0
   2.284 +            # Swap.
   2.285 +            a = new_actions[idx2]
   2.286 +            new_actions[idx2] = new_actions[idx]
   2.287 +            new_actions[idx] = a
   2.288 +
   2.289 +        self._actions = tuple( new_actions )
   2.290 +
   2.291 +        if REQUEST is not None:
   2.292 +            return self.manage_editActionsForm(
   2.293 +                REQUEST, manage_tabs_message=(
   2.294 +                'Moved down %d action(s).' % len(sels)))
   2.295 +
   2.296 +    #
   2.297 +    #   Helper methods
   2.298 +    #
   2.299 +    security.declarePrivate( '_cloneActions' )
   2.300 +    def _cloneActions( self ):
   2.301 +
   2.302 +        """ Return a list of actions, cloned from our current list.
   2.303 +        """
   2.304 +        return map( lambda x: x.clone(), list( self._actions ) )
   2.305 +
   2.306 +    security.declarePrivate( '_extractAction' )
   2.307 +    def _extractAction( self, properties, index ):
   2.308 +
   2.309 +        """ Extract an ActionInformation from the funky form properties.
   2.310 +        """
   2.311 +        id          = str( properties.get( 'id_%d'          % index, '' ) )
   2.312 +        title       = str( properties.get( 'name_%d'        % index, '' ) )
   2.313 +        action      = str( properties.get( 'action_%d'      % index, '' ) )
   2.314 +        condition   = str( properties.get( 'condition_%d'   % index, '' ) )
   2.315 +        category    = str( properties.get( 'category_%d'    % index, '' ))
   2.316 +        visible     = bool( properties.get('visible_%d'     % index, False) )
   2.317 +        permissions =      properties.get( 'permission_%d'  % index, () )
   2.318 +
   2.319 +        if not title:
   2.320 +            raise ValueError('A title is required.')
   2.321 +
   2.322 +        if category == '':
   2.323 +            category = 'object'
   2.324 +
   2.325 +        if isinstance(permissions, basestring):
   2.326 +            permissions = ( permissions, )
   2.327 +
   2.328 +        return ActionInformation( id=id
   2.329 +                                , title=title
   2.330 +                                , action=action
   2.331 +                                , condition=condition
   2.332 +                                , permissions=permissions
   2.333 +                                , category=category
   2.334 +                                , visible=visible
   2.335 +                                )
   2.336 +
   2.337 +    def _getOAI(self, object):
   2.338 +        return getOAI(self, object)
   2.339 +
   2.340 +    def _getExprContext(self, object):
   2.341 +        return getExprContext(self, object)
   2.342 +
   2.343 +InitializeClass(ActionProviderBase)
     3.1 new file mode 100644
     3.2 --- /dev/null
     3.3 +++ b/ActionsTool.py
     3.4 @@ -0,0 +1,254 @@
     3.5 +##############################################################################
     3.6 +#
     3.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     3.8 +#
     3.9 +# This software is subject to the provisions of the Zope Public License,
    3.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    3.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    3.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    3.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    3.14 +# FOR A PARTICULAR PURPOSE.
    3.15 +#
    3.16 +##############################################################################
    3.17 +""" Basic action list tool.
    3.18 +
    3.19 +$Id$
    3.20 +"""
    3.21 +
    3.22 +from warnings import warn
    3.23 +
    3.24 +from AccessControl import ClassSecurityInfo
    3.25 +from Acquisition import aq_base
    3.26 +from Globals import DTMLFile
    3.27 +from Globals import InitializeClass
    3.28 +from OFS.Folder import Folder
    3.29 +
    3.30 +from ActionInformation import ActionInformation
    3.31 +from ActionProviderBase import ActionProviderBase
    3.32 +from Expression import Expression
    3.33 +from interfaces.portal_actions import ActionProvider as IActionProvider
    3.34 +from interfaces.portal_actions import portal_actions as IActionsTool
    3.35 +from permissions import ListFolderContents
    3.36 +from permissions import ManagePortal
    3.37 +from utils import _checkPermission
    3.38 +from utils import _dtmldir
    3.39 +from utils import SimpleItemWithProperties
    3.40 +from utils import UniqueObject
    3.41 +
    3.42 +
    3.43 +class ActionsTool(UniqueObject, Folder, ActionProviderBase):
    3.44 +    """
    3.45 +        Weave together the various sources of "actions" which are apropos
    3.46 +        to the current user and context.
    3.47 +    """
    3.48 +
    3.49 +    __implements__ = (IActionsTool, ActionProviderBase.__implements__)
    3.50 +
    3.51 +    id = 'portal_actions'
    3.52 +    meta_type = 'CMF Actions Tool'
    3.53 +    _actions = (ActionInformation(id='folderContents'
    3.54 +                                , title='Folder contents'
    3.55 +                                , action=Expression(
    3.56 +               text='string:${folder_url}/folder_contents')
    3.57 +                                , condition=Expression(
    3.58 +               text='python: folder is not object')
    3.59 +                                , permissions=(ListFolderContents,)
    3.60 +                                , category='folder'
    3.61 +                                , visible=1
    3.62 +                                 )
    3.63 +               ,
    3.64 +               )
    3.65 +
    3.66 +    action_providers = ( 'portal_membership'
    3.67 +                       , 'portal_actions'
    3.68 +                       , 'portal_registration'
    3.69 +                       , 'portal_types'
    3.70 +                       , 'portal_discussion'
    3.71 +                       , 'portal_undo'
    3.72 +                       , 'portal_syndication'
    3.73 +                       , 'portal_workflow'
    3.74 +                       , 'portal_properties'
    3.75 +                       )
    3.76 +
    3.77 +    security = ClassSecurityInfo()
    3.78 +
    3.79 +    manage_options = ( ActionProviderBase.manage_options
    3.80 +                     + ( { 'label' : 'Action Providers'
    3.81 +                         , 'action' : 'manage_actionProviders'
    3.82 +                         }
    3.83 +                       , { 'label' : 'Overview'
    3.84 +                         , 'action' : 'manage_overview'
    3.85 +                         }
    3.86 +                     ) + Folder.manage_options
    3.87 +                     )
    3.88 +
    3.89 +    #
    3.90 +    #   ZMI methods
    3.91 +    #
    3.92 +    security.declareProtected(ManagePortal, 'manage_overview')
    3.93 +    manage_overview = DTMLFile( 'explainActionsTool', _dtmldir )
    3.94 +    manage_actionProviders = DTMLFile('manageActionProviders', _dtmldir)
    3.95 +
    3.96 +    security.declareProtected(ManagePortal, 'manage_aproviders')
    3.97 +    def manage_aproviders(self
    3.98 +                        , apname=''
    3.99 +                        , chosen=()
   3.100 +                        , add_provider=0
   3.101 +                        , del_provider=0
   3.102 +                        , REQUEST=None):
   3.103 +        """
   3.104 +        Manage action providers through-the-web.
   3.105 +        """
   3.106 +        providers = list(self.listActionProviders())
   3.107 +        new_providers = []
   3.108 +        if add_provider:
   3.109 +            providers.append(apname)
   3.110 +        elif del_provider:
   3.111 +            for item in providers:
   3.112 +                if item not in chosen:
   3.113 +                    new_providers.append(item)
   3.114 +            providers = new_providers
   3.115 +        self.action_providers = tuple(providers)
   3.116 +        if REQUEST is not None:
   3.117 +            return self.manage_actionProviders(self , REQUEST
   3.118 +                          , manage_tabs_message='Providers changed.')
   3.119 +
   3.120 +    #
   3.121 +    #   Programmatically manipulate the list of action providers
   3.122 +    #
   3.123 +    security.declareProtected(ManagePortal, 'listActionProviders')
   3.124 +    def listActionProviders(self):
   3.125 +        """ List the ids of all Action Providers queried by this tool.
   3.126 +        """
   3.127 +        return self.action_providers
   3.128 +
   3.129 +    security.declareProtected(ManagePortal, 'addActionProvider')
   3.130 +    def addActionProvider( self, provider_name ):
   3.131 +        """ Add an Action Provider id to the providers queried by this tool.
   3.132 +        """
   3.133 +        ap = list( self.action_providers )
   3.134 +        if hasattr( self, provider_name ) and provider_name not in ap:
   3.135 +            ap.append( provider_name )
   3.136 +            self.action_providers = tuple( ap )
   3.137 +
   3.138 +    security.declareProtected(ManagePortal, 'deleteActionProvider')
   3.139 +    def deleteActionProvider( self, provider_name ):
   3.140 +        """ Delete an Action Provider id from providers queried by this tool.
   3.141 +        """
   3.142 +        ap = list( self.action_providers )
   3.143 +        if provider_name in ap:
   3.144 +            ap.remove( provider_name )
   3.145 +            self.action_providers = tuple( ap )
   3.146 +
   3.147 +    #
   3.148 +    #   'portal_actions' interface methods
   3.149 +    #
   3.150 +    security.declarePublic('listFilteredActionsFor')
   3.151 +    def listFilteredActionsFor(self, object=None):
   3.152 +        """ List all actions available to the user.
   3.153 +        """
   3.154 +        actions = []
   3.155 +
   3.156 +        # Include actions from specific tools.
   3.157 +        for provider_name in self.listActionProviders():
   3.158 +            provider = getattr(self, provider_name)
   3.159 +            if IActionProvider.isImplementedBy(provider):
   3.160 +                actions.extend( provider.listActionInfos(object=object) )
   3.161 +            else:
   3.162 +                # for Action Providers written for CMF versions before 1.5
   3.163 +                actions.extend( self._listActionInfos(provider, object) )
   3.164 +
   3.165 +        # Include actions from object.
   3.166 +        if object is not None:
   3.167 +            base = aq_base(object)
   3.168 +            if IActionProvider.isImplementedBy(base):
   3.169 +                actions.extend( object.listActionInfos(object=object) )
   3.170 +            elif hasattr(base, 'listActions'):
   3.171 +                # for objects written for CMF versions before 1.5
   3.172 +                actions.extend( self._listActionInfos(object, object) )
   3.173 +
   3.174 +        # Reorganize the actions by category.
   3.175 +        filtered_actions={'user':[],
   3.176 +                          'folder':[],
   3.177 +                          'object':[],
   3.178 +                          'global':[],
   3.179 +                          'workflow':[],
   3.180 +                          }
   3.181 +
   3.182 +        for action in actions:
   3.183 +            catlist = filtered_actions.setdefault(action['category'], [])
   3.184 +            catlist.append(action)
   3.185 +
   3.186 +        return filtered_actions
   3.187 +
   3.188 +    # listFilteredActions() is an alias.
   3.189 +    security.declarePublic('listFilteredActions')
   3.190 +    listFilteredActions = listFilteredActionsFor
   3.191 +
   3.192 +    #
   3.193 +    #   Helper method for backwards compatibility
   3.194 +    #
   3.195 +    def _listActionInfos(self, provider, object):
   3.196 +        """ for Action Providers written for CMF versions before 1.5
   3.197 +        """
   3.198 +        warn('ActionProvider interface not up to date. In CMF 2.0 '
   3.199 +             'portal_actions will ignore listActions() of \'%s\'.'
   3.200 +             % provider.getId(),
   3.201 +             DeprecationWarning)
   3.202 +        info = self._getOAI(object)
   3.203 +        actions = provider.listActions(info)
   3.204 +
   3.205 +        action_infos = []
   3.206 +        if actions and not isinstance(actions[0], dict):
   3.207 +            ec = self._getExprContext(object)
   3.208 +            for ai in actions:
   3.209 +                if not ai.getVisibility():
   3.210 +                    continue
   3.211 +                permissions = ai.getPermissions()
   3.212 +                if permissions:
   3.213 +                    category = ai.getCategory()
   3.214 +                    if (object is not None and
   3.215 +                        (category.startswith('object') or
   3.216 +                         category.startswith('workflow'))):
   3.217 +                        context = object
   3.218 +                    elif (info['folder'] is not None and
   3.219 +                          category.startswith('folder')):
   3.220 +                        context = info['folder']
   3.221 +                    else:
   3.222 +                        context = info['portal']
   3.223 +                    for permission in permissions:
   3.224 +                        allowed = _checkPermission(permission, context)
   3.225 +                        if allowed:
   3.226 +                            break
   3.227 +                    if not allowed:
   3.228 +                        continue
   3.229 +                if not ai.testCondition(ec):
   3.230 +                    continue
   3.231 +                action_infos.append( ai.getAction(ec) )
   3.232 +        else:
   3.233 +            for i in actions:
   3.234 +                if not i.get('visible', 1):
   3.235 +                    continue
   3.236 +                permissions = i.get('permissions', None)
   3.237 +                if permissions:
   3.238 +                    category = i['category']
   3.239 +                    if (object is not None and
   3.240 +                        (category.startswith('object') or
   3.241 +                         category.startswith('workflow'))):
   3.242 +                        context = object
   3.243 +                    elif (info['folder'] is not None and
   3.244 +                          category.startswith('folder')):
   3.245 +                        context = info['folder']
   3.246 +                    else:
   3.247 +                        context = info['portal']
   3.248 +
   3.249 +                    for permission in permissions:
   3.250 +                        allowed = _checkPermission(permission, context)
   3.251 +                        if allowed:
   3.252 +                            break
   3.253 +                    if not allowed:
   3.254 +                        continue
   3.255 +                action_infos.append(i)
   3.256 +        return action_infos
   3.257 +
   3.258 +InitializeClass(ActionsTool)
     4.1 new file mode 100644
     4.2 --- /dev/null
     4.3 +++ b/CMFBTreeFolder.py
     4.4 @@ -0,0 +1,69 @@
     4.5 +##############################################################################
     4.6 +#
     4.7 +# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
     4.8 +# All Rights Reserved.
     4.9 +#
    4.10 +# This software is subject to the provisions of the Zope Public License,
    4.11 +# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
    4.12 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    4.13 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    4.14 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    4.15 +# FOR A PARTICULAR PURPOSE
    4.16 +#
    4.17 +##############################################################################
    4.18 +"""CMFBTreeFolder
    4.19 +
    4.20 +$Id$
    4.21 +"""
    4.22 +
    4.23 +from AccessControl.SecurityInfo import ClassSecurityInfo
    4.24 +from Globals import InitializeClass
    4.25 +from Products.BTreeFolder2.BTreeFolder2 import BTreeFolder2Base
    4.26 +
    4.27 +from PortalFolder import PortalFolderBase
    4.28 +from PortalFolder import factory_type_information as PortalFolder_FTI
    4.29 +
    4.30 +_actions = PortalFolder_FTI[0]['actions']
    4.31 +
    4.32 +factory_type_information = ( { 'id'             : 'CMF BTree Folder',
    4.33 +                               'meta_type'      : 'CMF BTree Folder',
    4.34 +                               'description'    : """\
    4.35 +CMF folder designed to hold a lot of objects.""",
    4.36 +                               'icon'           : 'folder_icon.gif',
    4.37 +                               'product'        : 'CMFCore',
    4.38 +                               'factory'        : 'manage_addCMFBTreeFolder',
    4.39 +                               'filter_content_types' : 0,
    4.40 +                               'immediate_view' : 'folder_edit_form',
    4.41 +                               'actions'        : _actions,
    4.42 +                               },
    4.43 +                           )
    4.44 +
    4.45 +
    4.46 +def manage_addCMFBTreeFolder(dispatcher, id, title='', REQUEST=None):
    4.47 +    """Adds a new BTreeFolder object with id *id*.
    4.48 +    """
    4.49 +    id = str(id)
    4.50 +    ob = CMFBTreeFolder(id)
    4.51 +    ob.title = str(title)
    4.52 +    dispatcher._setObject(id, ob)
    4.53 +    ob = dispatcher._getOb(id)
    4.54 +    if REQUEST is not None:
    4.55 +        REQUEST['RESPONSE'].redirect(ob.absolute_url() + '/manage_main' )
    4.56 +
    4.57 +
    4.58 +class CMFBTreeFolder(BTreeFolder2Base, PortalFolderBase):
    4.59 +    """BTree folder for CMF sites.
    4.60 +    """
    4.61 +    meta_type = 'CMF BTree Folder'
    4.62 +    security = ClassSecurityInfo()
    4.63 +
    4.64 +    def __init__(self, id, title=''):
    4.65 +        PortalFolderBase.__init__(self, id, title)
    4.66 +        BTreeFolder2Base.__init__(self, id)
    4.67 +
    4.68 +    def _checkId(self, id, allow_dup=0):
    4.69 +        PortalFolderBase._checkId(self, id, allow_dup)
    4.70 +        BTreeFolder2Base._checkId(self, id, allow_dup)
    4.71 +
    4.72 +
    4.73 +InitializeClass(CMFBTreeFolder)
     5.1 new file mode 100644
     5.2 --- /dev/null
     5.3 +++ b/CMFCatalogAware.py
     5.4 @@ -0,0 +1,285 @@
     5.5 +##############################################################################
     5.6 +#
     5.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     5.8 +#
     5.9 +# This software is subject to the provisions of the Zope Public License,
    5.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    5.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    5.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    5.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    5.14 +# FOR A PARTICULAR PURPOSE.
    5.15 +#
    5.16 +##############################################################################
    5.17 +""" Base class for catalog aware content items.
    5.18 +
    5.19 +$Id$
    5.20 +"""
    5.21 +
    5.22 +import logging
    5.23 +from AccessControl import ClassSecurityInfo
    5.24 +from Acquisition import aq_base
    5.25 +from ExtensionClass import Base
    5.26 +from Globals import DTMLFile
    5.27 +from Globals import InitializeClass
    5.28 +
    5.29 +from permissions import AccessContentsInformation
    5.30 +from permissions import ManagePortal
    5.31 +from permissions import ModifyPortalContent
    5.32 +from utils import _dtmldir
    5.33 +from utils import _getAuthenticatedUser
    5.34 +from utils import getToolByName
    5.35 +
    5.36 +from interfaces.IOpaqueItems import ICallableOpaqueItem
    5.37 +
    5.38 +
    5.39 +logger = logging.getLogger('CMFCore.CMFCatalogAware')
    5.40 +
    5.41 +
    5.42 +class CMFCatalogAware(Base):
    5.43 +    """Mix-in for notifying portal_catalog and portal_workflow
    5.44 +    """
    5.45 +
    5.46 +    security = ClassSecurityInfo()
    5.47 +
    5.48 +    # The following methods can be overriden using inheritence so that
    5.49 +    # it's possible to specifiy another catalog tool or workflow tool
    5.50 +    # for a given content type
    5.51 +
    5.52 +    def _getCatalogTool(self):
    5.53 +        return getToolByName(self, 'portal_catalog', None)
    5.54 +
    5.55 +    def _getWorkflowTool(self):
    5.56 +        return getToolByName(self, 'portal_workflow', None)
    5.57 +
    5.58 +    # Cataloging methods
    5.59 +    # ------------------
    5.60 +
    5.61 +    security.declareProtected(ModifyPortalContent, 'indexObject')
    5.62 +    def indexObject(self):
    5.63 +        """
    5.64 +            Index the object in the portal catalog.
    5.65 +        """
    5.66 +        catalog = self._getCatalogTool()
    5.67 +        if catalog is not None:
    5.68 +            catalog.indexObject(self)
    5.69 +
    5.70 +    security.declareProtected(ModifyPortalContent, 'unindexObject')
    5.71 +    def unindexObject(self):
    5.72 +        """
    5.73 +            Unindex the object from the portal catalog.
    5.74 +        """
    5.75 +        catalog = self._getCatalogTool()
    5.76 +        if catalog is not None:
    5.77 +            catalog.unindexObject(self)
    5.78 +
    5.79 +    security.declareProtected(ModifyPortalContent, 'reindexObject')
    5.80 +    def reindexObject(self, idxs=[]):
    5.81 +        """
    5.82 +            Reindex the object in the portal catalog.
    5.83 +            If idxs is present, only those indexes are reindexed.
    5.84 +            The metadata is always updated.
    5.85 +
    5.86 +            Also update the modification date of the object,
    5.87 +            unless specific indexes were requested.
    5.88 +        """
    5.89 +        if idxs == []:
    5.90 +            # Update the modification date.
    5.91 +            if hasattr(aq_base(self), 'notifyModified'):
    5.92 +                self.notifyModified()
    5.93 +        catalog = self._getCatalogTool()
    5.94 +        if catalog is not None:
    5.95 +            catalog.reindexObject(self, idxs=idxs)
    5.96 +
    5.97 +    _cmf_security_indexes = ('allowedRolesAndUsers',)
    5.98 +
    5.99 +    security.declareProtected(ModifyPortalContent, 'reindexObjectSecurity')
   5.100 +    def reindexObjectSecurity(self, skip_self=False):
   5.101 +        """Reindex security-related indexes on the object.
   5.102 +
   5.103 +        Recurses in the children to reindex them too.
   5.104 +
   5.105 +        If skip_self is True, only the children will be reindexed. This
   5.106 +        is a useful optimization if the object itself has just been
   5.107 +        fully reindexed, as there's no need to reindex its security twice.
   5.108 +        """
   5.109 +        catalog = self._getCatalogTool()
   5.110 +        if catalog is None:
   5.111 +            return
   5.112 +        path = '/'.join(self.getPhysicalPath())
   5.113 +
   5.114 +        # XXX if _getCatalogTool() is overriden we will have to change
   5.115 +        # this method for the sub-objects.
   5.116 +        for brain in catalog.unrestrictedSearchResults(path=path):
   5.117 +            brain_path = brain.getPath()
   5.118 +            if brain_path == path and skip_self:
   5.119 +                continue
   5.120 +            # Get the object
   5.121 +            if hasattr(aq_base(brain), '_unrestrictedGetObject'):
   5.122 +                ob = brain._unrestrictedGetObject()
   5.123 +            else:
   5.124 +                # BBB: Zope 2.7
   5.125 +                ob = self.unrestrictedTraverse(brain_path, None)
   5.126 +            if ob is None:
   5.127 +                # BBB: Ignore old references to deleted objects.
   5.128 +                # Can happen only in Zope 2.7, or when using
   5.129 +                # catalog-getObject-raises off in Zope 2.8
   5.130 +                logger.warning("reindexObjectSecurity: Cannot get %s from "
   5.131 +                               "catalog", brain_path)
   5.132 +                continue
   5.133 +            # Recatalog with the same catalog uid.
   5.134 +            s = getattr(ob, '_p_changed', 0)
   5.135 +            catalog.reindexObject(ob, idxs=self._cmf_security_indexes,
   5.136 +                                  update_metadata=0, uid=brain_path)
   5.137 +            if s is None: ob._p_deactivate()
   5.138 +
   5.139 +    # Workflow methods
   5.140 +    # ----------------
   5.141 +
   5.142 +    security.declarePrivate('notifyWorkflowCreated')
   5.143 +    def notifyWorkflowCreated(self):
   5.144 +        """
   5.145 +            Notify the workflow that self was just created.
   5.146 +        """
   5.147 +        wftool = self._getWorkflowTool()
   5.148 +        if wftool is not None:
   5.149 +            wftool.notifyCreated(self)
   5.150 +
   5.151 +    # Opaque subitems
   5.152 +    # ---------------
   5.153 +
   5.154 +    security.declareProtected(AccessContentsInformation, 'opaqueItems')
   5.155 +    def opaqueItems(self):
   5.156 +        """
   5.157 +            Return opaque items (subelements that are contained
   5.158 +            using something that is not an ObjectManager).
   5.159 +        """
   5.160 +        items = []
   5.161 +
   5.162 +        # Call 'talkback' knowing that it is an opaque item.
   5.163 +        # This will remain here as long as the discussion item does
   5.164 +        # not implement ICallableOpaqueItem (backwards compatibility).
   5.165 +        if hasattr(aq_base(self), 'talkback'):
   5.166 +            talkback = self.talkback
   5.167 +            if talkback is not None:
   5.168 +                items.append((talkback.id, talkback))
   5.169 +
   5.170 +        # Other opaque items than 'talkback' may have callable
   5.171 +        # manage_after* and manage_before* hooks.
   5.172 +        # Loop over all attributes and add those to 'items'
   5.173 +        # implementing 'ICallableOpaqueItem'.
   5.174 +        self_base = aq_base(self)
   5.175 +        for name in self_base.__dict__.keys():
   5.176 +            obj = getattr(self_base, name)
   5.177 +            if ICallableOpaqueItem.isImplementedBy(obj):
   5.178 +                items.append((obj.getId(), obj))
   5.179 +
   5.180 +        return tuple(items)
   5.181 +
   5.182 +    security.declareProtected(AccessContentsInformation, 'opaqueIds')
   5.183 +    def opaqueIds(self):
   5.184 +        """
   5.185 +            Return opaque ids (subelements that are contained
   5.186 +            using something that is not an ObjectManager).
   5.187 +        """
   5.188 +        return [t[0] for t in self.opaqueItems()]
   5.189 +
   5.190 +    security.declareProtected(AccessContentsInformation, 'opaqueValues')
   5.191 +    def opaqueValues(self):
   5.192 +        """
   5.193 +            Return opaque values (subelements that are contained
   5.194 +            using something that is not an ObjectManager).
   5.195 +        """
   5.196 +        return [t[1] for t in self.opaqueItems()]
   5.197 +
   5.198 +    # Hooks
   5.199 +    # -----
   5.200 +
   5.201 +    def manage_afterAdd(self, item, container):
   5.202 +        """
   5.203 +            Add self to the catalog.
   5.204 +            (Called when the object is created or moved.)
   5.205 +        """
   5.206 +        self.indexObject()
   5.207 +        self.__recurse('manage_afterAdd', item, container)
   5.208 +
   5.209 +    def manage_afterClone(self, item):
   5.210 +        """
   5.211 +            Add self to the workflow.
   5.212 +            (Called when the object is cloned.)
   5.213 +        """
   5.214 +        self.notifyWorkflowCreated()
   5.215 +        self.__recurse('manage_afterClone', item)
   5.216 +
   5.217 +        # Make sure owner local role is set after pasting
   5.218 +        # The standard Zope mechanisms take care of executable ownership
   5.219 +        current_user = _getAuthenticatedUser(self)
   5.220 +        if current_user is not None:
   5.221 +            local_role_holders = [x[0] for x in self.get_local_roles()]
   5.222 +            self.manage_delLocalRoles(local_role_holders)
   5.223 +            self.manage_setLocalRoles(current_user.getId(), ['Owner'])
   5.224 +
   5.225 +    def manage_beforeDelete(self, item, container):
   5.226 +        """
   5.227 +            Remove self from the catalog.
   5.228 +            (Called when the object is deleted or moved.)
   5.229 +        """
   5.230 +        self.__recurse('manage_beforeDelete', item, container)
   5.231 +        self.unindexObject()
   5.232 +
   5.233 +    def __recurse(self, name, *args):
   5.234 +        """
   5.235 +            Recurse in both normal and opaque subobjects.
   5.236 +        """
   5.237 +        values = self.objectValues()
   5.238 +        opaque_values = self.opaqueValues()
   5.239 +        for subobjects in values, opaque_values:
   5.240 +            for ob in subobjects:
   5.241 +                s = getattr(ob, '_p_changed', 0)
   5.242 +                if hasattr(aq_base(ob), name):
   5.243 +                    getattr(ob, name)(*args)
   5.244 +                if s is None: ob._p_deactivate()
   5.245 +
   5.246 +    # ZMI
   5.247 +    # ---
   5.248 +
   5.249 +    manage_options = ({'label': 'Workflows',
   5.250 +                       'action': 'manage_workflowsTab',
   5.251 +                       },
   5.252 +                       )
   5.253 +
   5.254 +    _manage_workflowsTab = DTMLFile('zmi_workflows', _dtmldir)
   5.255 +
   5.256 +    security.declareProtected(ManagePortal, 'manage_workflowsTab')
   5.257 +    def manage_workflowsTab(self, REQUEST, manage_tabs_message=None):
   5.258 +        """
   5.259 +            Tab displaying the current workflows for the content object.
   5.260 +        """
   5.261 +        ob = self
   5.262 +        wftool = self._getWorkflowTool()
   5.263 +        # XXX None ?
   5.264 +        if wftool is not None:
   5.265 +            wf_ids = wftool.getChainFor(ob)
   5.266 +            states = {}
   5.267 +            chain = []
   5.268 +            for wf_id in wf_ids:
   5.269 +                wf = wftool.getWorkflowById(wf_id)
   5.270 +                if wf is not None:
   5.271 +                    # XXX a standard API would be nice
   5.272 +                    if hasattr(wf, 'getReviewStateOf'):
   5.273 +                        # Default Workflow
   5.274 +                        state = wf.getReviewStateOf(ob)
   5.275 +                    elif hasattr(wf, '_getWorkflowStateOf'):
   5.276 +                        # DCWorkflow
   5.277 +                        state = wf._getWorkflowStateOf(ob, id_only=1)
   5.278 +                    else:
   5.279 +                        state = '(Unknown)'
   5.280 +                    states[wf_id] = state
   5.281 +                    chain.append(wf_id)
   5.282 +        return self._manage_workflowsTab(
   5.283 +            REQUEST,
   5.284 +            chain=chain,
   5.285 +            states=states,
   5.286 +            management_view='Workflows',
   5.287 +            manage_tabs_message=manage_tabs_message)
   5.288 +
   5.289 +InitializeClass(CMFCatalogAware)
     6.1 new file mode 100644
     6.2 --- /dev/null
     6.3 +++ b/CMFCorePermissions.py
     6.4 @@ -0,0 +1,24 @@
     6.5 +##############################################################################
     6.6 +#
     6.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     6.8 +#
     6.9 +# This software is subject to the provisions of the Zope Public License,
    6.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    6.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    6.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    6.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    6.14 +# FOR A PARTICULAR PURPOSE.
    6.15 +#
    6.16 +##############################################################################
    6.17 +""" Backward compatibility;  see Products.CMFCore.permissions
    6.18 +
    6.19 +$Id$
    6.20 +"""
    6.21 +
    6.22 +from permissions import *
    6.23 +
    6.24 +from warnings import warn
    6.25 +
    6.26 +warn( "The module, 'Products.CMFCore.CMFCorePermissions' is a deprecated "
    6.27 +      "compatiblity alias for 'Products.CMFCore.permissions';  please use "
    6.28 +      "the new module instead.", DeprecationWarning, stacklevel=2)
     7.1 new file mode 100644
     7.2 --- /dev/null
     7.3 +++ b/CachingPolicyManager.py
     7.4 @@ -0,0 +1,826 @@
     7.5 +##############################################################################
     7.6 +#
     7.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     7.8 +#
     7.9 +# This software is subject to the provisions of the Zope Public License,
    7.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    7.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    7.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    7.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    7.14 +# FOR A PARTICULAR PURPOSE.
    7.15 +#
    7.16 +##############################################################################
    7.17 +"""Caching tool implementation.
    7.18 +
    7.19 +$Id$
    7.20 +"""
    7.21 +
    7.22 +from AccessControl import ClassSecurityInfo
    7.23 +from App.Common import rfc1123_date
    7.24 +from DateTime.DateTime import DateTime
    7.25 +from Globals import DTMLFile
    7.26 +from Globals import InitializeClass
    7.27 +from Globals import PersistentMapping
    7.28 +from OFS.SimpleItem import SimpleItem
    7.29 +from Products.PageTemplates.Expressions import getEngine
    7.30 +from Products.PageTemplates.Expressions import SecureModuleImporter
    7.31 +
    7.32 +from permissions import ManagePortal
    7.33 +from permissions import View
    7.34 +from Expression import Expression
    7.35 +from interfaces.CachingPolicyManager \
    7.36 +        import CachingPolicyManager as ICachingPolicyManager
    7.37 +from utils import _dtmldir
    7.38 +from utils import getToolByName
    7.39 +
    7.40 +from zope.interface import implements
    7.41 +from interfaces import ICachingPolicy
    7.42 +
    7.43 +def createCPContext( content, view_method, keywords, time=None ):
    7.44 +    """
    7.45 +        Construct an expression context for TALES expressions,
    7.46 +        for use by CachingPolicy objects.
    7.47 +    """
    7.48 +    pm = getToolByName( content, 'portal_membership', None )
    7.49 +    if not pm or pm.isAnonymousUser():
    7.50 +        member = None
    7.51 +    else:
    7.52 +        member = pm.getAuthenticatedMember()
    7.53 +
    7.54 +    if time is None:
    7.55 +        time = DateTime()
    7.56 +
    7.57 +    # The name "content" is deprecated and will go away in CMF 2.0,
    7.58 +    # please use "object" in your policy
    7.59 +    data = { 'content'  : content
    7.60 +           , 'object'   : content
    7.61 +           , 'view'     : view_method
    7.62 +           , 'keywords' : keywords
    7.63 +           , 'request'  : getattr( content, 'REQUEST', {} )
    7.64 +           , 'member'   : member
    7.65 +           , 'modules'  : SecureModuleImporter
    7.66 +           , 'nothing'  : None
    7.67 +           , 'time'     : time
    7.68 +           }
    7.69 +
    7.70 +    return getEngine().getContext( data )
    7.71 +
    7.72 +
    7.73 +class CachingPolicy:
    7.74 +    """
    7.75 +        Represent a single class of cachable objects:
    7.76 +
    7.77 +          - class membership is defined by 'predicate', a TALES expression
    7.78 +            with access to the following top-level names:
    7.79 +
    7.80 +            'object' -- the object itself
    7.81 +
    7.82 +            'view' -- the name of the view method
    7.83 +
    7.84 +            'keywords' -- keywords passed to the request
    7.85 +
    7.86 +            'request' -- the REQUEST object itself
    7.87 +
    7.88 +            'member' -- the authenticated member, or None if anonymous
    7.89 +
    7.90 +            'modules' -- usual TALES access-with-import
    7.91 +
    7.92 +            'nothing' -- None
    7.93 +
    7.94 +            'time' -- A DateTime object for the current date and time
    7.95 +
    7.96 +          - mtime_func is used to set the "Last-modified" HTTP response
    7.97 +            header, which is another TALES expression evaluated
    7.98 +            against the same namespace.  If not specified explicitly,
    7.99 +            uses 'object/modified'.  mtime_func is also used in responding
   7.100 +            to conditional GETs.
   7.101 +
   7.102 +          - The "Expires" HTTP response header and the "max-age" token of
   7.103 +            the "Cache-control" header will be set using 'max_age_secs',
   7.104 +            if passed;  it should be an integer value in seconds.
   7.105 +
   7.106 +          - The "s-maxage" token of the "Cache-control" header will be
   7.107 +            set using 's_max_age_secs', if passed;  it should be an integer
   7.108 +            value in seconds.
   7.109 +
   7.110 +          - The "Vary" HTTP response headers will be set if a value is 
   7.111 +            provided. The Vary header is described in RFC 2616. In essence,
   7.112 +            it instructs caches that respect this header (such as Squid
   7.113 +            after version 2.4) to distinguish between requests not just by
   7.114 +            the request URL, but also by values found in the headers showing
   7.115 +            in the Vary tag. "Vary: Cookie" would force Squid to also take 
   7.116 +            Cookie headers into account when deciding what cached object to 
   7.117 +            choose and serve in response to a request.
   7.118 +
   7.119 +          - The "ETag" HTTP response header will be set if a value is
   7.120 +            provided. The value is a TALES expression and the result 
   7.121 +            after evaluation will be used as the ETag header value.
   7.122 +
   7.123 +          - Other tokens will be added to the "Cache-control" HTTP response
   7.124 +            header as follows:
   7.125 +
   7.126 +             'no_cache=1' argument => "no-cache" token
   7.127 +
   7.128 +             'no_store=1' argument => "no-store" token
   7.129 +
   7.130 +             'must_revalidate=1' argument => "must-revalidate" token
   7.131 +
   7.132 +             'proxy_revalidate=1' argument => "proxy-revalidate" token
   7.133 +             
   7.134 +             'public=1' argument => "public" token
   7.135 +             
   7.136 +             'private=1' argument => "private" token
   7.137 +
   7.138 +             'no_transform=1' argument => "no-transform" token
   7.139 +
   7.140 +          - The last_modified argument is used to determine whether to add a
   7.141 +            Last-Modified header.  last_modified=1 by default.  There appears
   7.142 +            to be a bug in IE 6 (and possibly other versions) that uses the
   7.143 +            Last-Modified header plus some heuristics rather than the other
   7.144 +            explicit caching headers to determine whether to render content
   7.145 +            from the cache.  If you set, say, max-age=0, must-revalidate and
   7.146 +            have a Last-Modified header some time in the past, IE will
   7.147 +            recognize that the page in cache is stale and will request an
   7.148 +            update from the server BUT if you have a Last-Modified header
   7.149 +            with an older date, will then ignore the update and render from
   7.150 +            the cache, so you may want to disable the Last-Modified header
   7.151 +            when controlling caching using Cache-Control headers.
   7.152 +
   7.153 +          - The pre-check and post-check Cache-Control tokens are Microsoft
   7.154 +            proprietary tokens added to IE 5+.  Documentation can be found
   7.155 +            here: http://msdn.microsoft.com/workshop/author/perf/perftips.asp
   7.156 +            Unfortunately these are needed to make IE behave correctly.
   7.157 +
   7.158 +    """
   7.159 +
   7.160 +    implements(ICachingPolicy)
   7.161 +
   7.162 +    def __init__( self
   7.163 +                , policy_id
   7.164 +                , predicate=''
   7.165 +                , mtime_func=''
   7.166 +                , max_age_secs=None
   7.167 +                , no_cache=0
   7.168 +                , no_store=0
   7.169 +                , must_revalidate=0
   7.170 +                , vary=''
   7.171 +                , etag_func=''
   7.172 +                , s_max_age_secs=None
   7.173 +                , proxy_revalidate=0
   7.174 +                , public=0
   7.175 +                , private=0
   7.176 +                , no_transform=0
   7.177 +                , enable_304s=0
   7.178 +                , last_modified=1
   7.179 +                , pre_check=None
   7.180 +                , post_check=None
   7.181 +                ):
   7.182 +
   7.183 +        if not predicate:
   7.184 +            predicate = 'python:1'
   7.185 +
   7.186 +        if not mtime_func:
   7.187 +            mtime_func = 'object/modified'
   7.188 +
   7.189 +        if max_age_secs is not None:
   7.190 +            if str(max_age_secs).strip() == '':
   7.191 +                max_age_secs = None
   7.192 +            else:
   7.193 +                max_age_secs = int( max_age_secs )
   7.194 +
   7.195 +        if s_max_age_secs is not None:
   7.196 +            if str(s_max_age_secs).strip() == '':
   7.197 +                s_max_age_secs = None
   7.198 +            else:
   7.199 +                s_max_age_secs = int( s_max_age_secs )
   7.200 +
   7.201 +        if pre_check is not None:
   7.202 +            if str(pre_check).strip() == '':
   7.203 +                pre_check = None
   7.204 +            else:
   7.205 +                pre_check = int(pre_check)
   7.206 +            
   7.207 +        if post_check is not None:
   7.208 +            if str(post_check).strip() == '':
   7.209 +                post_check = None
   7.210 +            else:
   7.211 +                post_check = int(post_check)
   7.212 +
   7.213 +        self._policy_id = policy_id
   7.214 +        self._predicate = Expression( text=predicate )
   7.215 +        self._mtime_func = Expression( text=mtime_func )
   7.216 +        self._max_age_secs = max_age_secs
   7.217 +        self._s_max_age_secs = s_max_age_secs
   7.218 +        self._no_cache = int( no_cache )
   7.219 +        self._no_store = int( no_store )
   7.220 +        self._must_revalidate = int( must_revalidate )
   7.221 +        self._proxy_revalidate = int( proxy_revalidate )
   7.222 +        self._public = int( public )
   7.223 +        self._private = int( private )
   7.224 +        self._no_transform = int( no_transform )
   7.225 +        self._vary = vary
   7.226 +        self._etag_func = Expression( text=etag_func )
   7.227 +        self._enable_304s = int ( enable_304s )
   7.228 +        self._last_modified = int( last_modified )
   7.229 +        self._pre_check = pre_check
   7.230 +        self._post_check = post_check
   7.231 +
   7.232 +    def getPolicyId( self ):
   7.233 +        """
   7.234 +        """
   7.235 +        return self._policy_id
   7.236 +
   7.237 +    def getPredicate( self ):
   7.238 +        """
   7.239 +        """
   7.240 +        return self._predicate.text
   7.241 +
   7.242 +    def getMTimeFunc( self ):
   7.243 +        """
   7.244 +        """
   7.245 +        return self._mtime_func.text
   7.246 +
   7.247 +    def getMaxAgeSecs( self ):
   7.248 +        """
   7.249 +        """
   7.250 +        return self._max_age_secs
   7.251 +
   7.252 +    def getSMaxAgeSecs( self ):
   7.253 +        """
   7.254 +        """
   7.255 +        return getattr(self, '_s_max_age_secs', None)
   7.256 +
   7.257 +    def getNoCache( self ):
   7.258 +        """
   7.259 +        """
   7.260 +        return self._no_cache
   7.261 +
   7.262 +    def getNoStore( self ):
   7.263 +        """
   7.264 +        """
   7.265 +        return self._no_store
   7.266 +
   7.267 +    def getMustRevalidate( self ):
   7.268 +        """
   7.269 +        """
   7.270 +        return self._must_revalidate
   7.271 +
   7.272 +    def getProxyRevalidate( self ):
   7.273 +        """
   7.274 +        """
   7.275 +        return getattr(self, '_proxy_revalidate', 0)
   7.276 +
   7.277 +    def getPublic( self ):
   7.278 +        """
   7.279 +        """
   7.280 +        return getattr(self, '_public', 0)
   7.281 +
   7.282 +    def getPrivate( self ):
   7.283 +        """
   7.284 +        """
   7.285 +        return getattr(self, '_private', 0)
   7.286 +
   7.287 +    def getNoTransform( self ):
   7.288 +        """
   7.289 +        """
   7.290 +        return getattr(self, '_no_transform', 0)
   7.291 +
   7.292 +    def getVary( self ):
   7.293 +        """
   7.294 +        """
   7.295 +        return getattr(self, '_vary', '')
   7.296 +
   7.297 +    def getETagFunc( self ):
   7.298 +        """
   7.299 +        """
   7.300 +        etag_func_text = ''
   7.301 +        etag_func = getattr(self, '_etag_func', None)
   7.302 +
   7.303 +        if etag_func is not None:
   7.304 +            etag_func_text = etag_func.text
   7.305 +
   7.306 +        return etag_func_text
   7.307 +
   7.308 +    def getEnable304s(self):
   7.309 +        """
   7.310 +        """
   7.311 +        return getattr(self, '_enable_304s', 0)
   7.312 +
   7.313 +    def getLastModified(self):
   7.314 +        """Should we set the last modified header?"""
   7.315 +        return getattr(self, '_last_modified', 1)
   7.316 +
   7.317 +    def getPreCheck(self):
   7.318 +        """
   7.319 +        """
   7.320 +        return getattr(self, '_pre_check', None)
   7.321 +
   7.322 +    def getPostCheck(self):
   7.323 +        """
   7.324 +        """
   7.325 +        return getattr(self, '_post_check', None)
   7.326 +    
   7.327 +    def testPredicate(self, expr_context):
   7.328 +        """ Does this request match our predicate?"""
   7.329 +        return self._predicate(expr_context)
   7.330 +
   7.331 +    def getHeaders( self, expr_context ):
   7.332 +        """
   7.333 +            Does this request match our predicate?  If so, return a
   7.334 +            sequence of caching headers as ( key, value ) tuples.
   7.335 +            Otherwise, return an empty sequence.
   7.336 +        """
   7.337 +        headers = []
   7.338 +
   7.339 +        if self.testPredicate( expr_context ):
   7.340 +
   7.341 +            if self.getLastModified():
   7.342 +                mtime = self._mtime_func( expr_context )
   7.343 +                if type( mtime ) is type( '' ):
   7.344 +                    mtime = DateTime( mtime )
   7.345 +                if mtime is not None:
   7.346 +                    mtime_str = rfc1123_date(mtime.timeTime())
   7.347 +                    headers.append( ( 'Last-modified', mtime_str ) )
   7.348 +
   7.349 +            control = []
   7.350 +
   7.351 +            if self.getMaxAgeSecs() is not None:
   7.352 +                now = expr_context.vars[ 'time' ]
   7.353 +                exp_time_str = rfc1123_date(now.timeTime() + self._max_age_secs)
   7.354 +                headers.append( ( 'Expires', exp_time_str ) )
   7.355 +                control.append( 'max-age=%d' % self._max_age_secs )
   7.356 +                
   7.357 +            if self.getSMaxAgeSecs() is not None:
   7.358 +                control.append( 's-maxage=%d' % self._s_max_age_secs )
   7.359 +
   7.360 +            if self.getNoCache():
   7.361 +                control.append( 'no-cache' )
   7.362 +                # The following is for HTTP 1.0 clients
   7.363 +                headers.append(('Pragma', 'no-cache'))
   7.364 +
   7.365 +            if self.getNoStore():
   7.366 +                control.append( 'no-store' )
   7.367 +
   7.368 +            if self.getPublic():
   7.369 +                control.append( 'public' )
   7.370 +
   7.371 +            if self.getPrivate():
   7.372 +                control.append( 'private' )
   7.373 +
   7.374 +            if self.getMustRevalidate():
   7.375 +                control.append( 'must-revalidate' )
   7.376 +
   7.377 +            if self.getProxyRevalidate():
   7.378 +                control.append( 'proxy-revalidate' )
   7.379 +
   7.380 +            if self.getNoTransform():
   7.381 +                control.append( 'no-transform' )
   7.382 +
   7.383 +            pre_check = self.getPreCheck()
   7.384 +            if pre_check is not None:
   7.385 +                control.append('pre-check=%d' % pre_check)
   7.386 +
   7.387 +            post_check = self.getPostCheck()
   7.388 +            if post_check is not None:
   7.389 +                control.append('post-check=%d' % post_check)
   7.390 +
   7.391 +            if control:
   7.392 +                headers.append( ( 'Cache-control', ', '.join( control ) ) )
   7.393 +
   7.394 +            if self.getVary():
   7.395 +                headers.append( ( 'Vary', self._vary ) )
   7.396 +
   7.397 +            if self.getETagFunc():
   7.398 +                headers.append( ( 'ETag', self._etag_func( expr_context ) ) )
   7.399 +
   7.400 +        return headers
   7.401 +
   7.402 +
   7.403 +
   7.404 +class CachingPolicyManager( SimpleItem ):
   7.405 +    """
   7.406 +        Manage the set of CachingPolicy objects for the site;  dispatch
   7.407 +        to them from skin methods.
   7.408 +    """
   7.409 +
   7.410 +    __implements__ = ICachingPolicyManager
   7.411 +
   7.412 +    id = 'caching_policy_manager'
   7.413 +    meta_type = 'CMF Caching Policy Manager'
   7.414 +
   7.415 +    security = ClassSecurityInfo()
   7.416 +
   7.417 +    def __init__( self ):
   7.418 +        self._policy_ids = ()
   7.419 +        self._policies = PersistentMapping()
   7.420 +
   7.421 +    #
   7.422 +    #   ZMI
   7.423 +    #
   7.424 +    manage_options = ( ( { 'label'  : 'Policies'
   7.425 +                         , 'action' : 'manage_cachingPolicies'
   7.426 +                         , 'help'   : ('CMFCore', 'CPMPolicies.stx')
   7.427 +                         }
   7.428 +                       ,
   7.429 +                       )
   7.430 +                     + SimpleItem.manage_options
   7.431 +                     )
   7.432 +
   7.433 +    security.declareProtected( ManagePortal, 'manage_cachingPolicies' )
   7.434 +    manage_cachingPolicies = DTMLFile( 'cachingPolicies', _dtmldir )
   7.435 +
   7.436 +    security.declarePublic( 'listPolicies' )
   7.437 +    def listPolicies( self ):
   7.438 +        """
   7.439 +            Return a sequence of tuples,
   7.440 +            '( policy_id, ( policy, typeObjectName ) )'
   7.441 +            for all policies in the registry 
   7.442 +        """
   7.443 +        result = []
   7.444 +        for policy_id in self._policy_ids:
   7.445 +            result.append( ( policy_id, self._policies[ policy_id ] ) )
   7.446 +        return tuple( result )
   7.447 +
   7.448 +    security.declareProtected( ManagePortal, 'addPolicy' )
   7.449 +    def addPolicy( self
   7.450 +                 , policy_id
   7.451 +                 , predicate           # TALES expr (def. 'python:1')
   7.452 +                 , mtime_func          # TALES expr (def. 'object/modified')
   7.453 +                 , max_age_secs        # integer, seconds (def. 0)
   7.454 +                 , no_cache            # boolean (def. 0)
   7.455 +                 , no_store            # boolean (def. 0)
   7.456 +                 , must_revalidate     # boolean (def. 0)
   7.457 +                 , vary                # string value
   7.458 +                 , etag_func           # TALES expr (def. '')
   7.459 +                 , REQUEST=None
   7.460 +                 , s_max_age_secs=None # integer, seconds (def. None)
   7.461 +                 , proxy_revalidate=0  # boolean (def. 0)
   7.462 +                 , public=0            # boolean (def. 0)
   7.463 +                 , private=0           # boolean (def. 0)
   7.464 +                 , no_transform=0      # boolean (def. 0)
   7.465 +                 , enable_304s=0       # boolean (def. 0)
   7.466 +                 , last_modified=1     # boolean (def. 1)
   7.467 +                 , pre_check=None      # integer, default None
   7.468 +                 , post_check=None     # integer, default None
   7.469 +                 ):
   7.470 +        """
   7.471 +            Add a caching policy.
   7.472 +        """
   7.473 +        if max_age_secs is None or str(max_age_secs).strip() == '':
   7.474 +            max_age_secs = None
   7.475 +        else:
   7.476 +            max_age_secs = int(max_age_secs)
   7.477 +
   7.478 +        if s_max_age_secs is None or str(s_max_age_secs).strip() == '':
   7.479 +            s_max_age_secs = None
   7.480 +        else:
   7.481 +            s_max_age_secs = int(s_max_age_secs)
   7.482 +            
   7.483 +        if pre_check is None or str(pre_check).strip() == '':
   7.484 +            pre_check = None
   7.485 +        else:
   7.486 +            pre_check = int(pre_check)
   7.487 +
   7.488 +        if post_check is None or str(post_check).strip() == '':
   7.489 +            post_check = None
   7.490 +        else:
   7.491 +            post_check = int(post_check)
   7.492 +
   7.493 +        self._addPolicy( policy_id
   7.494 +                       , predicate
   7.495 +                       , mtime_func
   7.496 +                       , max_age_secs
   7.497 +                       , no_cache
   7.498 +                       , no_store
   7.499 +                       , must_revalidate
   7.500 +                       , vary
   7.501 +                       , etag_func
   7.502 +                       , s_max_age_secs
   7.503 +                       , proxy_revalidate
   7.504 +                       , public
   7.505 +                       , private
   7.506 +                       , no_transform
   7.507 +                       , enable_304s
   7.508 +                       , last_modified
   7.509 +                       , pre_check
   7.510 +                       , post_check
   7.511 +                       )
   7.512 +        if REQUEST is not None: 
   7.513 +            REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   7.514 +                                          + '/manage_cachingPolicies'
   7.515 +                                          + '?manage_tabs_message='
   7.516 +                                          + 'Policy+added.'
   7.517 +                                          )
   7.518 +
   7.519 +    security.declareProtected( ManagePortal, 'updatePolicy' )
   7.520 +    def updatePolicy( self
   7.521 +                    , policy_id
   7.522 +                    , predicate           # TALES expr (def. 'python:1')
   7.523 +                    , mtime_func          # TALES expr (def. 'object/modified')
   7.524 +                    , max_age_secs        # integer, seconds (def. 0)
   7.525 +                    , no_cache            # boolean (def. 0)
   7.526 +                    , no_store            # boolean (def. 0)
   7.527 +                    , must_revalidate     # boolean (def. 0)
   7.528 +                    , vary                # string value
   7.529 +                    , etag_func           # TALES expr (def. '')
   7.530 +                    , REQUEST=None
   7.531 +                    , s_max_age_secs=None # integer, seconds (def. 0)
   7.532 +                    , proxy_revalidate=0  # boolean (def. 0)
   7.533 +                    , public=0            # boolean (def. 0)
   7.534 +                    , private=0           # boolean (def. 0)
   7.535 +                    , no_transform=0      # boolean (def. 0)
   7.536 +                    , enable_304s=0       # boolean (def. 0)
   7.537 +                    , last_modified=1     # boolean (def. 1)
   7.538 +                    , pre_check=0         # integer, default=None
   7.539 +                    , post_check=0        # integer, default=None
   7.540 +                    ):
   7.541 +        """
   7.542 +            Update a caching policy.
   7.543 +        """
   7.544 +        if max_age_secs is None or str(max_age_secs).strip() == '':
   7.545 +            max_age_secs = None
   7.546 +        else:
   7.547 +            max_age_secs = int(max_age_secs)
   7.548 +
   7.549 +        if s_max_age_secs is None or str(s_max_age_secs).strip() == '':
   7.550 +            s_max_age_secs = None
   7.551 +        else:
   7.552 +            s_max_age_secs = int(s_max_age_secs)
   7.553 +            
   7.554 +        if pre_check is None or str(pre_check).strip() == '':
   7.555 +            pre_check = None
   7.556 +        else:
   7.557 +            pre_check = int(pre_check)
   7.558 +
   7.559 +        if post_check is None or str(post_check).strip() == '':
   7.560 +            post_check = None
   7.561 +        else:
   7.562 +            post_check = int(post_check)
   7.563 +
   7.564 +        self._updatePolicy( policy_id
   7.565 +                          , predicate
   7.566 +                          , mtime_func
   7.567 +                          , max_age_secs
   7.568 +                          , no_cache
   7.569 +                          , no_store
   7.570 +                          , must_revalidate
   7.571 +                          , vary
   7.572 +                          , etag_func
   7.573 +                          , s_max_age_secs
   7.574 +                          , proxy_revalidate
   7.575 +                          , public
   7.576 +                          , private
   7.577 +                          , no_transform
   7.578 +                          , enable_304s
   7.579 +                          , last_modified
   7.580 +                          , pre_check
   7.581 +                          , post_check
   7.582 +                          )
   7.583 +        if REQUEST is not None: 
   7.584 +            REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   7.585 +                                          + '/manage_cachingPolicies'
   7.586 +                                          + '?manage_tabs_message='
   7.587 +                                          + 'Policy+updated.'
   7.588 +                                          )
   7.589 +
   7.590 +    security.declareProtected( ManagePortal, 'movePolicyUp' )
   7.591 +    def movePolicyUp( self, policy_id, REQUEST=None ):
   7.592 +        """
   7.593 +            Move a caching policy up in the list.
   7.594 +        """
   7.595 +        policy_ids = list( self._policy_ids )
   7.596 +        ndx = policy_ids.index( policy_id )
   7.597 +        if ndx == 0:
   7.598 +            msg = "Policy+already+first."
   7.599 +        else:
   7.600 +            self._reorderPolicy( policy_id, ndx - 1 )
   7.601 +            msg = "Policy+moved."
   7.602 +        if REQUEST is not None:
   7.603 +            REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   7.604 +                              + '/manage_cachingPolicies'
   7.605 +                              + '?manage_tabs_message=%s' % msg
   7.606 +                              )
   7.607 +
   7.608 +    security.declareProtected( ManagePortal, 'movePolicyDown' )
   7.609 +    def movePolicyDown( self, policy_id, REQUEST=None ):
   7.610 +        """
   7.611 +            Move a caching policy down in the list.
   7.612 +        """
   7.613 +        policy_ids = list( self._policy_ids )
   7.614 +        ndx = policy_ids.index( policy_id )
   7.615 +        if ndx == len( policy_ids ) - 1:
   7.616 +            msg = "Policy+already+last."
   7.617 +        else:
   7.618 +            self._reorderPolicy( policy_id, ndx + 1 )
   7.619 +            msg = "Policy+moved."
   7.620 +        if REQUEST is not None:
   7.621 +            REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   7.622 +                              + '/manage_cachingPolicies'
   7.623 +                              + '?manage_tabs_message=%s' % msg
   7.624 +                              )
   7.625 +
   7.626 +    security.declareProtected( ManagePortal, 'removePolicy' )
   7.627 +    def removePolicy( self, policy_id, REQUEST=None ):
   7.628 +        """
   7.629 +            Remove a caching policy.
   7.630 +        """
   7.631 +        self._removePolicy( policy_id )
   7.632 +        if REQUEST is not None:
   7.633 +            REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   7.634 +                              + '/manage_cachingPolicies'
   7.635 +                              + '?manage_tabs_message=Policy+removed.'
   7.636 +                              )
   7.637 +
   7.638 +    #
   7.639 +    #   Policy manipulation methods.
   7.640 +    #
   7.641 +    security.declarePrivate( '_addPolicy' )
   7.642 +    def _addPolicy( self
   7.643 +                  , policy_id
   7.644 +                  , predicate
   7.645 +                  , mtime_func
   7.646 +                  , max_age_secs
   7.647 +                  , no_cache
   7.648 +                  , no_store
   7.649 +                  , must_revalidate
   7.650 +                  , vary
   7.651 +                  , etag_func
   7.652 +                  , s_max_age_secs=None
   7.653 +                  , proxy_revalidate=0
   7.654 +                  , public=0
   7.655 +                  , private=0
   7.656 +                  , no_transform=0
   7.657 +                  , enable_304s=0
   7.658 +                  , last_modified=1
   7.659 +                  , pre_check=None
   7.660 +                  , post_check=None
   7.661 +                  ):
   7.662 +        """
   7.663 +            Add a policy to our registry.
   7.664 +        """
   7.665 +        policy_id = str( policy_id ).strip()
   7.666 +
   7.667 +        if not policy_id:
   7.668 +            raise ValueError, "Policy ID is required!"
   7.669 +
   7.670 +        if policy_id in self._policy_ids:
   7.671 +            raise KeyError, "Policy %s already exists!" % policy_id
   7.672 +
   7.673 +        self._policies[ policy_id ] = CachingPolicy( policy_id
   7.674 +                                                   , predicate
   7.675 +                                                   , mtime_func
   7.676 +                                                   , max_age_secs
   7.677 +                                                   , no_cache
   7.678 +                                                   , no_store
   7.679 +                                                   , must_revalidate
   7.680 +                                                   , vary
   7.681 +                                                   , etag_func
   7.682 +                                                   , s_max_age_secs
   7.683 +                                                   , proxy_revalidate
   7.684 +                                                   , public
   7.685 +                                                   , private
   7.686 +                                                   , no_transform
   7.687 +                                                   , enable_304s
   7.688 +                                                   , last_modified
   7.689 +                                                   , pre_check
   7.690 +                                                   , post_check
   7.691 +                                                   )
   7.692 +        idlist = list( self._policy_ids )
   7.693 +        idlist.append( policy_id )
   7.694 +        self._policy_ids = tuple( idlist )
   7.695 +
   7.696 +    security.declarePrivate( '_updatePolicy' )
   7.697 +    def _updatePolicy( self
   7.698 +                     , policy_id
   7.699 +                     , predicate
   7.700 +                     , mtime_func
   7.701 +                     , max_age_secs
   7.702 +                     , no_cache
   7.703 +                     , no_store
   7.704 +                     , must_revalidate
   7.705 +                     , vary
   7.706 +                     , etag_func
   7.707 +                     , s_max_age_secs=None
   7.708 +                     , proxy_revalidate=0
   7.709 +                     , public=0
   7.710 +                     , private=0
   7.711 +                     , no_transform=0
   7.712 +                     , enable_304s=0
   7.713 +                     , last_modified=1
   7.714 +                     , pre_check=None
   7.715 +                     , post_check=None
   7.716 +                     ):
   7.717 +        """
   7.718 +            Update a policy in our registry.
   7.719 +        """
   7.720 +        if policy_id not in self._policy_ids:
   7.721 +            raise KeyError, "Policy %s does not exist!" % policy_id
   7.722 +
   7.723 +        self._policies[ policy_id ] = CachingPolicy( policy_id
   7.724 +                                                   , predicate
   7.725 +                                                   , mtime_func
   7.726 +                                                   , max_age_secs
   7.727 +                                                   , no_cache
   7.728 +                                                   , no_store
   7.729 +                                                   , must_revalidate
   7.730 +                                                   , vary
   7.731 +                                                   , etag_func
   7.732 +                                                   , s_max_age_secs
   7.733 +                                                   , proxy_revalidate
   7.734 +                                                   , public
   7.735 +                                                   , private
   7.736 +                                                   , no_transform
   7.737 +                                                   , enable_304s
   7.738 +                                                   , last_modified
   7.739 +                                                   , pre_check
   7.740 +                                                   , post_check
   7.741 +                                                   )
   7.742 +
   7.743 +    security.declarePrivate( '_reorderPolicy' )
   7.744 +    def _reorderPolicy( self, policy_id, newIndex ):
   7.745 +        """
   7.746 +            Reorder a policy in our registry.
   7.747 +        """
   7.748 +        if policy_id not in self._policy_ids:
   7.749 +            raise KeyError, "Policy %s does not exist!" % policy_id
   7.750 +
   7.751 +        idlist = list( self._policy_ids )
   7.752 +        ndx = idlist.index( policy_id )
   7.753 +        pred = idlist[ ndx ]
   7.754 +        idlist = idlist[ :ndx ] + idlist[ ndx+1: ]
   7.755 +        idlist.insert( newIndex, pred )
   7.756 +        self._policy_ids = tuple( idlist )
   7.757 +
   7.758 +    security.declarePrivate( '_removePolicy' )
   7.759 +    def _removePolicy( self, policy_id ):
   7.760 +        """
   7.761 +            Remove a policy from our registry.
   7.762 +        """
   7.763 +        if policy_id not in self._policy_ids:
   7.764 +            raise KeyError, "Policy %s does not exist!" % policy_id
   7.765 +
   7.766 +        del self._policies[ policy_id ]
   7.767 +        idlist = list( self._policy_ids )
   7.768 +        ndx = idlist.index( policy_id )
   7.769 +        idlist = idlist[ :ndx ] + idlist[ ndx+1: ]
   7.770 +        self._policy_ids = tuple( idlist )
   7.771 +
   7.772 +
   7.773 +    #
   7.774 +    #   'portal_caching' interface methods
   7.775 +    #
   7.776 +    security.declareProtected( View, 'getHTTPCachingHeaders' )
   7.777 +    def getHTTPCachingHeaders( self, content, view_method, keywords, time=None):
   7.778 +        """
   7.779 +            Return a list of HTTP caching headers based on 'content',
   7.780 +            'view_method', and 'keywords'.
   7.781 +        """
   7.782 +        context = createCPContext( content, view_method, keywords, time=time )
   7.783 +        for policy_id, policy in self.listPolicies():
   7.784 +
   7.785 +            headers = policy.getHeaders( context )
   7.786 +            if headers:
   7.787 +                return headers
   7.788 +
   7.789 +        return ()
   7.790 +
   7.791 +    security.declareProtected( View, 'getModTimeAndETag' )
   7.792 +    def getModTimeAndETag( self, content, view_method, keywords, time=None):
   7.793 +        """ Return the modification time and ETag for the content object,
   7.794 +            view method, and keywords as the tuple (modification_time, etag,
   7.795 +            set_last_modified_header), where modification_time is a DateTime,
   7.796 +            or None.
   7.797 +        """
   7.798 +        context = createCPContext( content, view_method, keywords, time=time )
   7.799 +        for policy_id, policy in self.listPolicies():
   7.800 +            if policy.getEnable304s() and policy.testPredicate(context):
   7.801 +                
   7.802 +                last_modified = policy._mtime_func(context)
   7.803 +                if type(last_modified) is type(''):
   7.804 +                    last_modified = DateTime(last_modified)
   7.805 +
   7.806 +                content_etag = None
   7.807 +                if policy.getETagFunc():
   7.808 +                    content_etag = policy._etag_func(context)
   7.809 +                    
   7.810 +                return (last_modified, content_etag, policy.getLastModified())
   7.811 +            
   7.812 +        return None
   7.813 +
   7.814 +
   7.815 +InitializeClass( CachingPolicyManager )
   7.816 +
   7.817 +
   7.818 +def manage_addCachingPolicyManager( self, REQUEST=None ):
   7.819 +    """
   7.820 +        Add a CPM to self.
   7.821 +    """
   7.822 +    id = CachingPolicyManager.id
   7.823 +    mgr = CachingPolicyManager()
   7.824 +    self._setObject( id, mgr )
   7.825 +
   7.826 +    if REQUEST is not None:
   7.827 +        REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   7.828 +                      + '/manage_main'
   7.829 +                      + '?manage_tabs_message=Caching+Policy+Manager+added.'
   7.830 +                      )
     8.1 new file mode 100644
     8.2 --- /dev/null
     8.3 +++ b/CatalogTool.py
     8.4 @@ -0,0 +1,394 @@
     8.5 +##############################################################################
     8.6 +#
     8.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     8.8 +#
     8.9 +# This software is subject to the provisions of the Zope Public License,
    8.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    8.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    8.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    8.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    8.14 +# FOR A PARTICULAR PURPOSE.
    8.15 +#
    8.16 +##############################################################################
    8.17 +""" Basic portal catalog.
    8.18 +
    8.19 +$Id$
    8.20 +"""
    8.21 +
    8.22 +from warnings import warn
    8.23 +
    8.24 +from AccessControl import ClassSecurityInfo
    8.25 +from AccessControl.PermissionRole import rolesForPermissionOn
    8.26 +from Acquisition import aq_base
    8.27 +from DateTime import DateTime
    8.28 +from Globals import DTMLFile
    8.29 +from Globals import InitializeClass
    8.30 +from Products.PluginIndexes.common import safe_callable
    8.31 +from Products.ZCatalog.ZCatalog import ZCatalog
    8.32 +from Products.ZCTextIndex.HTMLSplitter import HTMLWordSplitter
    8.33 +from Products.ZCTextIndex.Lexicon import CaseNormalizer
    8.34 +from Products.ZCTextIndex.Lexicon import Splitter
    8.35 +from Products.ZCTextIndex.Lexicon import StopWordRemover
    8.36 +from Products.ZCTextIndex.ZCTextIndex import PLexicon
    8.37 +from zope.interface import providedBy
    8.38 +from zope.interface.declarations import getObjectSpecification
    8.39 +from zope.interface.declarations import ObjectSpecification
    8.40 +from zope.interface.declarations import ObjectSpecificationDescriptor
    8.41 +
    8.42 +from ActionProviderBase import ActionProviderBase
    8.43 +from interfaces.portal_catalog \
    8.44 +        import IndexableObjectWrapper as IIndexableObjectWrapper
    8.45 +from interfaces.portal_catalog import portal_catalog as ICatalogTool
    8.46 +from permissions import AccessInactivePortalContent
    8.47 +from permissions import ManagePortal
    8.48 +from permissions import View
    8.49 +from utils import _checkPermission
    8.50 +from utils import _dtmldir
    8.51 +from utils import _getAuthenticatedUser
    8.52 +from utils import _mergedLocalRoles
    8.53 +from utils import getToolByName
    8.54 +from utils import SimpleRecord
    8.55 +from utils import UniqueObject
    8.56 +
    8.57 +
    8.58 +class IndexableObjectSpecification(ObjectSpecificationDescriptor):
    8.59 +
    8.60 +    def __get__(self, inst, cls=None):
    8.61 +        if inst is None:
    8.62 +            return getObjectSpecification(cls)
    8.63 +        else:
    8.64 +            provided = providedBy(inst._IndexableObjectWrapper__ob)
    8.65 +            cls = type(inst)
    8.66 +            return ObjectSpecification(provided, cls)
    8.67 +
    8.68 +
    8.69 +class IndexableObjectWrapper(object):
    8.70 +
    8.71 +    __implements__ = IIndexableObjectWrapper
    8.72 +    __providedBy__ = IndexableObjectSpecification()
    8.73 +
    8.74 +    def __init__(self, vars, ob):
    8.75 +        self.__vars = vars
    8.76 +        self.__ob = ob
    8.77 +
    8.78 +    def __str__(self):
    8.79 +        try:
    8.80 +            # __str__ is used to get the data of File objects
    8.81 +            return self.__ob.__str__()
    8.82 +        except AttributeError:
    8.83 +            return object.__str__(self)
    8.84 +
    8.85 +    def __getattr__(self, name):
    8.86 +        vars = self.__vars
    8.87 +        if vars.has_key(name):
    8.88 +            return vars[name]
    8.89 +        return getattr(self.__ob, name)
    8.90 +
    8.91 +    def allowedRolesAndUsers(self):
    8.92 +        """
    8.93 +        Return a list of roles and users with View permission.
    8.94 +        Used by PortalCatalog to filter out items you're not allowed to see.
    8.95 +        """
    8.96 +        ob = self.__ob
    8.97 +        allowed = {}
    8.98 +        for r in rolesForPermissionOn(View, ob):
    8.99 +            allowed[r] = 1
   8.100 +        localroles = _mergedLocalRoles(ob)
   8.101 +        for user, roles in localroles.items():
   8.102 +            for role in roles:
   8.103 +                if allowed.has_key(role):
   8.104 +                    allowed['user:' + user] = 1
   8.105 +        if allowed.has_key('Owner'):
   8.106 +            del allowed['Owner']
   8.107 +        return list(allowed.keys())
   8.108 +
   8.109 +    def cmf_uid(self):
   8.110 +        """
   8.111 +        Return the CMFUid UID of the object while making sure
   8.112 +        it is not accidentally acquired.
   8.113 +        """
   8.114 +        cmf_uid = getattr(aq_base(self.__ob), 'cmf_uid', '')
   8.115 +        if safe_callable(cmf_uid):
   8.116 +            return cmf_uid()
   8.117 +        return cmf_uid
   8.118 +
   8.119 +
   8.120 +class CatalogTool(UniqueObject, ZCatalog, ActionProviderBase):
   8.121 +    """ This is a ZCatalog that filters catalog queries.
   8.122 +    """
   8.123 +
   8.124 +    __implements__ = (ICatalogTool, ZCatalog.__implements__,
   8.125 +                      ActionProviderBase.__implements__)
   8.126 +
   8.127 +    id = 'portal_catalog'
   8.128 +    meta_type = 'CMF Catalog'
   8.129 +    _actions = ()
   8.130 +
   8.131 +    security = ClassSecurityInfo()
   8.132 +
   8.133 +    manage_options = ( ZCatalog.manage_options +
   8.134 +                      ActionProviderBase.manage_options +
   8.135 +                      ({ 'label' : 'Overview', 'action' : 'manage_overview' }
   8.136 +                     ,
   8.137 +                     ))
   8.138 +
   8.139 +    def __init__(self):
   8.140 +        ZCatalog.__init__(self, self.getId())
   8.141 +        warn('CatalogTool._initIndexes is deprecated and will be removed in '
   8.142 +             'CMF 2.0.',
   8.143 +             DeprecationWarning)
   8.144 +        self._initIndexes()
   8.145 +
   8.146 +    #
   8.147 +    #   Subclass extension interface
   8.148 +    #
   8.149 +    security.declarePublic( 'enumerateIndexes' ) # Subclass can call
   8.150 +    def enumerateIndexes( self ):
   8.151 +        #   Return a list of ( index_name, type, extra ) tuples for the initial
   8.152 +        #   index set.
   8.153 +        #   Creator is deprecated and may go away, use listCreators!
   8.154 +        #   meta_type is deprecated and may go away, use portal_type!
   8.155 +        plaintext_extra = SimpleRecord( lexicon_id='plaintext_lexicon'
   8.156 +                                      , index_type='Okapi BM25 Rank'
   8.157 +                                      )
   8.158 +        htmltext_extra = SimpleRecord( lexicon_id='htmltext_lexicon'
   8.159 +                                     , index_type='Okapi BM25 Rank'
   8.160 +                                     )
   8.161 +
   8.162 +        return ( ('Title', 'ZCTextIndex', plaintext_extra)
   8.163 +               , ('Subject', 'KeywordIndex', None)
   8.164 +               , ('Description', 'ZCTextIndex', plaintext_extra)
   8.165 +               , ('Creator', 'FieldIndex', None)
   8.166 +               , ('listCreators', 'KeywordIndex', None)
   8.167 +               , ('SearchableText', 'ZCTextIndex', htmltext_extra)
   8.168 +               , ('Date', 'DateIndex', None)
   8.169 +               , ('Type', 'FieldIndex', None)
   8.170 +               , ('created', 'DateIndex', None)
   8.171 +               , ('effective', 'DateIndex', None)
   8.172 +               , ('expires', 'DateIndex', None)
   8.173 +               , ('modified', 'DateIndex', None)
   8.174 +               , ('allowedRolesAndUsers', 'KeywordIndex', None)
   8.175 +               , ('review_state', 'FieldIndex', None)
   8.176 +               , ('in_reply_to', 'FieldIndex', None)
   8.177 +               , ('meta_type', 'FieldIndex', None)
   8.178 +               , ('getId', 'FieldIndex', None)
   8.179 +               , ('path', 'PathIndex', None)
   8.180 +               , ('portal_type', 'FieldIndex', None)
   8.181 +               )
   8.182 +
   8.183 +    security.declarePublic('enumerateLexicons')
   8.184 +    def enumerateLexicons(self):
   8.185 +        return (
   8.186 +                 ( 'plaintext_lexicon'
   8.187 +                 , Splitter()
   8.188 +                 , CaseNormalizer()
   8.189 +                 , StopWordRemover()
   8.190 +                 )
   8.191 +               , ( 'htmltext_lexicon'
   8.192 +                 , HTMLWordSplitter()
   8.193 +                 , CaseNormalizer()
   8.194 +                 , StopWordRemover()
   8.195 +                 )
   8.196 +               )
   8.197 +
   8.198 +    security.declarePublic( 'enumerateColumns' )
   8.199 +    def enumerateColumns( self ):
   8.200 +        #   Return a sequence of schema names to be cached.
   8.201 +        #   Creator is deprecated and may go away, use listCreators!
   8.202 +        return ( 'Subject'
   8.203 +               , 'Title'
   8.204 +               , 'Description'
   8.205 +               , 'Type'
   8.206 +               , 'review_state'
   8.207 +               , 'Creator'
   8.208 +               , 'listCreators'
   8.209 +               , 'Date'
   8.210 +               , 'getIcon'
   8.211 +               , 'created'
   8.212 +               , 'effective'
   8.213 +               , 'expires'
   8.214 +               , 'modified'
   8.215 +               , 'CreationDate'
   8.216 +               , 'EffectiveDate'
   8.217 +               , 'ExpirationDate'
   8.218 +               , 'ModificationDate'
   8.219 +               , 'getId'
   8.220 +               , 'portal_type'
   8.221 +               )
   8.222 +
   8.223 +    def _initIndexes(self):
   8.224 +        # ZCTextIndex lexicons
   8.225 +        for id, splitter, normalizer, sw_remover in self.enumerateLexicons():
   8.226 +            lexicon = PLexicon(id, '', splitter, normalizer, sw_remover)
   8.227 +            self._setObject(id, lexicon)
   8.228 +
   8.229 +        # Content indexes
   8.230 +        self._catalog.indexes.clear()
   8.231 +        for index_name, index_type, extra in self.enumerateIndexes():
   8.232 +            self.addIndex(index_name, index_type, extra=extra)
   8.233 +
   8.234 +        # Cached metadata
   8.235 +        self._catalog.names = ()
   8.236 +        self._catalog.schema.clear()
   8.237 +        for column_name in self.enumerateColumns():
   8.238 +            self.addColumn(column_name)
   8.239 +
   8.240 +    #
   8.241 +    #   ZMI methods
   8.242 +    #
   8.243 +    security.declareProtected(ManagePortal, 'manage_overview')
   8.244 +    manage_overview = DTMLFile( 'explainCatalogTool', _dtmldir )
   8.245 +
   8.246 +    #
   8.247 +    #   'portal_catalog' interface methods
   8.248 +    #
   8.249 +
   8.250 +    def _listAllowedRolesAndUsers( self, user ):
   8.251 +        result = list( user.getRoles() )
   8.252 +        result.append( 'Anonymous' )
   8.253 +        result.append( 'user:%s' % user.getId() )
   8.254 +        return result
   8.255 +
   8.256 +    def _convertQuery(self, kw):
   8.257 +        # Convert query to modern syntax
   8.258 +        for k in 'effective', 'expires':
   8.259 +            kusage = k+'_usage'
   8.260 +            if not kw.has_key(kusage):
   8.261 +                continue
   8.262 +            usage = kw[kusage]
   8.263 +            if not usage.startswith('range:'):
   8.264 +                raise ValueError("Incorrect usage %s" % `usage`)
   8.265 +            kw[k] = {'query': kw[k], 'range': usage[6:]}
   8.266 +            del kw[kusage]
   8.267 +
   8.268 +    # searchResults has inherited security assertions.
   8.269 +    def searchResults(self, REQUEST=None, **kw):
   8.270 +        """
   8.271 +            Calls ZCatalog.searchResults with extra arguments that
   8.272 +            limit the results to what the user is allowed to see.
   8.273 +        """
   8.274 +        user = _getAuthenticatedUser(self)
   8.275 +        kw[ 'allowedRolesAndUsers' ] = self._listAllowedRolesAndUsers( user )
   8.276 +
   8.277 +        if not _checkPermission( AccessInactivePortalContent, self ):
   8.278 +            now = DateTime()
   8.279 +
   8.280 +            self._convertQuery(kw)
   8.281 +
   8.282 +            # Intersect query restrictions with those implicit to the tool
   8.283 +            for k in 'effective', 'expires':
   8.284 +                if kw.has_key(k):
   8.285 +                    range = kw[k]['range'] or ''
   8.286 +                    query = kw[k]['query']
   8.287 +                    if not isinstance(query, (tuple, list)):
   8.288 +                        query = (query,)
   8.289 +                else:
   8.290 +                    range = ''
   8.291 +                    query = None
   8.292 +                if range.find('min') > -1:
   8.293 +                    lo = min(query)
   8.294 +                else:
   8.295 +                    lo = None
   8.296 +                if range.find('max') > -1:
   8.297 +                    hi = max(query)
   8.298 +                else:
   8.299 +                    hi = None
   8.300 +                if k == 'effective':
   8.301 +                    if hi is None or hi > now:
   8.302 +                        hi = now
   8.303 +                    if lo is not None and hi < lo:
   8.304 +                        return ()
   8.305 +                else: # 'expires':
   8.306 +                    if lo is None or lo < now:
   8.307 +                        lo = now
   8.308 +                    if hi is not None and hi < lo:
   8.309 +                        return ()
   8.310 +                # Rebuild a query
   8.311 +                if lo is None:
   8.312 +                    query = hi
   8.313 +                    range = 'max'
   8.314 +                elif hi is None:
   8.315 +                    query = lo
   8.316 +                    range = 'min'
   8.317 +                else:
   8.318 +                    query = (lo, hi)
   8.319 +                    range = 'min:max'
   8.320 +                kw[k] = {'query': query, 'range': range}
   8.321 +
   8.322 +        return ZCatalog.searchResults(self, REQUEST, **kw)
   8.323 +
   8.324 +    __call__ = searchResults
   8.325 +
   8.326 +    security.declarePrivate('unrestrictedSearchResults')
   8.327 +    def unrestrictedSearchResults(self, REQUEST=None, **kw):
   8.328 +        """Calls ZCatalog.searchResults directly without restrictions.
   8.329 +
   8.330 +        This method returns every also not yet effective and already expired
   8.331 +        objects regardless of the roles the caller has.
   8.332 +
   8.333 +        CAUTION: Care must be taken not to open security holes by
   8.334 +        exposing the results of this method to non authorized callers!
   8.335 +
   8.336 +        If you're in doubt if you should use this method or
   8.337 +        'searchResults' use the latter.
   8.338 +        """
   8.339 +        return ZCatalog.searchResults(self, REQUEST, **kw)
   8.340 +
   8.341 +    def __url(self, ob):
   8.342 +        return '/'.join( ob.getPhysicalPath() )
   8.343 +
   8.344 +    manage_catalogFind = DTMLFile( 'catalogFind', _dtmldir )
   8.345 +
   8.346 +    def catalog_object(self, obj, uid=None, idxs=None, update_metadata=1,
   8.347 +                       pghandler=None):
   8.348 +        # Wraps the object with workflow and accessibility
   8.349 +        # information just before cataloging.
   8.350 +        wftool = getToolByName(self, 'portal_workflow', None)
   8.351 +        if wftool is not None:
   8.352 +            vars = wftool.getCatalogVariablesFor(obj)
   8.353 +        else:
   8.354 +            vars = {}
   8.355 +        w = IndexableObjectWrapper(vars, obj)
   8.356 +        try:
   8.357 +            ZCatalog.catalog_object(self, w, uid, idxs, update_metadata,
   8.358 +                                    pghandler)
   8.359 +        except TypeError:
   8.360 +            # BBB: for Zope 2.7
   8.361 +            ZCatalog.catalog_object(self, w, uid, idxs, update_metadata)
   8.362 +
   8.363 +    security.declarePrivate('indexObject')
   8.364 +    def indexObject(self, object):
   8.365 +        '''Add to catalog.
   8.366 +        '''
   8.367 +        url = self.__url(object)
   8.368 +        self.catalog_object(object, url)
   8.369 +
   8.370 +    security.declarePrivate('unindexObject')
   8.371 +    def unindexObject(self, object):
   8.372 +        '''Remove from catalog.
   8.373 +        '''
   8.374 +        url = self.__url(object)
   8.375 +        self.uncatalog_object(url)
   8.376 +
   8.377 +    security.declarePrivate('reindexObject')
   8.378 +    def reindexObject(self, object, idxs=[], update_metadata=1, uid=None):
   8.379 +        """Update catalog after object data has changed.
   8.380 +
   8.381 +        The optional idxs argument is a list of specific indexes
   8.382 +        to update (all of them by default).
   8.383 +
   8.384 +        The update_metadata flag controls whether the object's
   8.385 +        metadata record is updated as well.
   8.386 +
   8.387 +        If a non-None uid is passed, it will be used as the catalog uid
   8.388 +        for the object instead of its physical path.
   8.389 +        """
   8.390 +        if uid is None:
   8.391 +            uid = self.__url(object)
   8.392 +        if idxs != []:
   8.393 +            # Filter out invalid indexes.
   8.394 +            valid_indexes = self._catalog.indexes.keys()
   8.395 +            idxs = [i for i in idxs if i in valid_indexes]
   8.396 +        self.catalog_object(object, uid, idxs, update_metadata)
   8.397 +
   8.398 +InitializeClass(CatalogTool)
     9.1 new file mode 100644
     9.2 --- /dev/null
     9.3 +++ b/ContentTypeRegistry.py
     9.4 @@ -0,0 +1,571 @@
     9.5 +##############################################################################
     9.6 +#
     9.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     9.8 +#
     9.9 +# This software is subject to the provisions of the Zope Public License,
    9.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    9.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    9.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    9.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    9.14 +# FOR A PARTICULAR PURPOSE.
    9.15 +#
    9.16 +##############################################################################
    9.17 +""" Basic Site content type registry
    9.18 +
    9.19 +$Id$
    9.20 +"""
    9.21 +
    9.22 +import re, os, urllib
    9.23 +
    9.24 +from AccessControl import ClassSecurityInfo
    9.25 +from Globals import DTMLFile
    9.26 +from Globals import InitializeClass
    9.27 +from Globals import PersistentMapping
    9.28 +from OFS.SimpleItem import SimpleItem
    9.29 +from ZPublisher.mapply import mapply
    9.30 +
    9.31 +from interfaces.ContentTypeRegistry \
    9.32 +        import ContentTypeRegistry as z2IContentTypeRegistry
    9.33 +from interfaces.ContentTypeRegistry \
    9.34 +        import ContentTypeRegistryPredicate as z2IContentTypeRegistryPredicate
    9.35 +from permissions import ManagePortal
    9.36 +from utils import _dtmldir
    9.37 +from utils import getToolByName
    9.38 +
    9.39 +from zope.interface import implements
    9.40 +from interfaces import IContentTypeRegistry
    9.41 +from interfaces import IContentTypeRegistryPredicate
    9.42 +
    9.43 +class MajorMinorPredicate( SimpleItem ):
    9.44 +    """
    9.45 +        Predicate matching on 'major/minor' content types.
    9.46 +        Empty major or minor implies wildcard (all match).
    9.47 +    """
    9.48 +
    9.49 +    __implements__ = z2IContentTypeRegistryPredicate
    9.50 +    implements(IContentTypeRegistryPredicate)
    9.51 +
    9.52 +    major = minor = None
    9.53 +    PREDICATE_TYPE  = 'major_minor'
    9.54 +
    9.55 +    security = ClassSecurityInfo()
    9.56 +
    9.57 +    def __init__( self, id ):
    9.58 +        self.id = id
    9.59 +
    9.60 +    security.declareProtected( ManagePortal, 'getMajorType' )
    9.61 +    def getMajorType(self):
    9.62 +        """ Get major content types.
    9.63 +        """
    9.64 +        if self.major is None:
    9.65 +            return 'None'
    9.66 +        return ' '.join(self.major)
    9.67 +
    9.68 +    security.declareProtected( ManagePortal, 'getMinorType' )
    9.69 +    def getMinorType(self):
    9.70 +        """ Get minor content types.
    9.71 +        """
    9.72 +        if self.minor is None:
    9.73 +            return 'None'
    9.74 +        return ' '.join(self.minor)
    9.75 +
    9.76 +    security.declareProtected( ManagePortal, 'edit' )
    9.77 +    def edit( self, major, minor, COMMA_SPLIT=re.compile( r'[, ]' ) ):
    9.78 +
    9.79 +        if major == 'None':
    9.80 +            major = None
    9.81 +        if type( major ) is type( '' ):
    9.82 +            major = filter( None, COMMA_SPLIT.split( major ) )
    9.83 +
    9.84 +        if minor == 'None':
    9.85 +            minor = None
    9.86 +        if type( minor ) is type( '' ):
    9.87 +            minor = filter( None, COMMA_SPLIT.split( minor ) )
    9.88 +
    9.89 +        self.major = major
    9.90 +        self.minor = minor
    9.91 +
    9.92 +    #
    9.93 +    #   ContentTypeRegistryPredicate interface
    9.94 +    #
    9.95 +    security.declareObjectPublic()
    9.96 +    def __call__( self, name, typ, body ):
    9.97 +        """
    9.98 +            Return true if the rule matches, else false.
    9.99 +        """
   9.100 +        if self.major is None:
   9.101 +            return 0
   9.102 +
   9.103 +        if self.minor is None:
   9.104 +            return 0
   9.105 +
   9.106 +        typ = typ or '/'
   9.107 +        if not '/' in typ:
   9.108 +            typ = typ + '/'
   9.109 +        major, minor = typ.split('/', 1)
   9.110 +
   9.111 +        if self.major and not major in self.major:
   9.112 +            return 0
   9.113 +
   9.114 +        if self.minor and not minor in self.minor:
   9.115 +            return 0
   9.116 +
   9.117 +        return 1
   9.118 +
   9.119 +    security.declareProtected( ManagePortal, 'getTypeLabel' )
   9.120 +    def getTypeLabel( self ):
   9.121 +        """
   9.122 +            Return a human-readable label for the predicate type.
   9.123 +        """
   9.124 +        return self.PREDICATE_TYPE
   9.125 +
   9.126 +    security.declareProtected( ManagePortal, 'predicateWidget' )
   9.127 +    predicateWidget = DTMLFile( 'majorMinorWidget', _dtmldir )
   9.128 +
   9.129 +InitializeClass( MajorMinorPredicate )
   9.130 +
   9.131 +class ExtensionPredicate( SimpleItem ):
   9.132 +    """
   9.133 +        Predicate matching on filename extensions.
   9.134 +    """
   9.135 +
   9.136 +    __implements__ = z2IContentTypeRegistryPredicate
   9.137 +    implements(IContentTypeRegistryPredicate)
   9.138 +    
   9.139 +    extensions = None
   9.140 +    PREDICATE_TYPE  = 'extension'
   9.141 +
   9.142 +    security = ClassSecurityInfo()
   9.143 +
   9.144 +    def __init__( self, id ):
   9.145 +        self.id = id
   9.146 +
   9.147 +    security.declareProtected( ManagePortal, 'getExtensions' )
   9.148 +    def getExtensions(self):
   9.149 +        """ Get filename extensions.
   9.150 +        """
   9.151 +        if self.extensions is None:
   9.152 +            return 'None'
   9.153 +        return ' '.join(self.extensions)
   9.154 +
   9.155 +    security.declareProtected( ManagePortal, 'edit' )
   9.156 +    def edit( self, extensions, COMMA_SPLIT=re.compile( r'[, ]' ) ):
   9.157 +
   9.158 +        if extensions == 'None':
   9.159 +            extensions = None
   9.160 +        if type( extensions ) is type( '' ):
   9.161 +            extensions = filter( None, COMMA_SPLIT.split( extensions ) )
   9.162 +
   9.163 +        self.extensions = extensions
   9.164 +
   9.165 +    #
   9.166 +    #   ContentTypeRegistryPredicate interface
   9.167 +    #
   9.168 +    security.declareObjectPublic()
   9.169 +    def __call__( self, name, typ, body ):
   9.170 +        """
   9.171 +            Return true if the rule matches, else false.
   9.172 +        """
   9.173 +        if self.extensions is None:
   9.174 +            return 0
   9.175 +
   9.176 +        base, ext = os.path.splitext( name )
   9.177 +        if ext and ext[ 0 ] == '.':
   9.178 +            ext = ext[ 1: ]
   9.179 +
   9.180 +        return ext in self.extensions
   9.181 +
   9.182 +    security.declareProtected( ManagePortal, 'getTypeLabel' )
   9.183 +    def getTypeLabel( self ):
   9.184 +        """
   9.185 +            Return a human-readable label for the predicate type.
   9.186 +        """
   9.187 +        return self.PREDICATE_TYPE
   9.188 +
   9.189 +    security.declareProtected( ManagePortal, 'predicateWidget' )
   9.190 +    predicateWidget = DTMLFile( 'extensionWidget', _dtmldir )
   9.191 +
   9.192 +InitializeClass( ExtensionPredicate )
   9.193 +
   9.194 +class MimeTypeRegexPredicate( SimpleItem ):
   9.195 +    """
   9.196 +        Predicate matching only on 'typ', using regex matching for
   9.197 +        string patterns (other objects conforming to 'match' can
   9.198 +        also be passed).
   9.199 +    """
   9.200 +
   9.201 +    __implements__ = z2IContentTypeRegistryPredicate
   9.202 +    implements(IContentTypeRegistryPredicate)
   9.203 +
   9.204 +    pattern         = None
   9.205 +    PREDICATE_TYPE  = 'mimetype_regex'
   9.206 +
   9.207 +    security = ClassSecurityInfo()
   9.208 +
   9.209 +    def __init__( self, id ):
   9.210 +        self.id = id
   9.211 +
   9.212 +    security.declareProtected( ManagePortal, 'getPatternStr' )
   9.213 +    def getPatternStr( self ):
   9.214 +        if self.pattern is None:
   9.215 +            return 'None'
   9.216 +        return self.pattern.pattern
   9.217 +
   9.218 +    security.declareProtected( ManagePortal, 'edit' )
   9.219 +    def edit( self, pattern ):
   9.220 +        if pattern == 'None':
   9.221 +            pattern = None
   9.222 +        if type( pattern ) is type( '' ):
   9.223 +            pattern = re.compile( pattern )
   9.224 +        self.pattern = pattern
   9.225 +
   9.226 +    #
   9.227 +    #   ContentTypeRegistryPredicate interface
   9.228 +    #
   9.229 +    security.declareObjectPublic()
   9.230 +    def __call__( self, name, typ, body ):
   9.231 +        """
   9.232 +            Return true if the rule matches, else false.
   9.233 +        """
   9.234 +        if self.pattern is None:
   9.235 +            return 0
   9.236 +
   9.237 +        return self.pattern.match( typ )
   9.238 +
   9.239 +    security.declareProtected( ManagePortal, 'getTypeLabel' )
   9.240 +    def getTypeLabel( self ):
   9.241 +        """
   9.242 +            Return a human-readable label for the predicate type.
   9.243 +        """
   9.244 +        return self.PREDICATE_TYPE
   9.245 +
   9.246 +    security.declareProtected( ManagePortal, 'predicateWidget' )
   9.247 +    predicateWidget = DTMLFile( 'patternWidget', _dtmldir )
   9.248 +
   9.249 +InitializeClass( MimeTypeRegexPredicate )
   9.250 +
   9.251 +class NameRegexPredicate( SimpleItem ):
   9.252 +    """
   9.253 +        Predicate matching only on 'name', using regex matching
   9.254 +        for string patterns (other objects conforming to 'match'
   9.255 +        and 'pattern' can also be passed).
   9.256 +    """
   9.257 +
   9.258 +    __implements__ = z2IContentTypeRegistryPredicate
   9.259 +    implements(IContentTypeRegistryPredicate)
   9.260 +    
   9.261 +    pattern         = None
   9.262 +    PREDICATE_TYPE  = 'name_regex'
   9.263 +
   9.264 +    security = ClassSecurityInfo()
   9.265 +
   9.266 +    def __init__( self, id ):
   9.267 +        self.id = id
   9.268 +
   9.269 +    security.declareProtected( ManagePortal, 'getPatternStr' )
   9.270 +    def getPatternStr( self ):
   9.271 +        """
   9.272 +            Return a string representation of our pattern.
   9.273 +        """
   9.274 +        if self.pattern is None:
   9.275 +            return 'None'
   9.276 +        return self.pattern.pattern
   9.277 +
   9.278 +    security.declareProtected( ManagePortal, 'edit' )
   9.279 +    def edit( self, pattern ):
   9.280 +        if pattern == 'None':
   9.281 +            pattern = None
   9.282 +        if type( pattern ) is type( '' ):
   9.283 +            pattern = re.compile( pattern )
   9.284 +        self.pattern = pattern
   9.285 +
   9.286 +    #
   9.287 +    #   ContentTypeRegistryPredicate interface
   9.288 +    #
   9.289 +    security.declareObjectPublic()
   9.290 +    def __call__( self, name, typ, body ):
   9.291 +        """
   9.292 +            Return true if the rule matches, else false.
   9.293 +        """
   9.294 +        if self.pattern is None:
   9.295 +            return 0
   9.296 +
   9.297 +        return self.pattern.match( name )
   9.298 +
   9.299 +    security.declareProtected( ManagePortal, 'getTypeLabel' )
   9.300 +    def getTypeLabel( self ):
   9.301 +        """
   9.302 +            Return a human-readable label for the predicate type.
   9.303 +        """
   9.304 +        return self.PREDICATE_TYPE
   9.305 +
   9.306 +    security.declareProtected( ManagePortal, 'predicateWidget' )
   9.307 +    predicateWidget = DTMLFile( 'patternWidget', _dtmldir )
   9.308 +
   9.309 +InitializeClass( NameRegexPredicate )
   9.310 +
   9.311 +
   9.312 +_predicate_types = []
   9.313 +
   9.314 +def registerPredicateType( typeID, klass ):
   9.315 +    """
   9.316 +        Add a new predicate type.
   9.317 +    """
   9.318 +    _predicate_types.append( ( typeID, klass ) )
   9.319 +
   9.320 +for klass in ( MajorMinorPredicate
   9.321 +             , ExtensionPredicate
   9.322 +             , MimeTypeRegexPredicate
   9.323 +             , NameRegexPredicate
   9.324 +             ):
   9.325 +    registerPredicateType( klass.PREDICATE_TYPE, klass )
   9.326 +
   9.327 +
   9.328 +class ContentTypeRegistry( SimpleItem ):
   9.329 +    """
   9.330 +        Registry for rules which map PUT args to a CMF Type Object.
   9.331 +    """
   9.332 +
   9.333 +    __implements__ = z2IContentTypeRegistry
   9.334 +    implements(IContentTypeRegistry)
   9.335 +
   9.336 +    meta_type = 'Content Type Registry'
   9.337 +    id = 'content_type_registry'
   9.338 +
   9.339 +    manage_options = ( { 'label'    : 'Predicates'
   9.340 +                       , 'action'   : 'manage_predicates'
   9.341 +                       }
   9.342 +                     , { 'label'    : 'Test'
   9.343 +                       , 'action'   : 'manage_testRegistry'
   9.344 +                       }
   9.345 +                     ) + SimpleItem.manage_options
   9.346 +
   9.347 +    security = ClassSecurityInfo()
   9.348 +
   9.349 +    def __init__( self ):
   9.350 +        self.predicate_ids  = ()
   9.351 +        self.predicates     = PersistentMapping()
   9.352 +
   9.353 +    #
   9.354 +    #   ZMI
   9.355 +    #
   9.356 +    security.declarePublic( 'listPredicateTypes' )
   9.357 +    def listPredicateTypes( self ):
   9.358 +        """
   9.359 +        """
   9.360 +        return map( lambda x: x[0], _predicate_types )
   9.361 +
   9.362 +    security.declareProtected( ManagePortal, 'manage_predicates' )
   9.363 +    manage_predicates = DTMLFile( 'registryPredList', _dtmldir )
   9.364 +
   9.365 +    security.declareProtected( ManagePortal, 'doAddPredicate' )
   9.366 +    def doAddPredicate( self, predicate_id, predicate_type, REQUEST ):
   9.367 +        """
   9.368 +        """
   9.369 +        self.addPredicate( predicate_id, predicate_type )
   9.370 +        REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   9.371 +                              + '/manage_predicates'
   9.372 +                              + '?manage_tabs_message=Predicate+added.'
   9.373 +                              )
   9.374 +
   9.375 +    security.declareProtected( ManagePortal, 'doUpdatePredicate' )
   9.376 +    def doUpdatePredicate( self
   9.377 +                         , predicate_id
   9.378 +                         , predicate
   9.379 +                         , typeObjectName
   9.380 +                         , REQUEST
   9.381 +                         ):
   9.382 +        """
   9.383 +        """
   9.384 +        self.updatePredicate( predicate_id, predicate, typeObjectName )
   9.385 +        REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   9.386 +                              + '/manage_predicates'
   9.387 +                              + '?manage_tabs_message=Predicate+updated.'
   9.388 +                              )
   9.389 +
   9.390 +    security.declareProtected( ManagePortal, 'doMovePredicateUp' )
   9.391 +    def doMovePredicateUp( self, predicate_id, REQUEST ):
   9.392 +        """
   9.393 +        """
   9.394 +        predicate_ids = list( self.predicate_ids )
   9.395 +        ndx = predicate_ids.index( predicate_id )
   9.396 +        if ndx == 0:
   9.397 +            msg = "Predicate+already+first."
   9.398 +        else:
   9.399 +            self.reorderPredicate( predicate_id, ndx - 1 )
   9.400 +            msg = "Predicate+moved."
   9.401 +        REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   9.402 +                              + '/manage_predicates'
   9.403 +                              + '?manage_tabs_message=%s' % msg
   9.404 +                              )
   9.405 +
   9.406 +    security.declareProtected( ManagePortal, 'doMovePredicateDown' )
   9.407 +    def doMovePredicateDown( self, predicate_id, REQUEST ):
   9.408 +        """
   9.409 +        """
   9.410 +        predicate_ids = list( self.predicate_ids )
   9.411 +        ndx = predicate_ids.index( predicate_id )
   9.412 +        if ndx == len( predicate_ids ) - 1:
   9.413 +            msg = "Predicate+already+last."
   9.414 +        else:
   9.415 +            self.reorderPredicate( predicate_id, ndx + 1 )
   9.416 +            msg = "Predicate+moved."
   9.417 +        REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   9.418 +                              + '/manage_predicates'
   9.419 +                              + '?manage_tabs_message=%s' % msg
   9.420 +                              )
   9.421 +
   9.422 +    security.declareProtected( ManagePortal, 'doRemovePredicate' )
   9.423 +    def doRemovePredicate( self, predicate_id, REQUEST ):
   9.424 +        """
   9.425 +        """
   9.426 +        self.removePredicate( predicate_id )
   9.427 +        REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   9.428 +                              + '/manage_predicates'
   9.429 +                              + '?manage_tabs_message=Predicate+removed.'
   9.430 +                              )
   9.431 +
   9.432 +    security.declareProtected( ManagePortal, 'manage_testRegistry' )
   9.433 +    manage_testRegistry = DTMLFile( 'registryTest', _dtmldir )
   9.434 +
   9.435 +    security.declareProtected( ManagePortal, 'doTestRegistry' )
   9.436 +    def doTestRegistry( self, name, content_type, body, REQUEST ):
   9.437 +        """
   9.438 +        """
   9.439 +        typeName = self.findTypeName( name, content_type, body )
   9.440 +        if typeName is None:
   9.441 +            typeName = '<unknown>'
   9.442 +        else:
   9.443 +            types_tool = getToolByName(self, 'portal_types')
   9.444 +            typeName = types_tool.getTypeInfo(typeName).Title()
   9.445 +        REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   9.446 +                               + '/manage_testRegistry'
   9.447 +                               + '?testResults=Type:+%s'
   9.448 +                                       % urllib.quote( typeName )
   9.449 +                               )
   9.450 +
   9.451 +    #
   9.452 +    #   Predicate manipulation
   9.453 +    #
   9.454 +    security.declarePublic( 'getPredicate' )
   9.455 +    def getPredicate( self, predicate_id ):
   9.456 +        """
   9.457 +            Find the predicate whose id is 'id';  return the predicate
   9.458 +            object, if found, or else None.
   9.459 +        """
   9.460 +        return self.predicates.get( predicate_id, ( None, None ) )[0]
   9.461 +
   9.462 +    security.declarePublic( 'listPredicates' )
   9.463 +    def listPredicates( self ):
   9.464 +        """
   9.465 +            Return a sequence of tuples,
   9.466 +            '( id, ( predicate, typeObjectName ) )'
   9.467 +            for all predicates in the registry
   9.468 +        """
   9.469 +        result = []
   9.470 +        for predicate_id in self.predicate_ids:
   9.471 +            result.append( ( predicate_id, self.predicates[ predicate_id ] ) )
   9.472 +        return tuple( result )
   9.473 +
   9.474 +    security.declarePublic( 'getTypeObjectName' )
   9.475 +    def getTypeObjectName( self, predicate_id ):
   9.476 +        """
   9.477 +            Find the predicate whose id is 'id';  return the name of
   9.478 +            the type object, if found, or else None.
   9.479 +        """
   9.480 +        return self.predicates.get( predicate_id, ( None, None ) )[1]
   9.481 +
   9.482 +    security.declareProtected( ManagePortal, 'addPredicate' )
   9.483 +    def addPredicate( self, predicate_id, predicate_type ):
   9.484 +        """
   9.485 +            Add a predicate to this element of type 'typ' to the registry.
   9.486 +        """
   9.487 +        if predicate_id in self.predicate_ids:
   9.488 +            raise ValueError, "Existing predicate: %s" % predicate_id
   9.489 +
   9.490 +        klass = None
   9.491 +        for key, value in _predicate_types:
   9.492 +            if key == predicate_type:
   9.493 +                klass = value
   9.494 +
   9.495 +        if klass is None:
   9.496 +            raise ValueError, "Unknown predicate type: %s" % predicate_type
   9.497 +
   9.498 +        self.predicates[ predicate_id ] = ( klass( predicate_id ), None )
   9.499 +        self.predicate_ids = self.predicate_ids + ( predicate_id, )
   9.500 +
   9.501 +    security.declareProtected( ManagePortal, 'updatePredicate' )
   9.502 +    def updatePredicate( self, predicate_id, predicate, typeObjectName ):
   9.503 +        """
   9.504 +            Update a predicate in this element.
   9.505 +        """
   9.506 +        if not predicate_id in self.predicate_ids:
   9.507 +            raise ValueError, "Unknown predicate: %s" % predicate_id
   9.508 +
   9.509 +        predObj = self.predicates[ predicate_id ][0]
   9.510 +        mapply( predObj.edit, (), predicate.__dict__ )
   9.511 +        self.assignTypeName( predicate_id, typeObjectName )
   9.512 +
   9.513 +    security.declareProtected( ManagePortal, 'removePredicate' )
   9.514 +    def removePredicate( self, predicate_id ):
   9.515 +        """
   9.516 +            Remove a predicate from the registry.
   9.517 +        """
   9.518 +        del self.predicates[ predicate_id ]
   9.519 +        idlist = list( self.predicate_ids )
   9.520 +        ndx = idlist.index( predicate_id )
   9.521 +        idlist = idlist[ :ndx ] + idlist[ ndx+1: ]
   9.522 +        self.predicate_ids = tuple( idlist )
   9.523 +
   9.524 +    security.declareProtected( ManagePortal, 'reorderPredicate' )
   9.525 +    def reorderPredicate( self, predicate_id, newIndex ):
   9.526 +        """
   9.527 +            Move a given predicate to a new location in the list.
   9.528 +        """
   9.529 +        idlist = list( self.predicate_ids )
   9.530 +        ndx = idlist.index( predicate_id )
   9.531 +        pred = idlist[ ndx ]
   9.532 +        idlist = idlist[ :ndx ] + idlist[ ndx+1: ]
   9.533 +        idlist.insert( newIndex, pred )
   9.534 +        self.predicate_ids = tuple( idlist )
   9.535 +
   9.536 +    security.declareProtected( ManagePortal, 'assignTypeName' )
   9.537 +    def assignTypeName( self, predicate_id, typeObjectName ):
   9.538 +        """
   9.539 +            Bind the given predicate to a particular type object.
   9.540 +        """
   9.541 +        pred, oldTypeObjName = self.predicates[ predicate_id ]
   9.542 +        self.predicates[ predicate_id ] = ( pred, typeObjectName )
   9.543 +
   9.544 +    #
   9.545 +    #   ContentTypeRegistry interface
   9.546 +    #
   9.547 +    def findTypeName( self, name, typ, body ):
   9.548 +        """
   9.549 +            Perform a lookup over a collection of rules, returning the
   9.550 +            the name of the Type object corresponding to name/typ/body.
   9.551 +            Return None if no match found.
   9.552 +        """
   9.553 +        for predicate_id in self.predicate_ids:
   9.554 +            pred, typeObjectName = self.predicates[ predicate_id ]
   9.555 +            if pred( name, typ, body ):
   9.556 +                return typeObjectName
   9.557 +
   9.558 +        return None
   9.559 +
   9.560 +InitializeClass( ContentTypeRegistry )
   9.561 +
   9.562 +def manage_addRegistry( self, REQUEST=None ):
   9.563 +    """
   9.564 +        Add a CTR to self.
   9.565 +    """
   9.566 +    CTRID = ContentTypeRegistry.id
   9.567 +    reg = ContentTypeRegistry()
   9.568 +    self._setObject( CTRID, reg )
   9.569 +    reg = self._getOb( CTRID )
   9.570 +
   9.571 +    if REQUEST is not None:
   9.572 +        REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
   9.573 +                              + '/manage_main'
   9.574 +                              + '?manage_tabs_message=Registry+added.'
   9.575 +                              )
    10.1 new file mode 100644
    10.2 --- /dev/null
    10.3 +++ b/CookieCrumbler.py
    10.4 @@ -0,0 +1,439 @@
    10.5 +##############################################################################
    10.6 +#
    10.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    10.8 +# 
    10.9 +# This software is subject to the provisions of the Zope Public License,
   10.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   10.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   10.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   10.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   10.14 +# FOR A PARTICULAR PURPOSE.
   10.15 +# 
   10.16 +##############################################################################
   10.17 +""" Cookie Crumbler: Enable cookies for non-cookie user folders.
   10.18 +
   10.19 +$Id$
   10.20 +"""
   10.21 +
   10.22 +from base64 import encodestring, decodestring
   10.23 +from urllib import quote, unquote
   10.24 +import sys
   10.25 +
   10.26 +from Acquisition import aq_inner, aq_parent
   10.27 +from DateTime import DateTime
   10.28 +from AccessControl import getSecurityManager, ClassSecurityInfo, Permissions
   10.29 +from ZPublisher import BeforeTraverse
   10.30 +import Globals
   10.31 +from Globals import HTMLFile
   10.32 +from ZPublisher.HTTPRequest import HTTPRequest
   10.33 +from OFS.Folder import Folder
   10.34 +from zExceptions import Redirect
   10.35 +
   10.36 +from zope.interface import implements
   10.37 +from interfaces import ICookieCrumbler
   10.38 +
   10.39 +# Constants.
   10.40 +ATTEMPT_NONE = 0       # No attempt at authentication
   10.41 +ATTEMPT_LOGIN = 1      # Attempt to log in
   10.42 +ATTEMPT_RESUME = 2     # Attempt to resume session
   10.43 +
   10.44 +ModifyCookieCrumblers = 'Modify Cookie Crumblers'
   10.45 +ViewManagementScreens = Permissions.view_management_screens
   10.46 +
   10.47 +
   10.48 +class CookieCrumblerDisabled (Exception):
   10.49 +    """Cookie crumbler should not be used for a certain request"""
   10.50 +
   10.51 +
   10.52 +class CookieCrumbler (Folder):
   10.53 +    '''
   10.54 +    Reads cookies during traversal and simulates the HTTP
   10.55 +    authentication headers.
   10.56 +    '''
   10.57 +    meta_type = 'Cookie Crumbler'
   10.58 +
   10.59 +    implements(ICookieCrumbler)
   10.60 +
   10.61 +    security = ClassSecurityInfo()
   10.62 +    security.declareProtected(ModifyCookieCrumblers, 'manage_editProperties')
   10.63 +    security.declareProtected(ModifyCookieCrumblers, 'manage_changeProperties')
   10.64 +    security.declareProtected(ViewManagementScreens, 'manage_propertiesForm')
   10.65 +
   10.66 +    # By default, anonymous users can view login/logout pages.
   10.67 +    _View_Permission = ('Anonymous',)
   10.68 +
   10.69 +
   10.70 +    _properties = ({'id':'auth_cookie', 'type': 'string', 'mode':'w',
   10.71 +                    'label':'Authentication cookie name'},
   10.72 +                   {'id':'name_cookie', 'type': 'string', 'mode':'w',
   10.73 +                    'label':'User name form variable'},
   10.74 +                   {'id':'pw_cookie', 'type': 'string', 'mode':'w',
   10.75 +                    'label':'User password form variable'},
   10.76 +                   {'id':'persist_cookie', 'type': 'string', 'mode':'w',
   10.77 +                    'label':'User name persistence form variable'},
   10.78 +                   {'id':'auto_login_page', 'type': 'string', 'mode':'w',
   10.79 +                    'label':'Login page ID'},
   10.80 +                   {'id':'logout_page', 'type': 'string', 'mode':'w',
   10.81 +                    'label':'Logout page ID'},
   10.82 +                   {'id':'unauth_page', 'type': 'string', 'mode':'w',
   10.83 +                    'label':'Failed authorization page ID'},
   10.84 +                   {'id':'local_cookie_path', 'type': 'boolean', 'mode':'w',
   10.85 +                    'label':'Use cookie paths to limit scope'},
   10.86 +                   {'id':'cache_header_value', 'type': 'string', 'mode':'w',
   10.87 +                    'label':'Cache-Control header value'},
   10.88 +                   {'id':'log_username', 'type':'boolean', 'mode': 'w',
   10.89 +                    'label':'Log cookie auth username to access log'}
   10.90 +                   )
   10.91 +
   10.92 +    auth_cookie = '__ac'
   10.93 +    name_cookie = '__ac_name'
   10.94 +    pw_cookie = '__ac_password'
   10.95 +    persist_cookie = '__ac_persistent'
   10.96 +    auto_login_page = 'login_form'
   10.97 +    unauth_page = ''
   10.98 +    logout_page = 'logged_out'
   10.99 +    local_cookie_path = 0
  10.100 +    cache_header_value = 'private'
  10.101 +    log_username = 1
  10.102 +
  10.103 +    security.declarePrivate('delRequestVar')
  10.104 +    def delRequestVar(self, req, name):
  10.105 +        # No errors of any sort may propagate, and we don't care *what*
  10.106 +        # they are, even to log them.
  10.107 +        try: del req.other[name]
  10.108 +        except: pass
  10.109 +        try: del req.form[name]
  10.110 +        except: pass
  10.111 +        try: del req.cookies[name]
  10.112 +        except: pass
  10.113 +        try: del req.environ[name]
  10.114 +        except: pass
  10.115 +
  10.116 +    security.declarePublic('getCookiePath')
  10.117 +    def getCookiePath(self):
  10.118 +        if not self.local_cookie_path:
  10.119 +            return '/'
  10.120 +        parent = aq_parent(aq_inner(self))
  10.121 +        if parent is not None:
  10.122 +            return '/' + parent.absolute_url(1)
  10.123 +        else:
  10.124 +            return '/'
  10.125 +
  10.126 +    # Allow overridable cookie set/expiration methods.
  10.127 +    security.declarePrivate('getCookieMethod')
  10.128 +    def getCookieMethod(self, name, default=None):
  10.129 +        return getattr(self, name, default)
  10.130 +
  10.131 +    security.declarePrivate('defaultSetAuthCookie')
  10.132 +    def defaultSetAuthCookie(self, resp, cookie_name, cookie_value):
  10.133 +        kw = {}
  10.134 +        req = getattr(self, 'REQUEST', None)
  10.135 +        if req is not None and req.get('SERVER_URL', '').startswith('https:'):
  10.136 +            # Ask the client to send back the cookie only in SSL mode
  10.137 +            kw['secure'] = 'y'
  10.138 +        resp.setCookie(cookie_name, cookie_value,
  10.139 +                       path=self.getCookiePath(), **kw)
  10.140 +
  10.141 +    security.declarePrivate('defaultExpireAuthCookie')
  10.142 +    def defaultExpireAuthCookie(self, resp, cookie_name):
  10.143 +        resp.expireCookie(cookie_name, path=self.getCookiePath())
  10.144 +    
  10.145 +    def _setAuthHeader(self, ac, request, response):
  10.146 +        """Set the auth headers for both the Zope and Medusa http request
  10.147 +        objects.
  10.148 +        """
  10.149 +        request._auth = 'Basic %s' % ac
  10.150 +        response._auth = 1
  10.151 +        if self.log_username:
  10.152 +            # Set the authorization header in the medusa http request
  10.153 +            # so that the username can be logged to the Z2.log
  10.154 +            try:
  10.155 +                # Put the full-arm latex glove on now...
  10.156 +                medusa_headers = response.stdout._request._header_cache
  10.157 +            except AttributeError:
  10.158 +                pass
  10.159 +            else:
  10.160 +                medusa_headers['authorization'] = request._auth
  10.161 +
  10.162 +    security.declarePrivate('modifyRequest')
  10.163 +    def modifyRequest(self, req, resp):
  10.164 +        """Copies cookie-supplied credentials to the basic auth fields.
  10.165 +
  10.166 +        Returns a flag indicating what the user is trying to do with
  10.167 +        cookies: ATTEMPT_NONE, ATTEMPT_LOGIN, or ATTEMPT_RESUME.  If
  10.168 +        cookie login is disabled for this request, raises
  10.169 +        CookieCrumblerDisabled.
  10.170 +        """
  10.171 +        if (req.__class__ is not HTTPRequest
  10.172 +            or not req['REQUEST_METHOD'] in ('HEAD', 'GET', 'PUT', 'POST')
  10.173 +            or req.environ.has_key('WEBDAV_SOURCE_PORT')):
  10.174 +            raise CookieCrumblerDisabled
  10.175 +
  10.176 +        # attempt may contain information about an earlier attempt to
  10.177 +        # authenticate using a higher-up cookie crumbler within the
  10.178 +        # same request.
  10.179 +        attempt = getattr(req, '_cookie_auth', ATTEMPT_NONE)
  10.180 +
  10.181 +        if attempt == ATTEMPT_NONE:
  10.182 +            if req._auth:
  10.183 +                # An auth header was provided and no cookie crumbler
  10.184 +                # created it.  The user must be using basic auth.
  10.185 +                raise CookieCrumblerDisabled
  10.186 +
  10.187 +            if req.has_key(self.pw_cookie) and req.has_key(self.name_cookie):
  10.188 +                # Attempt to log in and set cookies.
  10.189 +                attempt = ATTEMPT_LOGIN
  10.190 +                name = req[self.name_cookie]
  10.191 +                pw = req[self.pw_cookie]
  10.192 +                ac = encodestring('%s:%s' % (name, pw)).rstrip()
  10.193 +                self._setAuthHeader(ac, req, resp)
  10.194 +                if req.get(self.persist_cookie, 0):
  10.195 +                    # Persist the user name (but not the pw or session)
  10.196 +                    expires = (DateTime() + 365).toZone('GMT').rfc822()
  10.197 +                    resp.setCookie(self.name_cookie, name,
  10.198 +                                   path=self.getCookiePath(),
  10.199 +                                   expires=expires)
  10.200 +                else:
  10.201 +                    # Expire the user name
  10.202 +                    resp.expireCookie(self.name_cookie,
  10.203 +                                      path=self.getCookiePath())
  10.204 +                method = self.getCookieMethod( 'setAuthCookie'
  10.205 +                                             , self.defaultSetAuthCookie )
  10.206 +                method( resp, self.auth_cookie, quote( ac ) )
  10.207 +                self.delRequestVar(req, self.name_cookie)
  10.208 +                self.delRequestVar(req, self.pw_cookie)
  10.209 +
  10.210 +            elif req.has_key(self.auth_cookie):
  10.211 +                # Attempt to resume a session if the cookie is valid.
  10.212 +                # Copy __ac to the auth header.
  10.213 +                ac = unquote(req[self.auth_cookie])
  10.214 +                if ac and ac != 'deleted':
  10.215 +                    try:
  10.216 +                        decodestring(ac)
  10.217 +                    except:
  10.218 +                        # Not a valid auth header.
  10.219 +                        pass
  10.220 +                    else:
  10.221 +                        attempt = ATTEMPT_RESUME
  10.222 +                        self._setAuthHeader(ac, req, resp)
  10.223 +                        self.delRequestVar(req, self.auth_cookie)
  10.224 +                        method = self.getCookieMethod(
  10.225 +                            'twiddleAuthCookie', None)
  10.226 +                        if method is not None:
  10.227 +                            method(resp, self.auth_cookie, quote(ac))
  10.228 +
  10.229 +        req._cookie_auth = attempt
  10.230 +        return attempt
  10.231 +
  10.232 +
  10.233 +    def __call__(self, container, req):
  10.234 +        '''The __before_publishing_traverse__ hook.'''
  10.235 +        resp = self.REQUEST['RESPONSE']
  10.236 +        try:
  10.237 +            attempt = self.modifyRequest(req, resp)
  10.238 +        except CookieCrumblerDisabled:
  10.239 +            return
  10.240 +        if req.get('disable_cookie_login__', 0):
  10.241 +            return
  10.242 +
  10.243 +        if (self.unauth_page or
  10.244 +            attempt == ATTEMPT_LOGIN or attempt == ATTEMPT_NONE):
  10.245 +            # Modify the "unauthorized" response.
  10.246 +            req._hold(ResponseCleanup(resp))
  10.247 +            resp.unauthorized = self.unauthorized
  10.248 +            resp._unauthorized = self._unauthorized
  10.249 +        if attempt != ATTEMPT_NONE:
  10.250 +            # Trying to log in or resume a session
  10.251 +            if self.cache_header_value:
  10.252 +                # we don't want caches to cache the resulting page
  10.253 +                resp.setHeader('Cache-Control', self.cache_header_value)
  10.254 +                # demystify this in the response.
  10.255 +                resp.setHeader('X-Cache-Control-Hdr-Modified-By',
  10.256 +                               'CookieCrumbler')
  10.257 +            phys_path = self.getPhysicalPath()
  10.258 +            if self.logout_page:
  10.259 +                # Cookies are in use.
  10.260 +                page = getattr(container, self.logout_page, None)
  10.261 +                if page is not None:
  10.262 +                    # Provide a logout page.
  10.263 +                    req._logout_path = phys_path + ('logout',)
  10.264 +            req._credentials_changed_path = (
  10.265 +                phys_path + ('credentialsChanged',))
  10.266 +
  10.267 +    security.declarePublic('credentialsChanged')
  10.268 +    def credentialsChanged(self, user, name, pw):
  10.269 +        ac = encodestring('%s:%s' % (name, pw)).rstrip()
  10.270 +        method = self.getCookieMethod( 'setAuthCookie'
  10.271 +                                       , self.defaultSetAuthCookie )
  10.272 +        resp = self.REQUEST['RESPONSE']
  10.273 +        method( resp, self.auth_cookie, quote( ac ) )
  10.274 +
  10.275 +    def _cleanupResponse(self):
  10.276 +        resp = self.REQUEST['RESPONSE']
  10.277 +        # No errors of any sort may propagate, and we don't care *what*
  10.278 +        # they are, even to log them.
  10.279 +        try: del resp.unauthorized
  10.280 +        except: pass
  10.281 +        try: del resp._unauthorized
  10.282 +        except: pass
  10.283 +        return resp
  10.284 +
  10.285 +    security.declarePrivate('unauthorized')
  10.286 +    def unauthorized(self):
  10.287 +        resp = self._cleanupResponse()
  10.288 +        # If we set the auth cookie before, delete it now.
  10.289 +        if resp.cookies.has_key(self.auth_cookie):
  10.290 +            del resp.cookies[self.auth_cookie]
  10.291 +        # Redirect if desired.
  10.292 +        url = self.getUnauthorizedURL()
  10.293 +        if url is not None:
  10.294 +            raise Redirect, url
  10.295 +        # Fall through to the standard unauthorized() call.
  10.296 +        resp.unauthorized()
  10.297 +
  10.298 +    def _unauthorized(self):
  10.299 +        resp = self._cleanupResponse()
  10.300 +        # If we set the auth cookie before, delete it now.
  10.301 +        if resp.cookies.has_key(self.auth_cookie):
  10.302 +            del resp.cookies[self.auth_cookie]
  10.303 +        # Redirect if desired.
  10.304 +        url = self.getUnauthorizedURL()
  10.305 +        if url is not None:
  10.306 +            resp.redirect(url, lock=1)
  10.307 +            # We don't need to raise an exception.
  10.308 +            return
  10.309 +        # Fall through to the standard _unauthorized() call.
  10.310 +        resp._unauthorized()
  10.311 +
  10.312 +    security.declarePublic('getUnauthorizedURL')
  10.313 +    def getUnauthorizedURL(self):
  10.314 +        '''
  10.315 +        Redirects to the login page.
  10.316 +        '''
  10.317 +        req = self.REQUEST
  10.318 +        resp = req['RESPONSE']
  10.319 +        attempt = getattr(req, '_cookie_auth', ATTEMPT_NONE)
  10.320 +        if attempt == ATTEMPT_NONE:
  10.321 +            # An anonymous user was denied access to something.
  10.322 +            page_id = self.auto_login_page
  10.323 +            retry = ''
  10.324 +        elif attempt == ATTEMPT_LOGIN:
  10.325 +            # The login attempt failed.  Try again.
  10.326 +            page_id = self.auto_login_page
  10.327 +            retry = '1'
  10.328 +        else:
  10.329 +            # An authenticated user was denied access to something.
  10.330 +            page_id = self.unauth_page
  10.331 +            retry = ''
  10.332 +        if page_id:
  10.333 +            page = self.restrictedTraverse(page_id, None)
  10.334 +            if page is not None:
  10.335 +                came_from = req.get('came_from', None)
  10.336 +                if came_from is None:
  10.337 +                    came_from = req.get('VIRTUAL_URL', None)
  10.338 +                    if came_from is None:
  10.339 +                        came_from = '%s%s%s' % ( req['SERVER_URL'].strip(),
  10.340 +                                                 req['SCRIPT_NAME'].strip(),
  10.341 +                                                 req['PATH_INFO'].strip() )
  10.342 +                    query = req.get('QUERY_STRING')
  10.343 +                    if query:
  10.344 +                        # Include the query string in came_from
  10.345 +                        if not query.startswith('?'):
  10.346 +                            query = '?' + query
  10.347 +                        came_from = came_from + query
  10.348 +                url = '%s?came_from=%s&retry=%s&disable_cookie_login__=1' % (
  10.349 +                    page.absolute_url(), quote(came_from), retry)
  10.350 +                return url
  10.351 +        return None
  10.352 +
  10.353 +    # backward compatible alias
  10.354 +    getLoginURL = getUnauthorizedURL
  10.355 +
  10.356 +    security.declarePublic('logout')
  10.357 +    def logout(self):
  10.358 +        '''
  10.359 +        Logs out the user and redirects to the logout page.
  10.360 +        '''
  10.361 +        req = self.REQUEST
  10.362 +        resp = req['RESPONSE']
  10.363 +        method = self.getCookieMethod( 'expireAuthCookie'
  10.364 +                                     , self.defaultExpireAuthCookie )
  10.365 +        method( resp, cookie_name=self.auth_cookie )
  10.366 +        if self.logout_page:
  10.367 +            page = self.restrictedTraverse(self.logout_page, None)
  10.368 +            if page is not None:
  10.369 +                resp.redirect('%s?disable_cookie_login__=1'
  10.370 +                              % page.absolute_url())
  10.371 +                return ''
  10.372 +        # We should not normally get here.
  10.373 +        return 'Logged out.'
  10.374 +
  10.375 +    # Installation and removal of traversal hooks.
  10.376 +
  10.377 +    def manage_beforeDelete(self, item, container):
  10.378 +        if item is self:
  10.379 +            handle = self.meta_type + '/' + self.getId()
  10.380 +            BeforeTraverse.unregisterBeforeTraverse(container, handle)
  10.381 +
  10.382 +    def manage_afterAdd(self, item, container):
  10.383 +        if item is self:
  10.384 +            handle = self.meta_type + '/' + self.getId()
  10.385 +            container = container.this()
  10.386 +            nc = BeforeTraverse.NameCaller(self.getId())
  10.387 +            BeforeTraverse.registerBeforeTraverse(container, nc, handle)
  10.388 +
  10.389 +    security.declarePublic('propertyLabel')
  10.390 +    def propertyLabel(self, id):
  10.391 +        """Return a label for the given property id
  10.392 +        """
  10.393 +        for p in self._properties:
  10.394 +            if p['id'] == id:
  10.395 +                return p.get('label', id)
  10.396 +        return id
  10.397 +
  10.398 +Globals.InitializeClass(CookieCrumbler)
  10.399 +
  10.400 +
  10.401 +class ResponseCleanup:
  10.402 +    def __init__(self, resp):
  10.403 +        self.resp = resp
  10.404 +
  10.405 +    def __del__(self):
  10.406 +        # Free the references.
  10.407 +        #
  10.408 +        # No errors of any sort may propagate, and we don't care *what*
  10.409 +        # they are, even to log them.
  10.410 +        try: del self.resp.unauthorized
  10.411 +        except: pass
  10.412 +        try: del self.resp._unauthorized
  10.413 +        except: pass
  10.414 +        try: del self.resp
  10.415 +        except: pass
  10.416 +
  10.417 +
  10.418 +manage_addCCForm = HTMLFile('dtml/addCC', globals())
  10.419 +manage_addCCForm.__name__ = 'addCC'
  10.420 +
  10.421 +def _create_forms(ob):
  10.422 +    ''' Create default forms inside ob '''
  10.423 +    import os
  10.424 +    from OFS.DTMLMethod import addDTMLMethod
  10.425 +    dtmldir = os.path.join(os.path.dirname(__file__), 'dtml')
  10.426 +    for fn in ('index_html', 'logged_in', 'logged_out', 'login_form',
  10.427 +                'standard_login_footer', 'standard_login_header'):
  10.428 +        filename = os.path.join(dtmldir, fn + '.dtml')
  10.429 +        f = open(filename, 'rt')
  10.430 +        try: data = f.read()
  10.431 +        finally: f.close()
  10.432 +        addDTMLMethod(ob, fn, file=data)
  10.433 +
  10.434 +def manage_addCC(dispatcher, id, create_forms=0, REQUEST=None):
  10.435 +    ' '
  10.436 +    ob = CookieCrumbler()
  10.437 +    ob.id = id
  10.438 +    dispatcher._setObject(ob.getId(), ob)
  10.439 +    ob = getattr(dispatcher.this(), ob.getId())
  10.440 +    if create_forms:
  10.441 +        _create_forms(ob)
  10.442 +    if REQUEST is not None:
  10.443 +        return dispatcher.manage_main(dispatcher, REQUEST)
    11.1 new file mode 100644
    11.2 --- /dev/null
    11.3 +++ b/DEPENDENCIES.txt
    11.4 @@ -0,0 +1,3 @@
    11.5 +Zope >= 2.8.5
    11.6 +Five >= 1.2
    11.7 +GenericSetup
    12.1 new file mode 100644
    12.2 --- /dev/null
    12.3 +++ b/DirectoryView.py
    12.4 @@ -0,0 +1,557 @@
    12.5 +##############################################################################
    12.6 +#
    12.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    12.8 +#
    12.9 +# This software is subject to the provisions of the Zope Public License,
   12.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   12.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   12.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   12.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   12.14 +# FOR A PARTICULAR PURPOSE.
   12.15 +#
   12.16 +##############################################################################
   12.17 +""" Views of filesystem directories as folders.
   12.18 +
   12.19 +$Id$
   12.20 +"""
   12.21 +
   12.22 +import re
   12.23 +from os import path, listdir, stat
   12.24 +from sys import exc_info
   12.25 +from sys import platform
   12.26 +import logging
   12.27 +from warnings import warn
   12.28 +
   12.29 +from AccessControl import ClassSecurityInfo
   12.30 +from Acquisition import aq_inner, aq_parent
   12.31 +from Globals import DevelopmentMode
   12.32 +from Globals import DTMLFile
   12.33 +from Globals import HTMLFile
   12.34 +from Globals import InitializeClass
   12.35 +from Globals import package_home
   12.36 +from Globals import Persistent
   12.37 +from OFS.Folder import Folder
   12.38 +from OFS.ObjectManager import bad_id
   12.39 +from zope.interface import implements
   12.40 +
   12.41 +from FSMetadata import FSMetadata
   12.42 +from FSObject import BadFile
   12.43 +from interfaces import IDirectoryView
   12.44 +from permissions import AccessContentsInformation
   12.45 +from permissions import ManagePortal
   12.46 +from utils import _dtmldir
   12.47 +from utils import expandpath as _new_expandpath
   12.48 +from utils import minimalpath
   12.49 +from utils import normalize
   12.50 +
   12.51 +
   12.52 +logger = logging.getLogger('CMFCore.DirectoryView')
   12.53 +
   12.54 +
   12.55 +def expandpath(p):
   12.56 +    """ utils.expandpath() wrapper for backwards compatibility.
   12.57 +    """
   12.58 +    warn('expandpath() doesn\'t belong to DirectoryView anymore and will be '
   12.59 +         'removed from that module in CMF 2.0. Please import expandpath from '
   12.60 +         'the utils module.',
   12.61 +         DeprecationWarning)
   12.62 +    return _new_expandpath(p)
   12.63 +
   12.64 +__reload_module__ = 0
   12.65 +
   12.66 +# Ignore filesystem artifacts
   12.67 +base_ignore = ('.', '..')
   12.68 +# Ignore version control subdirectories
   12.69 +ignore = ('CVS', 'SVN', '.', '..', '.svn')
   12.70 +# Ignore suspected backups and hidden files
   12.71 +ignore_re = re.compile(r'\.|(.*~$)|#')
   12.72 +
   12.73 +# and special names.
   12.74 +def _filtered_listdir(path, ignore=ignore):
   12.75 +    return [ name
   12.76 +             for name
   12.77 +             in listdir(path)
   12.78 +             if name not in ignore and not ignore_re.match(name) ]
   12.79 +
   12.80 +class _walker:
   12.81 +    def __init__(self, ignore=ignore):
   12.82 +        # make a dict for faster lookup
   12.83 +        self.ignore = dict([(x, None) for x in ignore])
   12.84 +
   12.85 +    def __call__(self, listdir, dirname, names):
   12.86 +        # filter names inplace, so filtered directories don't get visited
   12.87 +        names[:] = [ name
   12.88 +                     for name
   12.89 +                     in names
   12.90 +                     if name not in self.ignore and not ignore_re.match(name) ]
   12.91 +        # append with stat info
   12.92 +        results = [ (name, stat(path.join(dirname,name))[8])
   12.93 +                    for name in names ]
   12.94 +        listdir.extend(results)
   12.95 +
   12.96 +class DirectoryInformation:
   12.97 +    data = None
   12.98 +    _v_last_read = 0
   12.99 +    _v_last_filelist = [] # Only used on Win32
  12.100 +
  12.101 +    def __init__(self, filepath, minimal_fp, ignore=ignore):
  12.102 +        self._filepath = filepath
  12.103 +        self._minimal_fp = minimal_fp
  12.104 +        self.ignore=base_ignore + tuple(ignore)
  12.105 +        if platform == 'win32':
  12.106 +            self._walker = _walker(self.ignore)
  12.107 +        subdirs = []
  12.108 +        for entry in _filtered_listdir(self._filepath, ignore=self.ignore):
  12.109 +           entry_filepath = path.join(self._filepath, entry)
  12.110 +           if path.isdir(entry_filepath):
  12.111 +               subdirs.append(entry)
  12.112 +        self.subdirs = tuple(subdirs)
  12.113 +
  12.114 +    def getSubdirs(self):
  12.115 +        return self.subdirs
  12.116 +
  12.117 +    def _isAllowableFilename(self, entry):
  12.118 +        if entry[-1:] == '~':
  12.119 +            return 0
  12.120 +        if entry[:1] in ('_', '#'):
  12.121 +            return 0
  12.122 +        return 1
  12.123 +
  12.124 +    def reload(self):
  12.125 +        self.data = None
  12.126 +
  12.127 +    def _readTypesFile(self):
  12.128 +        """ Read the .objects file produced by FSDump.
  12.129 +        """
  12.130 +        types = {}
  12.131 +        try:
  12.132 +            f = open( path.join(self._filepath, '.objects'), 'rt' )
  12.133 +        except IOError:
  12.134 +            pass
  12.135 +        else:
  12.136 +            lines = f.readlines()
  12.137 +            f.close()
  12.138 +            for line in lines:
  12.139 +                try:
  12.140 +                    obname, meta_type = line.split(':')
  12.141 +                except ValueError:
  12.142 +                    pass
  12.143 +                else:
  12.144 +                    types[obname.strip()] = meta_type.strip()
  12.145 +        return types
  12.146 +
  12.147 +    if DevelopmentMode:
  12.148 +
  12.149 +        def _changed(self):
  12.150 +            mtime=0
  12.151 +            filelist=[]
  12.152 +            try:
  12.153 +                mtime = stat(self._filepath)[8]
  12.154 +                if platform == 'win32':
  12.155 +                    # some Windows directories don't change mtime
  12.156 +                    # when a file is added to or deleted from them :-(
  12.157 +                    # So keep a list of files as well, and see if that
  12.158 +                    # changes
  12.159 +                    path.walk(self._filepath, self._walker, filelist)
  12.160 +                    filelist.sort()
  12.161 +            except:
  12.162 +                logger.exception("Error checking for directory modification")
  12.163 +
  12.164 +            if mtime != self._v_last_read or filelist != self._v_last_filelist:
  12.165 +                self._v_last_read = mtime
  12.166 +                self._v_last_filelist = filelist
  12.167 +
  12.168 +                return 1
  12.169 +
  12.170 +            return 0
  12.171 +
  12.172 +    else:
  12.173 +
  12.174 +        def _changed(self):
  12.175 +            return 0
  12.176 +
  12.177 +    def getContents(self, registry):
  12.178 +        changed = self._changed()
  12.179 +        if self.data is None or changed:
  12.180 +            try:
  12.181 +                self.data, self.objects = self.prepareContents(registry,
  12.182 +                    register_subdirs=changed)
  12.183 +            except:
  12.184 +                logger.exception("Error during prepareContents")
  12.185 +                self.data = {}
  12.186 +                self.objects = ()
  12.187 +
  12.188 +        return self.data, self.objects
  12.189 +
  12.190 +    def prepareContents(self, registry, register_subdirs=0):
  12.191 +        # Creates objects for each file.
  12.192 +        data = {}
  12.193 +        objects = []
  12.194 +        types = self._readTypesFile()
  12.195 +        for entry in _filtered_listdir(self._filepath, ignore=self.ignore):
  12.196 +            if not self._isAllowableFilename(entry):
  12.197 +                continue
  12.198 +            entry_minimal_fp = '/'.join( (self._minimal_fp, entry) )
  12.199 +            entry_filepath = path.join(self._filepath, entry)
  12.200 +            if path.isdir(entry_filepath):
  12.201 +                # Add a subdirectory only if it was previously registered,
  12.202 +                # unless register_subdirs is set.
  12.203 +                info = registry.getDirectoryInfo(entry_minimal_fp)
  12.204 +                if info is None and register_subdirs:
  12.205 +                    # Register unknown subdirs
  12.206 +                    registry.registerDirectoryByPath(entry_filepath)
  12.207 +                    info = registry.getDirectoryInfo(entry_minimal_fp)
  12.208 +                if info is not None:
  12.209 +                    mt = types.get(entry)
  12.210 +                    t = None
  12.211 +                    if mt is not None:
  12.212 +                        t = registry.getTypeByMetaType(mt)
  12.213 +                    if t is None:
  12.214 +                        t = DirectoryView
  12.215 +                    ob = t(entry, entry_minimal_fp)
  12.216 +                    ob_id = ob.getId()
  12.217 +                    data[ob_id] = ob
  12.218 +                    objects.append({'id': ob_id, 'meta_type': ob.meta_type})
  12.219 +            else:
  12.220 +                pos = entry.rfind('.')
  12.221 +                if pos >= 0:
  12.222 +                    name = entry[:pos]
  12.223 +                    ext = path.normcase(entry[pos + 1:])
  12.224 +                else:
  12.225 +                    name = entry
  12.226 +                    ext = ''
  12.227 +                if not name or name == 'REQUEST':
  12.228 +                    # Not an allowable id.
  12.229 +                    continue
  12.230 +                mo = bad_id(name)
  12.231 +                if mo is not None and mo != -1:  # Both re and regex formats
  12.232 +                    # Not an allowable id.
  12.233 +                    continue
  12.234 +                t = None
  12.235 +                mt = types.get(entry, None)
  12.236 +                if mt is None:
  12.237 +                    mt = types.get(name, None)
  12.238 +                if mt is not None:
  12.239 +                    t = registry.getTypeByMetaType(mt)
  12.240 +                if t is None:
  12.241 +                    t = registry.getTypeByExtension(ext)
  12.242 +
  12.243 +                if t is not None:
  12.244 +                    metadata = FSMetadata(entry_filepath)
  12.245 +                    metadata.read()
  12.246 +                    try:
  12.247 +                        ob = t(name, entry_minimal_fp, fullname=entry,
  12.248 +                               properties=metadata.getProperties())
  12.249 +                    except:
  12.250 +                        import traceback
  12.251 +                        typ, val, tb = exc_info()
  12.252 +                        try:
  12.253 +                            logger.exception("prepareContents")
  12.254 +
  12.255 +                            exc_lines = traceback.format_exception( typ,
  12.256 +                                                                    val,
  12.257 +                                                                    tb )
  12.258 +                            ob = BadFile( name,
  12.259 +                                          entry_minimal_fp,
  12.260 +                                          exc_str='\r\n'.join(exc_lines),
  12.261 +                                          fullname=entry )
  12.262 +                        finally:
  12.263 +                            tb = None   # Avoid leaking frame!
  12.264 +
  12.265 +                    # FS-based security
  12.266 +                    permissions = metadata.getSecurity()
  12.267 +                    if permissions is not None:
  12.268 +                        for name in permissions.keys():
  12.269 +                            acquire, roles = permissions[name]
  12.270 +                            try:
  12.271 +                                ob.manage_permission(name,roles,acquire)
  12.272 +                            except ValueError:
  12.273 +                                logger.exception("Error setting permissions")
  12.274 +
  12.275 +                    # only DTML Methods and Python Scripts can have proxy roles
  12.276 +                    if hasattr(ob, '_proxy_roles'):
  12.277 +                        try:
  12.278 +                            ob._proxy_roles = tuple(metadata.getProxyRoles())
  12.279 +                        except:
  12.280 +                            logger.exception("Error setting proxy role")
  12.281 +
  12.282 +                    ob_id = ob.getId()
  12.283 +                    data[ob_id] = ob
  12.284 +                    objects.append({'id': ob_id, 'meta_type': ob.meta_type})
  12.285 +
  12.286 +        return data, tuple(objects)
  12.287 +
  12.288 +
  12.289 +class DirectoryRegistry:
  12.290 +
  12.291 +    def __init__(self):
  12.292 +        self._meta_types = {}
  12.293 +        self._object_types = {}
  12.294 +        self._directories = {}
  12.295 +
  12.296 +    def registerFileExtension(self, ext, klass):
  12.297 +        self._object_types[ext] = klass
  12.298 +
  12.299 +    def registerMetaType(self, mt, klass):
  12.300 +        self._meta_types[mt] = klass
  12.301 +
  12.302 +    def getTypeByExtension(self, ext):
  12.303 +        return self._object_types.get(ext, None)
  12.304 +
  12.305 +    def getTypeByMetaType(self, mt):
  12.306 +        return self._meta_types.get(mt, None)
  12.307 +
  12.308 +    def registerDirectory(self, name, _prefix, subdirs=1, ignore=ignore):
  12.309 +        # This what is actually called to register a
  12.310 +        # file system directory to become a FSDV.
  12.311 +        if not isinstance(_prefix, basestring):
  12.312 +            _prefix = package_home(_prefix)
  12.313 +        filepath = path.join(_prefix, name)
  12.314 +        self.registerDirectoryByPath(filepath, subdirs, ignore=ignore)
  12.315 +
  12.316 +    def registerDirectoryByPath(self, filepath, subdirs=1, ignore=ignore):
  12.317 +        # This is indirectly called during registration of
  12.318 +        # a directory. As you can see, minimalpath is called
  12.319 +        # on the supplied path at this point.
  12.320 +        # The idea is that the registry will only contain
  12.321 +        # small paths that are likely to work across platforms
  12.322 +        # and SOFTWARE_HOME, INSTANCE_HOME and PRODUCTS_PATH setups
  12.323 +        minimal_fp = minimalpath(filepath)
  12.324 +        info = DirectoryInformation(filepath, minimal_fp, ignore=ignore)
  12.325 +        self._directories[minimal_fp] = info
  12.326 +        if subdirs:
  12.327 +            for entry in info.getSubdirs():
  12.328 +                entry_filepath = path.join(filepath, entry)
  12.329 +                self.registerDirectoryByPath( entry_filepath
  12.330 +                                            , subdirs
  12.331 +                                            , ignore=ignore
  12.332 +                                            )
  12.333 +
  12.334 +    def reloadDirectory(self, minimal_fp):
  12.335 +        info = self.getDirectoryInfo(minimal_fp)
  12.336 +        if info is not None:
  12.337 +            info.reload()
  12.338 +
  12.339 +    def getDirectoryInfo(self, minimal_fp):
  12.340 +        # This is called when we need to get hold of the information
  12.341 +        # for a minimal path. Can return None.
  12.342 +        return self._directories.get(minimal_fp, None)
  12.343 +
  12.344 +    def listDirectories(self):
  12.345 +        dirs = self._directories.keys()
  12.346 +        dirs.sort()
  12.347 +        return dirs
  12.348 +
  12.349 +
  12.350 +_dirreg = DirectoryRegistry()
  12.351 +registerDirectory = _dirreg.registerDirectory
  12.352 +registerFileExtension = _dirreg.registerFileExtension
  12.353 +registerMetaType = _dirreg.registerMetaType
  12.354 +
  12.355 +
  12.356 +def listFolderHierarchy(ob, path, rval, adding_meta_type=None):
  12.357 +    if not hasattr(ob, 'objectValues'):
  12.358 +        return
  12.359 +    values = ob.objectValues()
  12.360 +    for subob in ob.objectValues():
  12.361 +        base = getattr(subob, 'aq_base', subob)
  12.362 +        if getattr(base, 'isPrincipiaFolderish', 0):
  12.363 +
  12.364 +            if adding_meta_type is not None and hasattr(
  12.365 +                base, 'filtered_meta_types'):
  12.366 +                # Include only if the user is allowed to
  12.367 +                # add the given meta type in this location.
  12.368 +                meta_types = subob.filtered_meta_types()
  12.369 +                found = 0
  12.370 +                for mt in meta_types:
  12.371 +                    if mt['name'] == adding_meta_type:
  12.372 +                        found = 1
  12.373 +                        break
  12.374 +                if not found:
  12.375 +                    continue
  12.376 +
  12.377 +            if path:
  12.378 +                subpath = path + '/' + subob.getId()
  12.379 +            else:
  12.380 +                subpath = subob.getId()
  12.381 +            title = getattr(subob, 'title', None)
  12.382 +            if title:
  12.383 +                name = '%s (%s)' % (subpath, title)
  12.384 +            else:
  12.385 +                name = subpath
  12.386 +            rval.append((subpath, name))
  12.387 +            listFolderHierarchy(subob, subpath, rval, adding_meta_type)
  12.388 +
  12.389 +
  12.390 +class DirectoryView (Persistent):
  12.391 +    """ Directory views mount filesystem directories.
  12.392 +    """
  12.393 +
  12.394 +    implements(IDirectoryView)
  12.395 +
  12.396 +    meta_type = 'Filesystem Directory View'
  12.397 +    _dirpath = None
  12.398 +    _objects = ()
  12.399 +
  12.400 +    def __init__(self, id, dirpath='', fullname=None):
  12.401 +        self.id = id
  12.402 +        self._dirpath = dirpath
  12.403 +
  12.404 +    def __of__(self, parent):
  12.405 +        dirpath = self._dirpath
  12.406 +        info = _dirreg.getDirectoryInfo(dirpath)
  12.407 +        if info is None:
  12.408 +            # for DirectoryViews created with CMF versions before 1.5
  12.409 +            # this is basically the old minimalpath() code
  12.410 +            dirpath = normalize(dirpath)
  12.411 +            index = dirpath.rfind('Products')
  12.412 +            if index == -1:
  12.413 +                index = dirpath.rfind('products')
  12.414 +            if index != -1:
  12.415 +                dirpath = dirpath[index+len('products/'):]
  12.416 +            info = _dirreg.getDirectoryInfo(dirpath)
  12.417 +            if info is not None:
  12.418 +                # update the directory view with a corrected path
  12.419 +                self._dirpath = dirpath
  12.420 +            elif self._dirpath:
  12.421 +                warn('DirectoryView %s refers to a non-existing path %s'
  12.422 +                     % (self.id, dirpath), UserWarning)
  12.423 +        if info is None:
  12.424 +            data = {}
  12.425 +            objects = ()
  12.426 +        else:
  12.427 +            data, objects = info.getContents(_dirreg)
  12.428 +        s = DirectoryViewSurrogate(self, data, objects)
  12.429 +        res = s.__of__(parent)
  12.430 +        return res
  12.431 +
  12.432 +    def getId(self):
  12.433 +        return self.id
  12.434 +
  12.435 +InitializeClass(DirectoryView)
  12.436 +
  12.437 +
  12.438 +class DirectoryViewSurrogate (Folder):
  12.439 +    """ Folderish DirectoryView.
  12.440 +    """
  12.441 +
  12.442 +    implements(IDirectoryView)
  12.443 +
  12.444 +    meta_type = 'Filesystem Directory View'
  12.445 +    all_meta_types = ()
  12.446 +    _isDirectoryView = 1
  12.447 +
  12.448 +#    _is_wrapperish = 1
  12.449 +
  12.450 +    security = ClassSecurityInfo()
  12.451 +
  12.452 +    def __init__(self, real, data, objects):
  12.453 +        d = self.__dict__
  12.454 +        d.update(data)
  12.455 +        d.update(real.__dict__)
  12.456 +        d['_real'] = real
  12.457 +        d['_objects'] = objects
  12.458 +
  12.459 +    def __setattr__(self, name, value):
  12.460 +        d = self.__dict__
  12.461 +        d[name] = value
  12.462 +        setattr(d['_real'], name, value)
  12.463 +
  12.464 +    def __delattr__(self, name):
  12.465 +        d = self.__dict__
  12.466 +        del d[name]
  12.467 +        delattr(d['_real'], name)
  12.468 +
  12.469 +    security.declareProtected(ManagePortal, 'manage_propertiesForm')
  12.470 +    manage_propertiesForm = DTMLFile( 'dirview_properties', _dtmldir )
  12.471 +
  12.472 +    security.declareProtected(ManagePortal, 'manage_properties')
  12.473 +    def manage_properties( self, dirpath, REQUEST=None ):
  12.474 +        """ Update the directory path of the DirectoryView.
  12.475 +        """
  12.476 +        self.__dict__['_real']._dirpath = dirpath
  12.477 +        if REQUEST is not None:
  12.478 +            REQUEST['RESPONSE'].redirect( '%s/manage_propertiesForm'
  12.479 +                                        % self.absolute_url() )
  12.480 +
  12.481 +    security.declareProtected(AccessContentsInformation, 'getCustomizableObject')
  12.482 +    def getCustomizableObject(self):
  12.483 +        ob = aq_parent(aq_inner(self))
  12.484 +        while getattr(ob, '_isDirectoryView', 0):
  12.485 +            ob = aq_parent(aq_inner(ob))
  12.486 +        return ob
  12.487 +
  12.488 +    security.declareProtected(AccessContentsInformation, 'listCustFolderPaths')
  12.489 +    def listCustFolderPaths(self, adding_meta_type=None):
  12.490 +        """ List possible customization folders as key, value pairs.
  12.491 +        """
  12.492 +        rval = []
  12.493 +        ob = self.getCustomizableObject()
  12.494 +        listFolderHierarchy(ob, '', rval, adding_meta_type)
  12.495 +        rval.sort()
  12.496 +        return rval
  12.497 +
  12.498 +    security.declareProtected(AccessContentsInformation, 'getDirPath')
  12.499 +    def getDirPath(self):
  12.500 +        return self.__dict__['_real']._dirpath
  12.501 +
  12.502 +    security.declarePublic('getId')
  12.503 +    def getId(self):
  12.504 +        return self.id
  12.505 +
  12.506 +InitializeClass(DirectoryViewSurrogate)
  12.507 +
  12.508 +
  12.509 +manage_addDirectoryViewForm = HTMLFile('dtml/addFSDirView', globals())
  12.510 +
  12.511 +def createDirectoryView(parent, minimal_fp, id=None):
  12.512 +    """ Add either a DirectoryView or a derivative object.
  12.513 +    """
  12.514 +    info = _dirreg.getDirectoryInfo(minimal_fp)
  12.515 +    if info is None:
  12.516 +        fixed_minimal_fp = minimal_fp.replace('\\','/')
  12.517 +        info = _dirreg.getDirectoryInfo(fixed_minimal_fp)
  12.518 +        if info is None:
  12.519 +            raise ValueError('Not a registered directory: %s' % minimal_fp)
  12.520 +        else:
  12.521 +            warn('createDirectoryView() expects a slash-separated path '
  12.522 +                 'relative to the Products path. \'%s\' will no longer work '
  12.523 +                 'in CMF 2.0.' % minimal_fp,
  12.524 +                 DeprecationWarning)
  12.525 +        minimal_fp = fixed_minimal_fp
  12.526 +    if not id:
  12.527 +        id = minimal_fp.split('/')[-1]
  12.528 +    else:
  12.529 +        id = str(id)
  12.530 +    ob = DirectoryView(id, minimal_fp)
  12.531 +    parent._setObject(id, ob)
  12.532 +
  12.533 +def addDirectoryViews(ob, name, _prefix):
  12.534 +    """ Add a directory view for every subdirectory of the given directory.
  12.535 +
  12.536 +    Meant to be called by filesystem-based code. Note that registerDirectory()
  12.537 +    still needs to be called by product initialization code to satisfy
  12.538 +    persistence demands.
  12.539 +    """
  12.540 +    if not isinstance(_prefix, basestring):
  12.541 +        _prefix = package_home(_prefix)
  12.542 +    filepath = path.join(_prefix, name)
  12.543 +    minimal_fp = minimalpath(filepath)
  12.544 +    info = _dirreg.getDirectoryInfo(minimal_fp)
  12.545 +    if info is None:
  12.546 +        raise ValueError('Not a registered directory: %s' % minimal_fp)
  12.547 +    for entry in info.getSubdirs():
  12.548 +        entry_minimal_fp = '/'.join( (minimal_fp, entry) )
  12.549 +        createDirectoryView(ob, entry_minimal_fp, entry)
  12.550 +
  12.551 +def manage_addDirectoryView(self, dirpath, id=None, REQUEST=None):
  12.552 +    """ Add either a DirectoryView or a derivative object.
  12.553 +    """
  12.554 +    createDirectoryView(self, dirpath, id)
  12.555 +    if REQUEST is not None:
  12.556 +        return self.manage_main(self, REQUEST)
  12.557 +
  12.558 +def manage_listAvailableDirectories(*args):
  12.559 +    """ List registered directories.
  12.560 +    """
  12.561 +    return list(_dirreg.listDirectories())
    13.1 new file mode 100644
    13.2 --- /dev/null
    13.3 +++ b/DiscussionTool.py
    13.4 @@ -0,0 +1,183 @@
    13.5 +##############################################################################
    13.6 +#
    13.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    13.8 +#
    13.9 +# This software is subject to the provisions of the Zope Public License,
   13.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   13.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   13.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   13.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   13.14 +# FOR A PARTICULAR PURPOSE.
   13.15 +#
   13.16 +##############################################################################
   13.17 +""" Basic portal discussion access tool.
   13.18 +
   13.19 +$Id$
   13.20 +"""
   13.21 +
   13.22 +from OFS.SimpleItem import SimpleItem
   13.23 +from Globals import InitializeClass, DTMLFile
   13.24 +from Acquisition import Implicit
   13.25 +from AccessControl import ClassSecurityInfo
   13.26 +
   13.27 +from ActionProviderBase import ActionProviderBase
   13.28 +from permissions import AccessContentsInformation
   13.29 +from permissions import ManagePortal
   13.30 +from permissions import ReplyToItem
   13.31 +from permissions import View
   13.32 +from interfaces.Discussions import OldDiscussable as IOldDiscussable
   13.33 +from interfaces.portal_discussion \
   13.34 +        import oldstyle_portal_discussion as IOldstyleDiscussionTool
   13.35 +from utils import _dtmldir
   13.36 +from utils import getToolByName
   13.37 +from utils import UniqueObject
   13.38 +
   13.39 +
   13.40 +class OldDiscussable(Implicit):
   13.41 +    """
   13.42 +        Adapter for PortalContent to implement "old-style" discussions.
   13.43 +    """
   13.44 +
   13.45 +    __implements__ = IOldDiscussable
   13.46 +
   13.47 +    _isDiscussable = 1
   13.48 +
   13.49 +    security = ClassSecurityInfo()
   13.50 +    
   13.51 +
   13.52 +    def __init__( self, content ):
   13.53 +        self.content = content
   13.54 +
   13.55 +    security.declareProtected(ReplyToItem, 'createReply')
   13.56 +    def createReply(self, title, text, REQUEST, RESPONSE):
   13.57 +        """
   13.58 +            Create a reply in the proper place
   13.59 +        """
   13.60 +
   13.61 +        location, id = self.getReplyLocationAndID(REQUEST)
   13.62 +        location.addDiscussionItem(id, title, title, 'structured-text',
   13.63 +                                   text, self.content)
   13.64 +
   13.65 +        RESPONSE.redirect( self.absolute_url() + '/view' )
   13.66 +
   13.67 +    def getReplyLocationAndID(self, REQUEST):
   13.68 +        # It is not yet clear to me what the correct location for this hook is
   13.69 +
   13.70 +        # Find the folder designated for replies, creating if missing
   13.71 +        membershiptool = getToolByName(self.content, 'portal_membership')
   13.72 +        home = membershiptool.getHomeFolder()
   13.73 +        if not hasattr(home, 'Correspondence'):
   13.74 +            home.manage_addPortalFolder('Correspondence')
   13.75 +        location = home.Correspondence
   13.76 +        location.manage_permission(View, ['Anonymous'], 1)
   13.77 +        location.manage_permission(AccessContentsInformation, ['Anonymous'], 1)
   13.78 +
   13.79 +        # Find an unused id in location
   13.80 +        id = int(DateTime().timeTime())
   13.81 +        while hasattr(location, `id`):
   13.82 +            id = id + 1
   13.83 +
   13.84 +        return location, `id`
   13.85 +
   13.86 +    security.declareProtected(View, 'getReplyResults')
   13.87 +    def getReplyResults(self):
   13.88 +        """
   13.89 +            Return the ZCatalog results that represent this object's replies.
   13.90 +
   13.91 +            Often, the actual objects are not needed.  This is less expensive
   13.92 +            than fetching the objects.
   13.93 +        """
   13.94 +        catalog = getToolByName(self.content, 'portal_catalog')
   13.95 +        return catalog.searchResults(in_reply_to=
   13.96 +                                      urllib.unquote('/'+self.absolute_url(1)))
   13.97 +
   13.98 +    security.declareProtected(View, 'getReplies')
   13.99 +    def getReplies(self):
  13.100 +        """
  13.101 +            Return a sequence of the DiscussionResponse objects which are
  13.102 +            associated with this Discussable
  13.103 +        """
  13.104 +        catalog = getToolByName(self.content, 'portal_catalog')
  13.105 +        results = self.getReplyResults()
  13.106 +        rids    = map(lambda x: x.data_record_id_, results)
  13.107 +        objects = map(catalog.getobject, rids)
  13.108 +        return objects
  13.109 +
  13.110 +    def quotedContents(self):
  13.111 +        """
  13.112 +            Return this object's contents in a form suitable for inclusion
  13.113 +            as a quote in a response.
  13.114 +        """
  13.115 +
  13.116 +        return ""
  13.117 +
  13.118 +
  13.119 +class DiscussionTool (UniqueObject, SimpleItem, ActionProviderBase):
  13.120 +
  13.121 +    __implements__ = (IOldstyleDiscussionTool,
  13.122 +                      ActionProviderBase.__implements__)
  13.123 +
  13.124 +    id = 'portal_discussion'
  13.125 +    meta_type = 'Oldstyle CMF Discussion Tool'
  13.126 +    # This tool is used to find the discussion for a given content object.
  13.127 +
  13.128 +    security = ClassSecurityInfo()
  13.129 +
  13.130 +    manage_options = ( { 'label' : 'Overview', 'action' : 'manage_overview' }
  13.131 +                     , 
  13.132 +                     ) + SimpleItem.manage_options
  13.133 +
  13.134 +    #
  13.135 +    #   ZMI methods
  13.136 +    #
  13.137 +    security.declareProtected(ManagePortal, 'manage_overview')
  13.138 +    manage_overview = DTMLFile( 'explainDiscussionTool', _dtmldir )
  13.139 +
  13.140 +    #
  13.141 +    #   'portal_discussion' interface methods
  13.142 +    #
  13.143 +    security.declarePublic('getDiscussionFor')
  13.144 +    def getDiscussionFor(self, content):
  13.145 +        '''Gets the PortalDiscussion object that applies to content.
  13.146 +        '''
  13.147 +        return OldDiscussable( content ).__of__( content )
  13.148 +
  13.149 +    security.declarePublic('isDiscussionAllowedFor')
  13.150 +    def isDiscussionAllowedFor(self, content):
  13.151 +        '''
  13.152 +            Returns a boolean indicating whether a discussion is
  13.153 +            allowed for the specified content.
  13.154 +        '''
  13.155 +        if hasattr( content, 'allow_discussion' ):
  13.156 +            return content.allow_discussion
  13.157 +        typeInfo = getToolByName(self, 'portal_types').getTypeInfo( content )
  13.158 +        if typeInfo:
  13.159 +            return typeInfo.allowDiscussion()
  13.160 +        return 0
  13.161 +
  13.162 +    security.declarePrivate('listActions')
  13.163 +    def listActions(self, info=None, object=None):
  13.164 +        # Return actions for reply and show replies
  13.165 +        if object is not None or info is None:
  13.166 +            info = self._getOAI(object)
  13.167 +        content = info.object
  13.168 +        if content is None or not self.isDiscussionAllowedFor(content):
  13.169 +            return ()
  13.170 +
  13.171 +        discussion = self.getDiscussionFor(content)
  13.172 +        if discussion.aq_base == content.aq_base:
  13.173 +            discussion_url = info.object_url
  13.174 +        else:
  13.175 +            discussion_url = discussion.absolute_url()
  13.176 +
  13.177 +        actions = (
  13.178 +            {'name': 'Reply',
  13.179 +             'url': discussion_url + '/discussion_reply_form',
  13.180 +             'permissions': [ReplyToItem],
  13.181 +             'category': 'object'
  13.182 +             },
  13.183 +            )
  13.184 +
  13.185 +        return actions
  13.186 +
  13.187 +InitializeClass(DiscussionTool)
    14.1 new file mode 100644
    14.2 --- /dev/null
    14.3 +++ b/DynamicType.py
    14.4 @@ -0,0 +1,149 @@
    14.5 +##############################################################################
    14.6 +#
    14.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    14.8 +#
    14.9 +# This software is subject to the provisions of the Zope Public License,
   14.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   14.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   14.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   14.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   14.14 +# FOR A PARTICULAR PURPOSE.
   14.15 +#
   14.16 +##############################################################################
   14.17 +""" DynamicType: Mixin for dynamic properties.
   14.18 +
   14.19 +$Id$
   14.20 +"""
   14.21 +
   14.22 +from urllib import quote
   14.23 +
   14.24 +from AccessControl import ClassSecurityInfo
   14.25 +from Globals import InitializeClass
   14.26 +
   14.27 +from interfaces.Dynamic import DynamicType as IDynamicType
   14.28 +from utils import getToolByName
   14.29 +
   14.30 +try:
   14.31 +    from zope.app.publisher.browser import queryDefaultViewName
   14.32 +    from zope.component import queryMultiAdapter
   14.33 +    def queryView(obj, name, request):
   14.34 +        return queryMultiAdapter((obj, request), name=name)
   14.35 +except ImportError:
   14.36 +    # BBB for Zope 2.8
   14.37 +    from zope.component import queryDefaultViewName, queryView
   14.38 +
   14.39 +class DynamicType:
   14.40 +    """
   14.41 +    Mixin for portal content that allows the object to take on
   14.42 +    a dynamic type property.
   14.43 +    """
   14.44 +
   14.45 +    __implements__ = IDynamicType
   14.46 +
   14.47 +    portal_type = None
   14.48 +
   14.49 +    security = ClassSecurityInfo()
   14.50 +
   14.51 +    def _setPortalTypeName(self, pt):
   14.52 +        """ Set the portal type name.
   14.53 +
   14.54 +        Called by portal_types during construction, records an ID that will be
   14.55 +        used later to locate the correct ContentTypeInformation.
   14.56 +        """
   14.57 +        self.portal_type = pt
   14.58 +
   14.59 +    security.declarePublic('getPortalTypeName')
   14.60 +    def getPortalTypeName(self):
   14.61 +        """ Get the portal type name that can be passed to portal_types.
   14.62 +        """
   14.63 +        pt = self.portal_type
   14.64 +        if callable( pt ):
   14.65 +            pt = pt()
   14.66 +        return pt
   14.67 +
   14.68 +    # deprecated alias
   14.69 +    _getPortalTypeName = getPortalTypeName
   14.70 +
   14.71 +    security.declarePublic('getTypeInfo')
   14.72 +    def getTypeInfo(self):
   14.73 +        """ Get the TypeInformation object specified by the portal type.
   14.74 +        """
   14.75 +        tool = getToolByName(self, 'portal_types', None)
   14.76 +        if tool is None:
   14.77 +            return None
   14.78 +        return tool.getTypeInfo(self)  # Can return None.
   14.79 +
   14.80 +    security.declarePublic('getActionInfo')
   14.81 +    def getActionInfo(self, action_chain, check_visibility=0,
   14.82 +                      check_condition=0):
   14.83 +        """ Get an Action info mapping specified by a chain of actions.
   14.84 +        """
   14.85 +        ti = self.getTypeInfo()
   14.86 +        if ti:
   14.87 +            return ti.getActionInfo(action_chain, self, check_visibility,
   14.88 +                                    check_condition)
   14.89 +        else:
   14.90 +            msg = 'Action "%s" not available for %s' % (
   14.91 +                        action_chain, '/'.join(self.getPhysicalPath()))
   14.92 +            raise ValueError(msg) 
   14.93 +
   14.94 +    # Support for dynamic icons
   14.95 +
   14.96 +    security.declarePublic('getIcon')
   14.97 +    def getIcon(self, relative_to_portal=0):
   14.98 +        """
   14.99 +        Using this method allows the content class
  14.100 +        creator to grab icons on the fly instead of using a fixed
  14.101 +        attribute on the class.
  14.102 +        """
  14.103 +        ti = self.getTypeInfo()
  14.104 +        if ti is not None:
  14.105 +            icon = quote(ti.getIcon())
  14.106 +            if icon:
  14.107 +                if relative_to_portal:
  14.108 +                    return icon
  14.109 +                else:
  14.110 +                    # Relative to REQUEST['BASEPATH1']
  14.111 +                    portal_url = getToolByName( self, 'portal_url' )
  14.112 +                    res = portal_url(relative=1) + '/' + icon
  14.113 +                    while res[:1] == '/':
  14.114 +                        res = res[1:]
  14.115 +                    return res
  14.116 +        return 'misc_/OFSP/dtmldoc.gif'
  14.117 +
  14.118 +    security.declarePublic('icon')
  14.119 +    icon = getIcon  # For the ZMI
  14.120 +
  14.121 +    def __before_publishing_traverse__(self, arg1, arg2=None):
  14.122 +        """ Pre-traversal hook.
  14.123 +        """
  14.124 +        # XXX hack around a bug(?) in BeforeTraverse.MultiHook
  14.125 +        REQUEST = arg2 or arg1
  14.126 +
  14.127 +        if REQUEST['REQUEST_METHOD'] not in ('GET', 'POST'):
  14.128 +            return
  14.129 +
  14.130 +        stack = REQUEST['TraversalRequestNameStack']
  14.131 +        key = stack and stack[-1] or '(Default)'
  14.132 +
  14.133 +        # if there's a Zope3-style default view name set and the
  14.134 +        # corresponding view exists, take that in favour of the FTI's
  14.135 +        # default view
  14.136 +        if key == '(Default)':
  14.137 +            viewname = queryDefaultViewName(self, REQUEST)
  14.138 +            if (viewname and
  14.139 +                queryView(self, viewname, REQUEST) is not None):
  14.140 +                stack.append(viewname)
  14.141 +                REQUEST._hacked_path = 1
  14.142 +                return
  14.143 +
  14.144 +        ti = self.getTypeInfo()
  14.145 +        method_id = ti and ti.queryMethodID(key, context=self)
  14.146 +        if method_id:
  14.147 +            if key != '(Default)':
  14.148 +                stack.pop()
  14.149 +            if method_id != '(Default)':
  14.150 +                stack.append(method_id)
  14.151 +            REQUEST._hacked_path = 1
  14.152 +
  14.153 +InitializeClass(DynamicType)
    15.1 new file mode 100644
    15.2 --- /dev/null
    15.3 +++ b/Expression.py
    15.4 @@ -0,0 +1,109 @@
    15.5 +##############################################################################
    15.6 +#
    15.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    15.8 +#
    15.9 +# This software is subject to the provisions of the Zope Public License,
   15.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   15.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   15.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   15.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   15.14 +# FOR A PARTICULAR PURPOSE.
   15.15 +#
   15.16 +##############################################################################
   15.17 +""" Expressions in a web-configurable workflow.
   15.18 +
   15.19 +$Id$
   15.20 +"""
   15.21 +
   15.22 +from AccessControl import ClassSecurityInfo
   15.23 +from Acquisition import aq_base, aq_inner, aq_parent
   15.24 +from Globals import InitializeClass
   15.25 +from Globals import Persistent
   15.26 +from Products.PageTemplates.Expressions import getEngine
   15.27 +from Products.PageTemplates.Expressions import SecureModuleImporter
   15.28 +
   15.29 +from utils import getToolByName
   15.30 +
   15.31 +
   15.32 +class Expression (Persistent):
   15.33 +    text = ''
   15.34 +    _v_compiled = None
   15.35 +
   15.36 +    security = ClassSecurityInfo()
   15.37 +
   15.38 +    def __init__(self, text):
   15.39 +        self.text = text
   15.40 +        self._v_compiled = getEngine().compile(text)
   15.41 +
   15.42 +    def __call__(self, econtext):
   15.43 +        compiled = self._v_compiled
   15.44 +        if compiled is None:
   15.45 +            compiled = self._v_compiled = getEngine().compile(self.text)
   15.46 +        # ?? Maybe expressions should manipulate the security
   15.47 +        # context stack.
   15.48 +        res = compiled(econtext)
   15.49 +        if isinstance(res, Exception):
   15.50 +            raise res
   15.51 +        #print 'returning %s from %s' % (`res`, self.text)
   15.52 +        return res
   15.53 +
   15.54 +InitializeClass(Expression)
   15.55 +
   15.56 +
   15.57 +def getExprContext(context, object=None):
   15.58 +    request = getattr(context, 'REQUEST', None)
   15.59 +    if request:
   15.60 +        cache = request.get('_ec_cache', None)
   15.61 +        if cache is None:
   15.62 +            request['_ec_cache'] = cache = {}
   15.63 +        ec = cache.get( id(object), None )
   15.64 +    else:
   15.65 +        ec = None
   15.66 +    if ec is None:
   15.67 +        utool = getToolByName(context, 'portal_url')
   15.68 +        portal = utool.getPortalObject()
   15.69 +        if object is None or not hasattr(object, 'aq_base'):
   15.70 +            folder = portal
   15.71 +        else:
   15.72 +            folder = object
   15.73 +            # Search up the containment hierarchy until we find an
   15.74 +            # object that claims it's a folder.
   15.75 +            while folder is not None:
   15.76 +                if getattr(aq_base(folder), 'isPrincipiaFolderish', 0):
   15.77 +                    # found it.
   15.78 +                    break
   15.79 +                else:
   15.80 +                    folder = aq_parent(aq_inner(folder))
   15.81 +        ec = createExprContext(folder, portal, object)
   15.82 +        if request:
   15.83 +            cache[ id(object) ] = ec
   15.84 +    return ec
   15.85 +
   15.86 +
   15.87 +def createExprContext(folder, portal, object):
   15.88 +    '''
   15.89 +    An expression context provides names for TALES expressions.
   15.90 +    '''
   15.91 +    pm = getToolByName(portal, 'portal_membership')
   15.92 +    if object is None:
   15.93 +        object_url = ''
   15.94 +    else:
   15.95 +        object_url = object.absolute_url()
   15.96 +    if pm.isAnonymousUser():
   15.97 +        member = None
   15.98 +    else:
   15.99 +        member = pm.getAuthenticatedMember()
  15.100 +    data = {
  15.101 +        'object_url':   object_url,
  15.102 +        'folder_url':   folder.absolute_url(),
  15.103 +        'portal_url':   portal.absolute_url(),
  15.104 +        'object':       object,
  15.105 +        'folder':       folder,
  15.106 +        'portal':       portal,
  15.107 +        'nothing':      None,
  15.108 +        'request':      getattr(portal, 'REQUEST', None),
  15.109 +        'modules':      SecureModuleImporter,
  15.110 +        'member':       member,
  15.111 +        'here':         object,
  15.112 +        }
  15.113 +    return getEngine().getContext(data)
    16.1 new file mode 100644
    16.2 --- /dev/null
    16.3 +++ b/Extensions/TestRecord.py
    16.4 @@ -0,0 +1,10 @@
    16.5 +#
    16.6 +#   TestRecord      - Used for FSZSQLMethod tests
    16.7 +#
    16.8 +
    16.9 +class MyRecord:
   16.10 +
   16.11 +    def my_uncle(self):
   16.12 +        """ Bruce is my uncle """
   16.13 +        return 'bruce'
   16.14 +
    17.1 new file mode 100644
    17.2 --- /dev/null
    17.3 +++ b/FSDTMLMethod.py
    17.4 @@ -0,0 +1,204 @@
    17.5 +##############################################################################
    17.6 +#
    17.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    17.8 +#
    17.9 +# This software is subject to the provisions of the Zope Public License,
   17.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   17.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   17.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   17.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   17.14 +# FOR A PARTICULAR PURPOSE.
   17.15 +#
   17.16 +##############################################################################
   17.17 +""" Customizable DTML methods that come from the filesystem.
   17.18 +
   17.19 +$Id$
   17.20 +"""
   17.21 +
   17.22 +import Globals
   17.23 +from AccessControl import ClassSecurityInfo, getSecurityManager
   17.24 +from AccessControl.DTML import RestrictedDTML
   17.25 +from AccessControl.Role import RoleManager
   17.26 +from OFS.Cache import Cacheable
   17.27 +from OFS.DTMLMethod import DTMLMethod, decapitate, guess_content_type
   17.28 +
   17.29 +from DirectoryView import registerFileExtension
   17.30 +from DirectoryView import registerMetaType
   17.31 +from FSObject import FSObject
   17.32 +from permissions import FTPAccess
   17.33 +from permissions import View
   17.34 +from permissions import ViewManagementScreens
   17.35 +from utils import _dtmldir
   17.36 +from utils import _setCacheHeaders, _checkConditionalGET
   17.37 +from utils import expandpath
   17.38 +
   17.39 +
   17.40 +_marker = []  # Create a new marker object.
   17.41 +
   17.42 +
   17.43 +class FSDTMLMethod(RestrictedDTML, RoleManager, FSObject, Globals.HTML):
   17.44 +    """FSDTMLMethods act like DTML methods but are not directly
   17.45 +    modifiable from the management interface."""
   17.46 +
   17.47 +    meta_type = 'Filesystem DTML Method'
   17.48 +    _owner = None
   17.49 +
   17.50 +    manage_options=(
   17.51 +        (
   17.52 +            {'label':'Customize', 'action':'manage_main'},
   17.53 +            {'label':'View', 'action':'',
   17.54 +             'help':('OFSP','DTML-DocumentOrMethod_View.stx')},
   17.55 +            {'label':'Proxy', 'action':'manage_proxyForm',
   17.56 +             'help':('OFSP','DTML-DocumentOrMethod_Proxy.stx')},
   17.57 +            )
   17.58 +            +Cacheable.manage_options
   17.59 +        )
   17.60 +
   17.61 +    _proxy_roles=()
   17.62 +    _cache_namespace_keys=()
   17.63 +
   17.64 +    # Use declarative security
   17.65 +    security = ClassSecurityInfo()
   17.66 +    security.declareObjectProtected(View)
   17.67 +
   17.68 +    security.declareProtected(ViewManagementScreens, 'manage_main')
   17.69 +    manage_main = Globals.DTMLFile('custdtml', _dtmldir)
   17.70 +
   17.71 +    _reading = 0
   17.72 +
   17.73 +    def __init__(self, id, filepath, fullname=None, properties=None):
   17.74 +        FSObject.__init__(self, id, filepath, fullname, properties)
   17.75 +        # Normally called via HTML.__init__ but we don't need the rest that
   17.76 +        # happens there.
   17.77 +        self.initvars(None, {})
   17.78 +
   17.79 +    def _createZODBClone(self):
   17.80 +        """Create a ZODB (editable) equivalent of this object."""
   17.81 +        return DTMLMethod(self.read(), __name__=self.getId())
   17.82 +
   17.83 +    def _readFile(self, reparse):
   17.84 +        fp = expandpath(self._filepath)
   17.85 +        file = open(fp, 'r')    # not 'rb', as this is a text file!
   17.86 +        try:
   17.87 +            data = file.read()
   17.88 +        finally:
   17.89 +            file.close()
   17.90 +        self.raw = data
   17.91 +        if reparse:
   17.92 +            self._reading = 1  # Avoid infinite recursion
   17.93 +            try:
   17.94 +                self.cook()
   17.95 +            finally:
   17.96 +                self._reading = 0
   17.97 +
   17.98 +    # Hook up chances to reload in debug mode
   17.99 +    security.declarePrivate('read_raw')
  17.100 +    def read_raw(self):
  17.101 +        if not self._reading:
  17.102 +            self._updateFromFS()
  17.103 +        return Globals.HTML.read_raw(self)
  17.104 +
  17.105 +    #### The following is mainly taken from OFS/DTMLMethod.py ###
  17.106 +
  17.107 +    index_html=None # Prevent accidental acquisition
  17.108 +
  17.109 +    # Documents masquerade as functions:
  17.110 +    func_code = DTMLMethod.func_code
  17.111 +
  17.112 +    default_content_type = 'text/html'
  17.113 +
  17.114 +    def __call__(self, client=None, REQUEST={}, RESPONSE=None, **kw):
  17.115 +        """Render the document given a client object, REQUEST mapping,
  17.116 +        Response, and key word arguments."""
  17.117 +
  17.118 +        self._updateFromFS()
  17.119 +
  17.120 +        kw['document_id']   =self.getId()
  17.121 +        kw['document_title']=self.title
  17.122 +
  17.123 +        if client is not None:
  17.124 +            if _checkConditionalGET(self, kw):
  17.125 +                return ''
  17.126 +
  17.127 +        if not self._cache_namespace_keys:
  17.128 +            data = self.ZCacheable_get(default=_marker)
  17.129 +            if data is not _marker:
  17.130 +                # Return cached results.
  17.131 +                return data
  17.132 +
  17.133 +        __traceback_info__ = self._filepath
  17.134 +        security=getSecurityManager()
  17.135 +        security.addContext(self)
  17.136 +        try:
  17.137 +            r = Globals.HTML.__call__(self, client, REQUEST, **kw)
  17.138 +
  17.139 +            if client is None:
  17.140 +                # Called as subtemplate, so don't need error propagation!
  17.141 +                if RESPONSE is None: result = r
  17.142 +                else: result = decapitate(r, RESPONSE)
  17.143 +                if not self._cache_namespace_keys:
  17.144 +                    self.ZCacheable_set(result)
  17.145 +                return result
  17.146 +
  17.147 +            if not isinstance(r, basestring) or RESPONSE is None:
  17.148 +                if not self._cache_namespace_keys:
  17.149 +                    self.ZCacheable_set(r)
  17.150 +                return r
  17.151 +
  17.152 +        finally: security.removeContext(self)
  17.153 +
  17.154 +        have_key=RESPONSE.headers.has_key
  17.155 +        if not (have_key('content-type') or have_key('Content-Type')):
  17.156 +            if self.__dict__.has_key('content_type'):
  17.157 +                c=self.content_type
  17.158 +            else:
  17.159 +                c, e=guess_content_type(self.getId(), r)
  17.160 +            RESPONSE.setHeader('Content-Type', c)
  17.161 +        if RESPONSE is not None:
  17.162 +            # caching policy manager hook
  17.163 +            _setCacheHeaders(self, {})
  17.164 +        result = decapitate(r, RESPONSE)
  17.165 +        if not self._cache_namespace_keys:
  17.166 +            self.ZCacheable_set(result)
  17.167 +        return result
  17.168 +
  17.169 +    def getCacheNamespaceKeys(self):
  17.170 +        '''
  17.171 +        Returns the cacheNamespaceKeys.
  17.172 +        '''
  17.173 +        return self._cache_namespace_keys
  17.174 +
  17.175 +    def setCacheNamespaceKeys(self, keys, REQUEST=None):
  17.176 +        '''
  17.177 +        Sets the list of names that should be looked up in the
  17.178 +        namespace to provide a cache key.
  17.179 +        '''
  17.180 +        ks = []
  17.181 +        for key in keys:
  17.182 +            key = strip(str(key))
  17.183 +            if key:
  17.184 +                ks.append(key)
  17.185 +        self._cache_namespace_keys = tuple(ks)
  17.186 +        if REQUEST is not None:
  17.187 +            return self.ZCacheable_manage(self, REQUEST)
  17.188 +
  17.189 +    # Zope 2.3.x way:
  17.190 +    def validate(self, inst, parent, name, value, md=None):
  17.191 +        return getSecurityManager().validate(inst, parent, name, value)
  17.192 +
  17.193 +    security.declareProtected(FTPAccess, 'manage_FTPget')
  17.194 +    manage_FTPget = DTMLMethod.manage_FTPget.im_func
  17.195 +
  17.196 +    security.declareProtected(ViewManagementScreens, 'PrincipiaSearchSource')
  17.197 +    PrincipiaSearchSource = DTMLMethod.PrincipiaSearchSource.im_func
  17.198 +
  17.199 +    security.declareProtected(ViewManagementScreens, 'document_src')
  17.200 +    document_src = DTMLMethod.document_src.im_func
  17.201 +
  17.202 +    security.declareProtected(ViewManagementScreens, 'manage_haveProxy')
  17.203 +    manage_haveProxy = DTMLMethod.manage_haveProxy.im_func
  17.204 +
  17.205 +Globals.InitializeClass(FSDTMLMethod)
  17.206 +
  17.207 +registerFileExtension('dtml', FSDTMLMethod)
  17.208 +registerMetaType('DTML Method', FSDTMLMethod)
    18.1 new file mode 100644
    18.2 --- /dev/null
    18.3 +++ b/FSFile.py
    18.4 @@ -0,0 +1,180 @@
    18.5 +##############################################################################
    18.6 +#
    18.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    18.8 +#
    18.9 +# This software is subject to the provisions of the Zope Public License,
   18.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   18.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   18.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   18.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   18.14 +# FOR A PARTICULAR PURPOSE.
   18.15 +#
   18.16 +##############################################################################
   18.17 +""" Customizable image objects that come from the filesystem.
   18.18 +
   18.19 +$Id$
   18.20 +"""
   18.21 +
   18.22 +import codecs
   18.23 +import Globals
   18.24 +from AccessControl import ClassSecurityInfo
   18.25 +from DateTime import DateTime
   18.26 +from OFS.Cache import Cacheable
   18.27 +from OFS.Image import File
   18.28 +try:
   18.29 +    from zope.contenttype import guess_content_type
   18.30 +except ImportError:
   18.31 +    # BBB: for Zope < 2.10
   18.32 +    try:
   18.33 +        from zope.app.content_types import guess_content_type
   18.34 +    except ImportError:
   18.35 +        # BBB: for Zope < 2.9
   18.36 +        from OFS.content_types import guess_content_type
   18.37 +
   18.38 +from DirectoryView import registerFileExtension
   18.39 +from DirectoryView import registerMetaType
   18.40 +from FSObject import FSObject
   18.41 +from permissions import FTPAccess
   18.42 +from permissions import View
   18.43 +from permissions import ViewManagementScreens
   18.44 +from utils import _dtmldir
   18.45 +from utils import _setCacheHeaders, _ViewEmulator
   18.46 +from utils import expandpath, _FSCacheHeaders, _checkConditionalGET
   18.47 +
   18.48 +
   18.49 +class FSFile(FSObject):
   18.50 +    """FSFiles act like images but are not directly
   18.51 +    modifiable from the management interface."""
   18.52 +    # Note that OFS.Image.File is not a base class because it is mutable.
   18.53 +
   18.54 +    meta_type = 'Filesystem File'
   18.55 +
   18.56 +    manage_options=(
   18.57 +        {'label':'Customize', 'action':'manage_main'},
   18.58 +        ) + Cacheable.manage_options
   18.59 +
   18.60 +    security = ClassSecurityInfo()
   18.61 +    security.declareObjectProtected(View)
   18.62 +
   18.63 +    def __init__(self, id, filepath, fullname=None, properties=None):
   18.64 +        id = fullname or id # Use the whole filename.
   18.65 +        FSObject.__init__(self, id, filepath, fullname, properties)
   18.66 +
   18.67 +    security.declareProtected(ViewManagementScreens, 'manage_main')
   18.68 +    manage_main = Globals.DTMLFile('custfile', _dtmldir)
   18.69 +    content_type = 'unknown/unknown'
   18.70 +
   18.71 +    def _createZODBClone(self):
   18.72 +        return File(self.getId(), '', self._readFile(1))
   18.73 +
   18.74 +    def _get_content_type(self, file, body, id, content_type=None):
   18.75 +        # Consult self.content_type first, this is either
   18.76 +        # the default (unknown/unknown) or it got a value from a
   18.77 +        # .metadata file
   18.78 +        default_type = 'unknown/unknown'
   18.79 +        if getattr(self, 'content_type', default_type) != default_type:
   18.80 +            return self.content_type
   18.81 +
   18.82 +        # Next, look at file headers
   18.83 +        headers=getattr(file, 'headers', None)
   18.84 +        if headers and headers.has_key('content-type'):
   18.85 +            content_type=headers['content-type']
   18.86 +        else:
   18.87 +            # Last resort: Use the (imperfect) content type guessing
   18.88 +            # mechanism from OFS.Image, which ultimately uses the
   18.89 +            # Python mimetypes module.
   18.90 +            if not isinstance(body, basestring):
   18.91 +                body = body.data
   18.92 +            content_type, enc=guess_content_type(
   18.93 +                getattr(file, 'filename',id), body, content_type)
   18.94 +            if (enc is None
   18.95 +                and (content_type.startswith('text/') or
   18.96 +                     content_type.startswith('application/'))
   18.97 +                and body.startswith(codecs.BOM_UTF8)):
   18.98 +                content_type += '; charset=utf-8'
   18.99 +
  18.100 +        return content_type
  18.101 +
  18.102 +    def _readFile(self, reparse):
  18.103 +        fp = expandpath(self._filepath)
  18.104 +        file = open(fp, 'rb')
  18.105 +        try: data = file.read()
  18.106 +        finally: file.close()
  18.107 +        if reparse or self.content_type == 'unknown/unknown':
  18.108 +            self.ZCacheable_invalidate()
  18.109 +            self.content_type=self._get_content_type(file, data, self.id)
  18.110 +        return data
  18.111 +
  18.112 +    #### The following is mainly taken from OFS/File.py ###
  18.113 +
  18.114 +    def __str__(self):
  18.115 +        self._updateFromFS()
  18.116 +        return str( self._readFile( 0 ) )
  18.117 +
  18.118 +    def modified(self):
  18.119 +        return self.getModTime()
  18.120 +
  18.121 +    security.declareProtected(View, 'index_html')
  18.122 +    def index_html(self, REQUEST, RESPONSE):
  18.123 +        """
  18.124 +        The default view of the contents of a File or Image.
  18.125 +
  18.126 +        Returns the contents of the file or image.  Also, sets the
  18.127 +        Content-Type HTTP header to the objects content type.
  18.128 +        """
  18.129 +        self._updateFromFS()
  18.130 +        view = _ViewEmulator().__of__(self)
  18.131 +
  18.132 +        # If we have a conditional get, set status 304 and return
  18.133 +        # no content
  18.134 +        if _checkConditionalGET(view, extra_context={}):
  18.135 +            return ''
  18.136 +
  18.137 +        RESPONSE.setHeader('Content-Type', self.content_type)
  18.138 +
  18.139 +        # old-style If-Modified-Since header handling.
  18.140 +        if self._setOldCacheHeaders():
  18.141 +            # Make sure the CachingPolicyManager gets a go as well
  18.142 +            _setCacheHeaders(view, extra_context={})
  18.143 +            return ''
  18.144 +
  18.145 +        data = self._readFile(0)
  18.146 +        data_len = len(data)
  18.147 +        RESPONSE.setHeader('Content-Length', data_len)
  18.148 +
  18.149 +        #There are 2 Cache Managers which can be in play....
  18.150 +        #need to decide which to use to determine where the cache headers
  18.151 +        #are decided on.
  18.152 +        if self.ZCacheable_getManager() is not None:
  18.153 +            self.ZCacheable_set(None)
  18.154 +        else:
  18.155 +            _setCacheHeaders(view, extra_context={})
  18.156 +        return data
  18.157 +
  18.158 +    def _setOldCacheHeaders(self):
  18.159 +        # return False to disable this simple caching behaviour
  18.160 +        return _FSCacheHeaders(self)
  18.161 +
  18.162 +    security.declareProtected(View, 'getContentType')
  18.163 +    def getContentType(self):
  18.164 +        """Get the content type of a file or image.
  18.165 +
  18.166 +        Returns the content type (MIME type) of a file or image.
  18.167 +        """
  18.168 +        self._updateFromFS()
  18.169 +        return self.content_type
  18.170 +
  18.171 +    security.declareProtected(FTPAccess, 'manage_FTPget')
  18.172 +    manage_FTPget = index_html
  18.173 +
  18.174 +Globals.InitializeClass(FSFile)
  18.175 +
  18.176 +registerFileExtension('doc', FSFile)
  18.177 +registerFileExtension('pdf', FSFile)
  18.178 +registerFileExtension('swf', FSFile)
  18.179 +registerFileExtension('jar', FSFile)
  18.180 +registerFileExtension('cab', FSFile)
  18.181 +registerFileExtension('ico', FSFile)
  18.182 +registerFileExtension('js', FSFile)
  18.183 +registerFileExtension('css', FSFile)
  18.184 +registerMetaType('File', FSFile)
    19.1 new file mode 100644
    19.2 --- /dev/null
    19.3 +++ b/FSImage.py
    19.4 @@ -0,0 +1,160 @@
    19.5 +##############################################################################
    19.6 +#
    19.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    19.8 +#
    19.9 +# This software is subject to the provisions of the Zope Public License,
   19.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   19.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   19.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   19.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   19.14 +# FOR A PARTICULAR PURPOSE.
   19.15 +#
   19.16 +##############################################################################
   19.17 +""" Customizable image objects that come from the filesystem.
   19.18 +
   19.19 +$Id$
   19.20 +"""
   19.21 +
   19.22 +import Globals
   19.23 +from DateTime import DateTime
   19.24 +from AccessControl import ClassSecurityInfo
   19.25 +from OFS.Cache import Cacheable
   19.26 +from OFS.Image import Image, getImageInfo
   19.27 +
   19.28 +from permissions import FTPAccess
   19.29 +from permissions import View
   19.30 +from permissions import ViewManagementScreens
   19.31 +from DirectoryView import registerFileExtension
   19.32 +from DirectoryView import registerMetaType
   19.33 +from FSObject import FSObject
   19.34 +from utils import _dtmldir
   19.35 +from utils import _setCacheHeaders, _ViewEmulator
   19.36 +from utils import expandpath, _FSCacheHeaders, _checkConditionalGET
   19.37 +
   19.38 +
   19.39 +class FSImage(FSObject):
   19.40 +    """FSImages act like images but are not directly
   19.41 +    modifiable from the management interface."""
   19.42 +    # Note that OFS.Image.Image is not a base class because it is mutable.
   19.43 +
   19.44 +    meta_type = 'Filesystem Image'
   19.45 +
   19.46 +    _data = None
   19.47 +
   19.48 +    manage_options=(
   19.49 +        {'label':'Customize', 'action':'manage_main'},
   19.50 +        ) + Cacheable.manage_options
   19.51 +
   19.52 +    security = ClassSecurityInfo()
   19.53 +    security.declareObjectProtected(View)
   19.54 +
   19.55 +    def __init__(self, id, filepath, fullname=None, properties=None):
   19.56 +        id = fullname or id # Use the whole filename.
   19.57 +        FSObject.__init__(self, id, filepath, fullname, properties)
   19.58 +
   19.59 +    security.declareProtected(ViewManagementScreens, 'manage_main')
   19.60 +    manage_main = Globals.DTMLFile('custimage', _dtmldir)
   19.61 +    content_type = 'unknown/unknown'
   19.62 +
   19.63 +    def _createZODBClone(self):
   19.64 +        return Image(self.getId(), '', self._readFile(1))
   19.65 +
   19.66 +    def _readFile(self, reparse):
   19.67 +        fp = expandpath(self._filepath)
   19.68 +        file = open(fp, 'rb')
   19.69 +        try:
   19.70 +            data = self._data = file.read()
   19.71 +        finally:
   19.72 +            file.close()
   19.73 +        if reparse or self.content_type == 'unknown/unknown':
   19.74 +            self.ZCacheable_invalidate()
   19.75 +            ct, width, height = getImageInfo( data )
   19.76 +            self.content_type = ct
   19.77 +            self.width = width
   19.78 +            self.height = height
   19.79 +        return data
   19.80 +
   19.81 +    #### The following is mainly taken from OFS/Image.py ###
   19.82 +
   19.83 +    __str__ = Image.__str__.im_func
   19.84 +
   19.85 +    _image_tag = Image.tag.im_func
   19.86 +    security.declareProtected(View, 'tag')
   19.87 +    def tag(self, *args, **kw):
   19.88 +        # Hook into an opportunity to reload metadata.
   19.89 +        self._updateFromFS()
   19.90 +        return self._image_tag(*args, **kw)
   19.91 +
   19.92 +    security.declareProtected(View, 'index_html')
   19.93 +    def index_html(self, REQUEST, RESPONSE):
   19.94 +        """
   19.95 +        The default view of the contents of a File or Image.
   19.96 +
   19.97 +        Returns the contents of the file or image.  Also, sets the
   19.98 +        Content-Type HTTP header to the objects content type.
   19.99 +        """
  19.100 +
  19.101 +        self._updateFromFS()
  19.102 +        view = _ViewEmulator().__of__(self)
  19.103 +
  19.104 +        # If we have a conditional get, set status 304 and return
  19.105 +        # no content
  19.106 +        if _checkConditionalGET(view, extra_context={}):
  19.107 +            return ''
  19.108 +
  19.109 +        RESPONSE.setHeader('Content-Type', self.content_type)
  19.110 +
  19.111 +        # old-style If-Modified-Since header handling.
  19.112 +        if self._setOldCacheHeaders():
  19.113 +            # Make sure the CachingPolicyManager gets a go as well
  19.114 +            _setCacheHeaders(view, extra_context={})
  19.115 +            return ''
  19.116 +
  19.117 +        data = self._readFile(0)
  19.118 +        data_len = len(data)
  19.119 +        RESPONSE.setHeader('Content-Length', data_len)
  19.120 +
  19.121 +        #There are 2 Cache Managers which can be in play....
  19.122 +        #need to decide which to use to determine where the cache headers
  19.123 +        #are decided on.
  19.124 +        if self.ZCacheable_getManager() is not None:
  19.125 +            self.ZCacheable_set(None)
  19.126 +        else:
  19.127 +            _setCacheHeaders(view, extra_context={})
  19.128 +        return data
  19.129 +
  19.130 +    def _setOldCacheHeaders(self):
  19.131 +        # return False to disable this simple caching behaviour
  19.132 +        return _FSCacheHeaders(self)
  19.133 +
  19.134 +    def modified(self):
  19.135 +        return self.getModTime()
  19.136 +
  19.137 +    security.declareProtected(View, 'getContentType')
  19.138 +    def getContentType(self):
  19.139 +        """Get the content type of a file or image.
  19.140 +
  19.141 +        Returns the content type (MIME type) of a file or image.
  19.142 +        """
  19.143 +        self._updateFromFS()
  19.144 +        return self.content_type
  19.145 +
  19.146 +    security.declareProtected(View, 'get_size')
  19.147 +    def get_size( self ):
  19.148 +        """
  19.149 +            Return the size of the image.
  19.150 +        """
  19.151 +        self._updateFromFS()
  19.152 +        return self._data and len( self._data ) or 0
  19.153 +
  19.154 +    security.declareProtected(FTPAccess, 'manage_FTPget')
  19.155 +    manage_FTPget = index_html
  19.156 +
  19.157 +Globals.InitializeClass(FSImage)
  19.158 +
  19.159 +registerFileExtension('gif', FSImage)
  19.160 +registerFileExtension('jpg', FSImage)
  19.161 +registerFileExtension('jpeg', FSImage)
  19.162 +registerFileExtension('png', FSImage)
  19.163 +registerFileExtension('bmp', FSImage)
  19.164 +registerMetaType('Image', FSImage)
    20.1 new file mode 100644
    20.2 --- /dev/null
    20.3 +++ b/FSMetadata.py
    20.4 @@ -0,0 +1,210 @@
    20.5 +##############################################################################
    20.6 +#
    20.7 +# Copyright (c) 2003 Zope Corporation and Contributors. All Rights Reserved.
    20.8 +#
    20.9 +# This software is subject to the provisions of the Zope Public License,
   20.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   20.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   20.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   20.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   20.14 +# FOR A PARTICULAR PURPOSE.
   20.15 +#
   20.16 +##############################################################################
   20.17 +""" Handles reading the properties for an object that comes from the filesystem.
   20.18 +
   20.19 +$Id$
   20.20 +"""
   20.21 +
   20.22 +import logging
   20.23 +from os.path import exists
   20.24 +from ConfigParser import ConfigParser
   20.25 +from warnings import warn
   20.26 +
   20.27 +import re
   20.28 +
   20.29 +
   20.30 +logger = logging.getLogger('CMFCore.FSMetadata')
   20.31 +
   20.32 +
   20.33 +class CMFConfigParser(ConfigParser):
   20.34 +    """ This our wrapper around ConfigParser to
   20.35 +    solve a few minor niggles with the code """
   20.36 +    # adding in a space so that names can contain spaces
   20.37 +    OPTCRE = re.compile(
   20.38 +        r'(?P<option>[]\-[ \w_.*,(){}]+)'      # a lot of stuff found by IvL
   20.39 +        r'[ \t]*(?P<vi>[:=])[ \t]*'           # any number of space/tab,
   20.40 +                                              # followed by separator
   20.41 +                                              # (either : or =), followed
   20.42 +                                              # by any # space/tab
   20.43 +        r'(?P<value>.*)$'                     # everything up to eol
   20.44 +        )
   20.45 +
   20.46 +    def optionxform(self, optionstr):
   20.47 +        """
   20.48 +        Stop converting the key to lower case, very annoying for security etc
   20.49 +        """
   20.50 +        return optionstr.strip()
   20.51 +
   20.52 +class FSMetadata:
   20.53 +    # public API
   20.54 +    def __init__(self, filename):
   20.55 +        self._filename = filename
   20.56 +
   20.57 +    def read(self):
   20.58 +        """ Find the files to read, either the old security and
   20.59 +        properties type or the new metadata type """
   20.60 +        filename = self._filename + '.metadata'
   20.61 +        if exists(filename):
   20.62 +            # found the new type, lets use that
   20.63 +            self._readMetadata()
   20.64 +        else:
   20.65 +            # not found so try the old ones
   20.66 +            self._properties = self._old_readProperties()
   20.67 +            self._security = self._old_readSecurity()
   20.68 +
   20.69 +    def getProxyRoles(self):
   20.70 +        """ Returns the proxy roles """
   20.71 +        if self.getProperties():
   20.72 +            pxy = self.getProperties().get('proxy')
   20.73 +            if pxy:
   20.74 +                return [r.strip() for r in pxy.split(',') if r.strip()]
   20.75 +        return []
   20.76 +
   20.77 +    def getSecurity(self):
   20.78 +        """ Gets the security settings """
   20.79 +        return self._security
   20.80 +
   20.81 +    def getProperties(self):
   20.82 +        """ Gets the properties settings """
   20.83 +        return self._properties
   20.84 +
   20.85 +    # private API
   20.86 +    def _readMetadata(self):
   20.87 +        """ Read the new file format using ConfigParser """
   20.88 +        self._properties = {}
   20.89 +        self._security = {}
   20.90 +
   20.91 +        try:
   20.92 +            cfg = CMFConfigParser()
   20.93 +            cfg.read(self._filename + '.metadata')
   20.94 +
   20.95 +            # the two sections we care about
   20.96 +            self._properties = self._getSectionDict(cfg, 'default')
   20.97 +            self._security = self._getSectionDict(cfg, 'security',
   20.98 +                                                  self._securityParser)
   20.99 +        except:
  20.100 +            logger.exception("Error parsing .metadata file")
  20.101 +
  20.102 +        # to add in a new value such as proxy roles,
  20.103 +        # just add in the section, call it using getSectionDict
  20.104 +        # if you need a special parser for some whacky
  20.105 +        # config, then just pass through a special parser
  20.106 +
  20.107 +    def _nullParser(self, data):
  20.108 +        """
  20.109 +        This is the standard rather boring null parser that does very little
  20.110 +        """
  20.111 +        return data
  20.112 +
  20.113 +    def _securityParser(self, data):
  20.114 +        """ A specific parser for security lines
  20.115 +
  20.116 +        Security lines must be of the format
  20.117 +
  20.118 +        Permission = (0|1):Role[,Role...]
  20.119 +
  20.120 +        Where 0|1 is the acquire permission setting
  20.121 +        and Role is the roles for this permission
  20.122 +        eg: 1:Manager or 0:Manager,Anonymous
  20.123 +        """
  20.124 +        if data.find(':') < 1:
  20.125 +            raise ValueError, "The security declaration of file " + \
  20.126 +                  "%r is in the wrong format" % self._filename
  20.127 +
  20.128 +        acquire, roles = data.split(':')
  20.129 +        roles = [r.strip() for r in roles.split(',') if r.strip()]
  20.130 +        return (int(acquire), roles)
  20.131 +
  20.132 +    def _getSectionDict(self, cfg, section, parser=None):
  20.133 +        """
  20.134 +        Get a section and put it into a dict, mostly a convenience
  20.135 +        function around the ConfigParser
  20.136 +
  20.137 +        Note: the parser is a function to parse each value, so you can
  20.138 +        have custom values for the key value pairs
  20.139 +        """
  20.140 +        if parser is None:
  20.141 +            parser = self._nullParser
  20.142 +
  20.143 +        props = {}
  20.144 +        if cfg.has_section(section):
  20.145 +            for opt in cfg.options(section):
  20.146 +                props[opt] = parser(cfg.get(section, opt))
  20.147 +            return props
  20.148 +
  20.149 +        # we need to return None if we have none to be compatible
  20.150 +        # with existing API
  20.151 +        return None
  20.152 +
  20.153 +    def _old_readProperties(self):
  20.154 +        """
  20.155 +        Reads the properties file next to an object.
  20.156 +
  20.157 +        Moved from DirectoryView.py to here with only minor
  20.158 +        modifications. Old and deprecated in favour of .metadata now
  20.159 +        """
  20.160 +        fp = self._filename + '.properties'
  20.161 +        try:
  20.162 +            f = open(fp, 'rt')
  20.163 +        except IOError:
  20.164 +            return None
  20.165 +        else:
  20.166 +            warn('.properties objects will disappear in CMF 2.0 - Use '
  20.167 +                 '.metadata objects instead.', DeprecationWarning)
  20.168 +            lines = f.readlines()
  20.169 +            f.close()
  20.170 +            props = {}
  20.171 +            for line in lines:
  20.172 +                kv = line.split('=', 1)
  20.173 +                if len(kv) == 2:
  20.174 +                    props[kv[0].strip()] = kv[1].strip()
  20.175 +                else:
  20.176 +                    logger.exception("Error parsing .properties file")
  20.177 +
  20.178 +            return props
  20.179 +
  20.180 +    def _old_readSecurity(self):
  20.181 +        """
  20.182 +        Reads the security file next to an object.
  20.183 +
  20.184 +        Moved from DirectoryView.py to here with only minor
  20.185 +        modifications. Old and deprecated in favour of .metadata now
  20.186 +        """
  20.187 +        fp = self._filename + '.security'
  20.188 +        try:
  20.189 +            f = open(fp, 'rt')
  20.190 +        except IOError:
  20.191 +            return None
  20.192 +        else:
  20.193 +            warn('.security objects will disappear in CMF 2.0 - Use '
  20.194 +                 '.metadata objects instead.', DeprecationWarning)
  20.195 +            lines = f.readlines()
  20.196 +            f.close()
  20.197 +            prm = {}
  20.198 +            for line in lines:
  20.199 +                try:
  20.200 +                    c1 = line.index(':')+1
  20.201 +                    c2 = line.index(':',c1)
  20.202 +                    permission = line[:c1-1]
  20.203 +                    acquire = bool(line[c1:c2])
  20.204 +                    proles = line[c2+1:].split(',')
  20.205 +                    roles=[]
  20.206 +                    for role in proles:
  20.207 +                        role = role.strip()
  20.208 +                        if role:
  20.209 +                            roles.append(role)
  20.210 +                except:
  20.211 +                    logger.exception("Error reading permission "
  20.212 +                                     "from .security file")
  20.213 +                prm[permission]=(acquire,roles)
  20.214 +            return prm
    21.1 new file mode 100644
    21.2 --- /dev/null
    21.3 +++ b/FSObject.py
    21.4 @@ -0,0 +1,277 @@
    21.5 +##############################################################################
    21.6 +#
    21.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    21.8 +#
    21.9 +# This software is subject to the provisions of the Zope Public License,
   21.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   21.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   21.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   21.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   21.14 +# FOR A PARTICULAR PURPOSE.
   21.15 +#
   21.16 +##############################################################################
   21.17 +""" Customizable objects that come from the filesystem (base class).
   21.18 +
   21.19 +$Id$
   21.20 +"""
   21.21 +
   21.22 +from os import path, stat
   21.23 +
   21.24 +import Globals
   21.25 +from AccessControl import ClassSecurityInfo
   21.26 +from AccessControl.Role import RoleManager
   21.27 +from AccessControl.Permission import Permission
   21.28 +from Acquisition import Implicit
   21.29 +from Acquisition import aq_base
   21.30 +from Acquisition import aq_inner
   21.31 +from Acquisition import aq_parent
   21.32 +from OFS.Cache import Cacheable
   21.33 +from OFS.SimpleItem import Item
   21.34 +from DateTime import DateTime
   21.35 +from Products.PythonScripts.standard import html_quote
   21.36 +
   21.37 +from permissions import ManagePortal
   21.38 +from permissions import View
   21.39 +from permissions import ViewManagementScreens
   21.40 +from utils import expandpath
   21.41 +from utils import getToolByName
   21.42 +
   21.43 +
   21.44 +class FSObject(Implicit, Item, RoleManager, Cacheable):
   21.45 +    """FSObject is a base class for all filesystem based look-alikes.
   21.46 +
   21.47 +    Subclasses of this class mimic ZODB based objects like Image and
   21.48 +    DTMLMethod, but are not directly modifiable from the management
   21.49 +    interface. They provide means to create a TTW editable copy, however.
   21.50 +    """
   21.51 +
   21.52 +    # Always empty for FS based, non-editable objects.
   21.53 +    title = ''
   21.54 +
   21.55 +    security = ClassSecurityInfo()
   21.56 +    security.declareObjectProtected(View)
   21.57 +
   21.58 +    _file_mod_time = 0
   21.59 +    _parsed = 0
   21.60 +
   21.61 +    def __init__(self, id, filepath, fullname=None, properties=None):
   21.62 +        if properties:
   21.63 +            # Since props come from the filesystem, this should be
   21.64 +            # safe.
   21.65 +            self.__dict__.update(properties)
   21.66 +            if fullname and properties.get('keep_extension', 0):
   21.67 +                id = fullname
   21.68 +
   21.69 +            cache = properties.get('cache')
   21.70 +            if cache:
   21.71 +                self.ZCacheable_setManagerId(cache)
   21.72 +
   21.73 +        self.id = id
   21.74 +        self.__name__ = id # __name__ is used in traceback reporting
   21.75 +        self._filepath = filepath
   21.76 +        fp = expandpath(self._filepath)
   21.77 +
   21.78 +        try: self._file_mod_time = stat(fp)[8]
   21.79 +        except: pass
   21.80 +        self._readFile(0)
   21.81 +
   21.82 +    security.declareProtected(ViewManagementScreens, 'manage_doCustomize')
   21.83 +    def manage_doCustomize(self, folder_path, RESPONSE=None):
   21.84 +        """Makes a ZODB Based clone with the same data.
   21.85 +
   21.86 +        Calls _createZODBClone for the actual work.
   21.87 +        """
   21.88 +
   21.89 +        obj = self._createZODBClone()
   21.90 +        parent = aq_parent(aq_inner(self))
   21.91 +
   21.92 +        # Preserve cache manager associations
   21.93 +        cachemgr_id = self.ZCacheable_getManagerId()
   21.94 +        if ( cachemgr_id and 
   21.95 +             getattr(obj, 'ZCacheable_setManagerId', None) is not None ):
   21.96 +            obj.ZCacheable_setManagerId(cachemgr_id)
   21.97 +
   21.98 +        # If there are proxy roles we preserve them
   21.99 +        proxy_roles = getattr(aq_base(self), '_proxy_roles', None)
  21.100 +        if proxy_roles is not None and isinstance(proxy_roles, tuple):
  21.101 +            obj._proxy_roles = tuple(self._proxy_roles)
  21.102 +
  21.103 +        # Also, preserve any permission settings that might have come
  21.104 +        # from a metadata file or from fiddling in the ZMI
  21.105 +        old_info = [x[:2] for x in self.ac_inherited_permissions(1)]
  21.106 +        for old_perm, value in old_info:
  21.107 +            p = Permission(old_perm, value, self)
  21.108 +            acquired = int(isinstance(p.getRoles(default=[]), list))
  21.109 +            rop_info = self.rolesOfPermission(old_perm)
  21.110 +            roles = [x['name'] for x in rop_info if x['selected'] != '']
  21.111 +            try:
  21.112 +                # if obj is based on OFS.ObjectManager an acquisition context is
  21.113 +                # required for _subobject_permissions()
  21.114 +                obj.__of__(parent).manage_permission(old_perm, roles=roles,
  21.115 +                                                     acquire=acquired)
  21.116 +            except ValueError:
  21.117 +                # The permission was invalid, never mind
  21.118 +                pass
  21.119 +
  21.120 +        skins_tool_namegetter = getattr(self, 'getSkinsFolderName', None)
  21.121 +        if skins_tool_namegetter is not None:
  21.122 +            skins_tool_name = skins_tool_namegetter()
  21.123 +        else:
  21.124 +            skins_tool_name = 'portal_skins'
  21.125 +
  21.126 +        id = obj.getId()
  21.127 +        fpath = tuple( folder_path.split('/') )
  21.128 +        portal_skins = getToolByName(self, skins_tool_name)
  21.129 +        folder = portal_skins.restrictedTraverse(fpath)
  21.130 +        if id in folder.objectIds():
  21.131 +            # we cant catch the badrequest so
  21.132 +            # we'll that to check before hand
  21.133 +            obj = folder._getOb(id)
  21.134 +            if RESPONSE is not None:
  21.135 +                RESPONSE.redirect('%s/manage_main?manage_tabs_message=%s' % (
  21.136 +                    obj.absolute_url(), html_quote("An object with this id already exists")
  21.137 +                    ))
  21.138 +        else:
  21.139 +            folder._verifyObjectPaste(obj, validate_src=0)
  21.140 +            folder._setObject(id, obj)
  21.141 +
  21.142 +            if RESPONSE is not None:
  21.143 +                RESPONSE.redirect('%s/%s/manage_main' % (
  21.144 +                folder.absolute_url(), id))
  21.145 +
  21.146 +        if RESPONSE is not None:
  21.147 +            RESPONSE.redirect('%s/%s/manage_main' % (
  21.148 +                folder.absolute_url(), id))
  21.149 +
  21.150 +    def _createZODBClone(self):
  21.151 +        """Create a ZODB (editable) equivalent of this object."""
  21.152 +        raise NotImplementedError, "This should be implemented in a subclass."
  21.153 +
  21.154 +    def _readFile(self, reparse):
  21.155 +        """Read the data from the filesystem.
  21.156 +
  21.157 +        Read the file indicated by exandpath(self._filepath), and parse the
  21.158 +        data if necessary.  'reparse' is set when reading the second
  21.159 +        time and beyond.
  21.160 +        """
  21.161 +        raise NotImplementedError, "This should be implemented in a subclass."
  21.162 +
  21.163 +    # Refresh our contents from the filesystem if that is newer and we are
  21.164 +    # running in debug mode.
  21.165 +    def _updateFromFS(self):
  21.166 +        parsed = self._parsed
  21.167 +        if not parsed or Globals.DevelopmentMode:
  21.168 +            fp = expandpath(self._filepath)
  21.169 +            try:    mtime=stat(fp)[8]
  21.170 +            except: mtime=0
  21.171 +            if not parsed or mtime != self._file_mod_time:
  21.172 +                # if we have to read the file again, remove the cache
  21.173 +                self.ZCacheable_invalidate()
  21.174 +                self._readFile(1)
  21.175 +                self._file_mod_time = mtime
  21.176 +                self._parsed = 1
  21.177 +
  21.178 +    security.declareProtected(View, 'get_size')
  21.179 +    def get_size(self):
  21.180 +        """Get the size of the underlying file."""
  21.181 +        fp = expandpath(self._filepath)
  21.182 +        return path.getsize(fp)
  21.183 +
  21.184 +    security.declareProtected(View, 'getModTime')
  21.185 +    def getModTime(self):
  21.186 +        """Return the last_modified date of the file we represent.
  21.187 +
  21.188 +        Returns a DateTime instance.
  21.189 +        """
  21.190 +        self._updateFromFS()
  21.191 +        return DateTime(self._file_mod_time)
  21.192 +
  21.193 +    security.declareProtected(ViewManagementScreens, 'getObjectFSPath')
  21.194 +    def getObjectFSPath(self):
  21.195 +        """Return the path of the file we represent"""
  21.196 +        self._updateFromFS()
  21.197 +        return self._filepath
  21.198 +
  21.199 +Globals.InitializeClass(FSObject)
  21.200 +
  21.201 +
  21.202 +class BadFile( FSObject ):
  21.203 +    """
  21.204 +        Represent a file which was not readable or parseable
  21.205 +        as its intended type.
  21.206 +    """
  21.207 +    meta_type = 'Bad File'
  21.208 +    icon = 'p_/broken'
  21.209 +
  21.210 +    BAD_FILE_VIEW = """\
  21.211 +<dtml-var manage_page_header>
  21.212 +<dtml-var manage_tabs>
  21.213 +<h2> Bad Filesystem Object: &dtml-getId; </h2>
  21.214 +
  21.215 +<h3> File Contents </h3>
  21.216 +<pre>
  21.217 +<dtml-var getFileContents>
  21.218 +</pre>
  21.219 +
  21.220 +<h3> Exception </h3>
  21.221 +<pre>
  21.222 +<dtml-var getExceptionText>
  21.223 +</pre>
  21.224 +<dtml-var manage_page_footer>
  21.225 +"""
  21.226 +
  21.227 +    manage_options=(
  21.228 +        {'label':'Error', 'action':'manage_showError'},
  21.229 +        )
  21.230 +
  21.231 +    def __init__( self, id, filepath, exc_str=''
  21.232 +                , fullname=None, properties=None):
  21.233 +        id = fullname or id # Use the whole filename.
  21.234 +        self.exc_str = exc_str
  21.235 +        self.file_contents = ''
  21.236 +        FSObject.__init__(self, id, filepath, fullname, properties)
  21.237 +
  21.238 +    security = ClassSecurityInfo()
  21.239 +
  21.240 +    showError = Globals.HTML( BAD_FILE_VIEW )
  21.241 +    security.declareProtected(ManagePortal, 'manage_showError')
  21.242 +    def manage_showError( self, REQUEST ):
  21.243 +        """
  21.244 +        """
  21.245 +        return self.showError( self, REQUEST )
  21.246 +
  21.247 +    security.declarePrivate( '_readFile' )
  21.248 +    def _readFile( self, reparse ):
  21.249 +        """Read the data from the filesystem.
  21.250 +
  21.251 +        Read the file indicated by exandpath(self._filepath), and parse the
  21.252 +        data if necessary.  'reparse' is set when reading the second
  21.253 +        time and beyond.
  21.254 +        """
  21.255 +        try:
  21.256 +            fp = expandpath(self._filepath)
  21.257 +            file = open(fp, 'rb')
  21.258 +            try:
  21.259 +                data = self.file_contents = file.read()
  21.260 +            finally:
  21.261 +                file.close()
  21.262 +        except:  # No errors of any sort may propagate
  21.263 +            data = self.file_contents = None #give up
  21.264 +        return data
  21.265 +
  21.266 +    security.declarePublic( 'getFileContents' )
  21.267 +    def getFileContents( self ):
  21.268 +        """
  21.269 +            Return the contents of the file, if we could read it.
  21.270 +        """
  21.271 +        return self.file_contents
  21.272 +
  21.273 +    security.declarePublic( 'getExceptionText' )
  21.274 +    def getExceptionText( self ):
  21.275 +        """
  21.276 +            Return the exception thrown while reading or parsing
  21.277 +            the file.
  21.278 +        """
  21.279 +        return self.exc_str
  21.280 +
  21.281 +Globals.InitializeClass( BadFile )
    22.1 new file mode 100644
    22.2 --- /dev/null
    22.3 +++ b/FSPageTemplate.py
    22.4 @@ -0,0 +1,233 @@
    22.5 +##############################################################################
    22.6 +#
    22.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    22.8 +#
    22.9 +# This software is subject to the provisions of the Zope Public License,
   22.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   22.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   22.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   22.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   22.14 +# FOR A PARTICULAR PURPOSE.
   22.15 +#
   22.16 +##############################################################################
   22.17 +""" Customizable page templates that come from the filesystem.
   22.18 +
   22.19 +$Id$
   22.20 +"""
   22.21 +
   22.22 +import re, sys
   22.23 +
   22.24 +import Globals
   22.25 +from DocumentTemplate.DT_Util import html_quote
   22.26 +from AccessControl import getSecurityManager, ClassSecurityInfo
   22.27 +from OFS.Cache import Cacheable
   22.28 +from Shared.DC.Scripts.Script import Script
   22.29 +from Products.PageTemplates.PageTemplate import PageTemplate
   22.30 +from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate, Src
   22.31 +
   22.32 +from permissions import FTPAccess
   22.33 +from permissions import View
   22.34 +from permissions import ViewManagementScreens
   22.35 +from DirectoryView import registerFileExtension
   22.36 +from DirectoryView import registerMetaType
   22.37 +from FSObject import FSObject
   22.38 +from utils import _setCacheHeaders, _checkConditionalGET
   22.39 +from utils import expandpath
   22.40 +
   22.41 +xml_detect_re = re.compile('^\s*<\?xml\s+(?:[^>]*?encoding=["\']([^"\'>]+))?')
   22.42 +_marker = []  # Create a new marker object.
   22.43 +
   22.44 +
   22.45 +class FSPageTemplate(FSObject, Script, PageTemplate):
   22.46 +    "Wrapper for Page Template"
   22.47 +
   22.48 +    meta_type = 'Filesystem Page Template'
   22.49 +
   22.50 +    _owner = None  # Unowned
   22.51 +
   22.52 +    manage_options=(
   22.53 +        (
   22.54 +            {'label':'Customize', 'action':'manage_main'},
   22.55 +            {'label':'Test', 'action':'ZScriptHTML_tryForm'},
   22.56 +            )
   22.57 +            +Cacheable.manage_options
   22.58 +        )
   22.59 +
   22.60 +    security = ClassSecurityInfo()
   22.61 +    security.declareObjectProtected(View)
   22.62 +
   22.63 +    security.declareProtected(ViewManagementScreens, 'manage_main')
   22.64 +    manage_main = Globals.DTMLFile('dtml/custpt', globals())
   22.65 +
   22.66 +    # Declare security for unprotected PageTemplate methods.
   22.67 +    security.declarePrivate('pt_edit', 'write')
   22.68 +
   22.69 +    def __init__(self, id, filepath, fullname=None, properties=None):
   22.70 +        FSObject.__init__(self, id, filepath, fullname, properties)
   22.71 +        self.ZBindings_edit(self._default_bindings)
   22.72 +
   22.73 +    def _createZODBClone(self):
   22.74 +        """Create a ZODB (editable) equivalent of this object."""
   22.75 +        obj = ZopePageTemplate(self.getId(), self._text, self.content_type)
   22.76 +        obj.expand = 0
   22.77 +        obj.write(self.read())
   22.78 +        return obj
   22.79 +
   22.80 +#    def ZCacheable_isCachingEnabled(self):
   22.81 +#        return 0
   22.82 +
   22.83 +    def _readFile(self, reparse):
   22.84 +        fp = expandpath(self._filepath)
   22.85 +        file = open(fp, 'rU')    # not 'rb', as this is a text file!
   22.86 +        try:
   22.87 +            data = file.read()
   22.88 +        finally:
   22.89 +            file.close()
   22.90 +
   22.91 +        if reparse:
   22.92 +            # If we already have a content_type set it must come from a
   22.93 +            # .metadata file and we should always honor that. The content
   22.94 +            # type is initialized as text/html by default, so we only
   22.95 +            # attempt further detection if the default is encountered.
   22.96 +            # One previous misbehavior remains: It is not possible to
   22.97 +            # force a text./html type if parsing detects it as XML.
   22.98 +            if getattr(self, 'content_type', 'text/html') == 'text/html':
   22.99 +                xml_info = xml_detect_re.match(data)
  22.100 +                if xml_info:
  22.101 +                    # Smells like xml
  22.102 +                    # set "content_type" from the XML declaration
  22.103 +                    encoding = xml_info.group(1) or 'utf-8'
  22.104 +                    self.content_type = 'text/xml; charset=%s' % encoding
  22.105 +
  22.106 +            self.write(data)
  22.107 +
  22.108 +    security.declarePrivate('read')
  22.109 +    def read(self):
  22.110 +        # Tie in on an opportunity to auto-update
  22.111 +        self._updateFromFS()
  22.112 +        return FSPageTemplate.inheritedAttribute('read')(self)
  22.113 +
  22.114 +    ### The following is mainly taken from ZopePageTemplate.py ###
  22.115 +
  22.116 +    expand = 0
  22.117 +
  22.118 +    func_defaults = None
  22.119 +    func_code = ZopePageTemplate.func_code
  22.120 +    _default_bindings = ZopePageTemplate._default_bindings
  22.121 +
  22.122 +    security.declareProtected(View, '__call__')
  22.123 +
  22.124 +    def pt_macros(self):
  22.125 +        # Tie in on an opportunity to auto-reload
  22.126 +        self._updateFromFS()
  22.127 +        return FSPageTemplate.inheritedAttribute('pt_macros')(self)
  22.128 +
  22.129 +    def pt_render(self, source=0, extra_context={}):
  22.130 +        self._updateFromFS()  # Make sure the template has been loaded.
  22.131 +
  22.132 +        if not source:
  22.133 +            # If we have a conditional get, set status 304 and return
  22.134 +            # no content
  22.135 +            if _checkConditionalGET(self, extra_context):
  22.136 +                return ''
  22.137 +        
  22.138 +        result = FSPageTemplate.inheritedAttribute('pt_render')(
  22.139 +                                self, source, extra_context
  22.140 +                                )
  22.141 +        if not source:
  22.142 +            _setCacheHeaders(self, extra_context)
  22.143 +        return result
  22.144 +
  22.145 +    security.declareProtected(ViewManagementScreens, 'pt_source_file')
  22.146 +    def pt_source_file(self):
  22.147 +
  22.148 +        """ Return a file name to be compiled into the TAL code.
  22.149 +        """
  22.150 +        return 'file:%s' % self._filepath
  22.151 +
  22.152 +    security.declarePrivate( '_ZPT_exec' )
  22.153 +    _ZPT_exec = ZopePageTemplate._exec.im_func
  22.154 +
  22.155 +    security.declarePrivate( '_exec' )
  22.156 +    def _exec(self, bound_names, args, kw):
  22.157 +        """Call a FSPageTemplate"""
  22.158 +        try:
  22.159 +            response = self.REQUEST.RESPONSE
  22.160 +        except AttributeError:
  22.161 +            response = None
  22.162 +        # Read file first to get a correct content_type default value.
  22.163 +        self._updateFromFS()
  22.164 +
  22.165 +        if not kw.has_key('args'):
  22.166 +            kw['args'] = args
  22.167 +        bound_names['options'] = kw
  22.168 +
  22.169 +        try:
  22.170 +            response = self.REQUEST.RESPONSE
  22.171 +            if not response.headers.has_key('content-type'):
  22.172 +                response.setHeader('content-type', self.content_type)
  22.173 +        except AttributeError:
  22.174 +            pass
  22.175 +
  22.176 +        security=getSecurityManager()
  22.177 +        bound_names['user'] = security.getUser()
  22.178 +
  22.179 +        # Retrieve the value from the cache.
  22.180 +        keyset = None
  22.181 +        if self.ZCacheable_isCachingEnabled():
  22.182 +            # Prepare a cache key.
  22.183 +            keyset = {
  22.184 +                      # Why oh why?
  22.185 +                      # All this code is cut and paste
  22.186 +                      # here to make sure that we
  22.187 +                      # dont call _getContext and hence can't cache
  22.188 +                      # Annoying huh?
  22.189 +                      'here': self.aq_parent.getPhysicalPath(),
  22.190 +                      'bound_names': bound_names}
  22.191 +            result = self.ZCacheable_get(keywords=keyset)
  22.192 +            if result is not None:
  22.193 +                # Got a cached value.
  22.194 +                return result
  22.195 +
  22.196 +        # Execute the template in a new security context.
  22.197 +        security.addContext(self)
  22.198 +        try:
  22.199 +            result = self.pt_render(extra_context=bound_names)
  22.200 +            if keyset is not None:
  22.201 +                # Store the result in the cache.
  22.202 +                self.ZCacheable_set(result, keywords=keyset)
  22.203 +            return result
  22.204 +        finally:
  22.205 +            security.removeContext(self)
  22.206 +
  22.207 +        return result
  22.208 +
  22.209 +    # Copy over more methods
  22.210 +    security.declareProtected(FTPAccess, 'manage_FTPget')
  22.211 +    manage_FTPget = ZopePageTemplate.manage_FTPget.im_func
  22.212 +
  22.213 +    security.declareProtected(View, 'get_size')
  22.214 +    get_size = ZopePageTemplate.get_size.im_func
  22.215 +    getSize = get_size
  22.216 +
  22.217 +    security.declareProtected(ViewManagementScreens, 'PrincipiaSearchSource')
  22.218 +    PrincipiaSearchSource = ZopePageTemplate.PrincipiaSearchSource.im_func
  22.219 +
  22.220 +    security.declareProtected(ViewManagementScreens, 'document_src')
  22.221 +    document_src = ZopePageTemplate.document_src.im_func
  22.222 +
  22.223 +    pt_getContext = ZopePageTemplate.pt_getContext.im_func
  22.224 +
  22.225 +    ZScriptHTML_tryParams = ZopePageTemplate.ZScriptHTML_tryParams.im_func
  22.226 +
  22.227 +    source_dot_xml = Src()
  22.228 +
  22.229 +setattr(FSPageTemplate, 'source.xml',  FSPageTemplate.source_dot_xml)
  22.230 +setattr(FSPageTemplate, 'source.html', FSPageTemplate.source_dot_xml)
  22.231 +Globals.InitializeClass(FSPageTemplate)
  22.232 +
  22.233 +registerFileExtension('pt', FSPageTemplate)
  22.234 +registerFileExtension('zpt', FSPageTemplate)
  22.235 +registerFileExtension('html', FSPageTemplate)
  22.236 +registerFileExtension('htm', FSPageTemplate)
  22.237 +registerMetaType('Page Template', FSPageTemplate)
    23.1 new file mode 100644
    23.2 --- /dev/null
    23.3 +++ b/FSPropertiesObject.py
    23.4 @@ -0,0 +1,143 @@
    23.5 +##############################################################################
    23.6 +#
    23.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    23.8 +#
    23.9 +# This software is subject to the provisions of the Zope Public License,
   23.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   23.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   23.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   23.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   23.14 +# FOR A PARTICULAR PURPOSE.
   23.15 +#
   23.16 +##############################################################################
   23.17 +""" Customizable properties that come from the filesystem.
   23.18 +
   23.19 +$Id$
   23.20 +"""
   23.21 +import Globals
   23.22 +from AccessControl import ClassSecurityInfo
   23.23 +from Acquisition import ImplicitAcquisitionWrapper
   23.24 +from OFS.Folder import Folder
   23.25 +from OFS.PropertyManager import PropertyManager
   23.26 +from ZPublisher.Converters import get_converter
   23.27 +
   23.28 +from DirectoryView import registerFileExtension
   23.29 +from DirectoryView import registerMetaType
   23.30 +from FSObject import FSObject
   23.31 +from permissions import ViewManagementScreens
   23.32 +from utils import _dtmldir
   23.33 +from utils import expandpath
   23.34 +
   23.35 +
   23.36 +class FSPropertiesObject (FSObject, PropertyManager):
   23.37 +    """FSPropertiesObjects simply hold properties."""
   23.38 +
   23.39 +    meta_type = 'Filesystem Properties Object'
   23.40 +
   23.41 +    manage_options = ({'label':'Customize', 'action':'manage_main'},)
   23.42 +
   23.43 +    security = ClassSecurityInfo()
   23.44 +
   23.45 +    security.declareProtected(ViewManagementScreens, 'manage_main')
   23.46 +    manage_main = Globals.DTMLFile('custprops', _dtmldir)
   23.47 +
   23.48 +    # Declare all (inherited) mutating methods private.
   23.49 +    security.declarePrivate('manage_addProperty')
   23.50 +    security.declarePrivate('manage_editProperties')
   23.51 +    security.declarePrivate('manage_delProperties')
   23.52 +    security.declarePrivate('manage_changeProperties')
   23.53 +    security.declarePrivate('manage_propertiesForm')
   23.54 +    security.declarePrivate('manage_propertyTypeForm')
   23.55 +    security.declarePrivate('manage_changePropertyTypes')
   23.56 +
   23.57 +    security.declareProtected(ViewManagementScreens, 'manage_doCustomize')
   23.58 +    def manage_doCustomize(self, folder_path, RESPONSE=None):
   23.59 +        """Makes a ZODB Based clone with the same data.
   23.60 +
   23.61 +        Calls _createZODBClone for the actual work.
   23.62 +        """
   23.63 +        # Overridden here to provide a different redirect target.
   23.64 +
   23.65 +        FSObject.manage_doCustomize(self, folder_path, RESPONSE)
   23.66 +
   23.67 +        if RESPONSE is not None:
   23.68 +            fpath = tuple(folder_path.split('/'))
   23.69 +            folder = self.restrictedTraverse(fpath)
   23.70 +            RESPONSE.redirect('%s/%s/manage_propertiesForm' % (
   23.71 +                folder.absolute_url(), self.getId()))
   23.72 +
   23.73 +    def _createZODBClone(self):
   23.74 +        """Create a ZODB (editable) equivalent of this object."""
   23.75 +        # Create a Folder to hold the properties.
   23.76 +        obj = Folder()
   23.77 +        obj.id = self.getId()
   23.78 +        map = []
   23.79 +        for p in self._properties:
   23.80 +            # This should be secure since the properties come
   23.81 +            # from the filesystem.
   23.82 +            setattr(obj, p['id'], getattr(self, p['id']))
   23.83 +            map.append({'id': p['id'],
   23.84 +                        'type': p['type'],
   23.85 +                        'mode': 'wd',})
   23.86 +        obj._properties = tuple(map)
   23.87 +
   23.88 +        return obj
   23.89 +
   23.90 +    def _readFile(self, reparse):
   23.91 +        """Read the data from the filesystem.
   23.92 +
   23.93 +        Read the file (indicated by exandpath(self._filepath), and parse the
   23.94 +        data if necessary.
   23.95 +        """
   23.96 +        fp = expandpath(self._filepath)
   23.97 +
   23.98 +        file = open(fp, 'r')    # not 'rb', as this is a text file!
   23.99 +        try:
  23.100 +            lines = file.readlines()
  23.101 +        finally:
  23.102 +            file.close()
  23.103 +
  23.104 +        map = []
  23.105 +        lino=0
  23.106 +
  23.107 +        for line in lines:
  23.108 +
  23.109 +            lino = lino + 1
  23.110 +            line = line.strip()
  23.111 +
  23.112 +            if not line or line[0] == '#':
  23.113 +                continue
  23.114 +
  23.115 +            try:
  23.116 +                propname, proptv = line.split(':',1)
  23.117 +                #XXX multi-line properties?
  23.118 +                proptype, propvstr = proptv.split( '=', 1 )
  23.119 +                propname = propname.strip()
  23.120 +                proptype = proptype.strip()
  23.121 +                propvstr = propvstr.strip()
  23.122 +                converter = get_converter( proptype, lambda x: x )
  23.123 +                propvalue = converter( propvstr )
  23.124 +                # Should be safe since we're loading from
  23.125 +                # the filesystem.
  23.126 +                setattr(self, propname, propvalue)
  23.127 +                map.append({'id':propname,
  23.128 +                            'type':proptype,
  23.129 +                            'mode':'',
  23.130 +                            'default_value':propvalue,
  23.131 +                            })
  23.132 +            except:
  23.133 +                raise ValueError, ( 'Error processing line %s of %s:\n%s'
  23.134 +                                  % (lino,fp,line) )
  23.135 +        self._properties = tuple(map)
  23.136 +
  23.137 +    if Globals.DevelopmentMode:
  23.138 +        # Provide an opportunity to update the properties.
  23.139 +        def __of__(self, parent):
  23.140 +            self = ImplicitAcquisitionWrapper(self, parent)
  23.141 +            self._updateFromFS()
  23.142 +            return self
  23.143 +
  23.144 +Globals.InitializeClass(FSPropertiesObject)
  23.145 +
  23.146 +registerFileExtension('props', FSPropertiesObject)
  23.147 +registerMetaType('Properties Object', FSPropertiesObject)
    24.1 new file mode 100644
    24.2 --- /dev/null
    24.3 +++ b/FSPythonScript.py
    24.4 @@ -0,0 +1,293 @@
    24.5 +##############################################################################
    24.6 +#
    24.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    24.8 +#
    24.9 +# This software is subject to the provisions of the Zope Public License,
   24.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   24.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   24.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   24.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   24.14 +# FOR A PARTICULAR PURPOSE.
   24.15 +#
   24.16 +##############################################################################
   24.17 +""" Customizable Python scripts that come from the filesystem.
   24.18 +
   24.19 +$Id$
   24.20 +"""
   24.21 +
   24.22 +import new
   24.23 +
   24.24 +from AccessControl import ClassSecurityInfo
   24.25 +from AccessControl import getSecurityManager
   24.26 +from ComputedAttribute import ComputedAttribute
   24.27 +from Globals import DTMLFile
   24.28 +from Globals import InitializeClass
   24.29 +from OFS.Cache import Cacheable
   24.30 +from Products.PythonScripts.PythonScript import PythonScript
   24.31 +from Shared.DC.Scripts.Script import Script
   24.32 +
   24.33 +from DirectoryView import registerFileExtension
   24.34 +from DirectoryView import registerMetaType
   24.35 +from FSObject import FSObject
   24.36 +from permissions import FTPAccess
   24.37 +from permissions import View
   24.38 +from permissions import ViewManagementScreens
   24.39 +from utils import _dtmldir
   24.40 +from utils import expandpath
   24.41 +
   24.42 +_marker = []
   24.43 +
   24.44 +
   24.45 +class bad_func_code:
   24.46 +    co_varnames = ()
   24.47 +    co_argcount = 0
   24.48 +
   24.49 +
   24.50 +class FSPythonScript (FSObject, Script):
   24.51 +    """FSPythonScripts act like Python Scripts but are not directly
   24.52 +    modifiable from the management interface."""
   24.53 +
   24.54 +    meta_type = 'Filesystem Script (Python)'
   24.55 +    _params = _body = ''
   24.56 +    _v_f = None
   24.57 +    _proxy_roles = ()
   24.58 +
   24.59 +    _owner = None  # Unowned
   24.60 +
   24.61 +    manage_options=(
   24.62 +        (
   24.63 +            {'label':'Customize', 'action':'manage_main'},
   24.64 +            {'label':'Test',
   24.65 +             'action':'ZScriptHTML_tryForm',
   24.66 +             'help': ('PythonScripts', 'PythonScript_test.stx')},
   24.67 +            )
   24.68 +            + Cacheable.manage_options
   24.69 +        )
   24.70 +
   24.71 +    # Use declarative security
   24.72 +    security = ClassSecurityInfo()
   24.73 +    security.declareObjectProtected(View)
   24.74 +    security.declareProtected(View, 'index_html',)
   24.75 +    # Prevent the bindings from being edited TTW
   24.76 +    security.declarePrivate('ZBindings_edit','ZBindingsHTML_editForm',
   24.77 +                            'ZBindingsHTML_editAction')
   24.78 +
   24.79 +    security.declareProtected(ViewManagementScreens, 'manage_main')
   24.80 +    manage_main = DTMLFile('custpy', _dtmldir)
   24.81 +
   24.82 +    def _createZODBClone(self):
   24.83 +        """Create a ZODB (editable) equivalent of this object."""
   24.84 +        obj = PythonScript(self.getId())
   24.85 +        obj.write(self.read())
   24.86 +        return obj
   24.87 +
   24.88 +    def _readFile(self, reparse):
   24.89 +        """Read the data from the filesystem.
   24.90 +
   24.91 +        Read the file (indicated by exandpath(self._filepath), and parse the
   24.92 +        data if necessary.
   24.93 +        """
   24.94 +        fp = expandpath(self._filepath)
   24.95 +        file = open(fp, 'rU')
   24.96 +        try: data = file.read()
   24.97 +        finally: file.close()
   24.98 +        if reparse:
   24.99 +            self._write(data, reparse)
  24.100 +
  24.101 +    def _validateProxy(self, roles=None):
  24.102 +        pass
  24.103 +
  24.104 +    def __render_with_namespace__(self, namespace):
  24.105 +        '''Calls the script.'''
  24.106 +        self._updateFromFS()
  24.107 +        return Script.__render_with_namespace__(self, namespace)
  24.108 +
  24.109 +    def __call__(self, *args, **kw):
  24.110 +        '''Calls the script.'''
  24.111 +        self._updateFromFS()
  24.112 +        return Script.__call__(self, *args, **kw)
  24.113 +
  24.114 +    #### The following is mainly taken from PythonScript.py ###
  24.115 +
  24.116 +    def _exec(self, bound_names, args, kw):
  24.117 +        """Call a Python Script
  24.118 +
  24.119 +        Calling a Python Script is an actual function invocation.
  24.120 +        """
  24.121 +        # do caching
  24.122 +        keyset = None
  24.123 +        if self.ZCacheable_isCachingEnabled():
  24.124 +            # Prepare a cache key.
  24.125 +            keyset = kw.copy()
  24.126 +            asgns = self.getBindingAssignments()
  24.127 +            name_context = asgns.getAssignedName('name_context', None)
  24.128 +            if name_context:
  24.129 +                keyset[name_context] = self.aq_parent.getPhysicalPath()
  24.130 +            name_subpath = asgns.getAssignedName('name_subpath', None)
  24.131 +            if name_subpath:
  24.132 +                keyset[name_subpath] = self._getTraverseSubpath()
  24.133 +            # Note: perhaps we should cache based on name_ns also.
  24.134 +            keyset['*'] = args
  24.135 +            result = self.ZCacheable_get(keywords=keyset, default=_marker)
  24.136 +            if result is not _marker:
  24.137 +                # Got a cached value.
  24.138 +                return result
  24.139 +
  24.140 +        # Prepare the function.
  24.141 +        f = self._v_f
  24.142 +        if f is None:
  24.143 +            # The script has errors.
  24.144 +            __traceback_supplement__ = (
  24.145 +                FSPythonScriptTracebackSupplement, self, 0)
  24.146 +            raise RuntimeError, '%s has errors.' % self._filepath
  24.147 +
  24.148 +        # Updating func_globals directly is not thread safe here.
  24.149 +        # In normal PythonScripts, every thread has its own
  24.150 +        # copy of the function.  But in FSPythonScripts
  24.151 +        # there is only one copy.  So here's another way.
  24.152 +        new_globals = f.func_globals.copy()
  24.153 +        new_globals['__traceback_supplement__'] = (
  24.154 +            FSPythonScriptTracebackSupplement, self)
  24.155 +        new_globals['__file__'] = self._filepath
  24.156 +        if bound_names:
  24.157 +            new_globals.update(bound_names)
  24.158 +        if f.func_defaults:
  24.159 +            f = new.function(f.func_code, new_globals, f.func_name,
  24.160 +                             f.func_defaults)
  24.161 +        else:
  24.162 +            f = new.function(f.func_code, new_globals, f.func_name)
  24.163 +
  24.164 +        # Execute the function in a new security context.
  24.165 +        security=getSecurityManager()
  24.166 +        security.addContext(self)
  24.167 +        try:
  24.168 +            result = f(*args, **kw)
  24.169 +            if keyset is not None:
  24.170 +                # Store the result in the cache.
  24.171 +                self.ZCacheable_set(result, keywords=keyset)
  24.172 +            return result
  24.173 +        finally:
  24.174 +            security.removeContext(self)
  24.175 +
  24.176 +    security.declareProtected(ViewManagementScreens, 'getModTime')
  24.177 +    # getModTime defined in FSObject
  24.178 +
  24.179 +    security.declareProtected(ViewManagementScreens, 'ZScriptHTML_tryForm')
  24.180 +    # ZScriptHTML_tryForm defined in Shared.DC.Scripts.Script.Script
  24.181 +
  24.182 +    def ZScriptHTML_tryParams(self):
  24.183 +        """Parameters to test the script with."""
  24.184 +        param_names = []
  24.185 +        for name in self._params.split(','):
  24.186 +            name = name.strip()
  24.187 +            if name and name[0] != '*':
  24.188 +                param_names.append( name.split('=', 1)[0] )
  24.189 +        return param_names
  24.190 +
  24.191 +    security.declareProtected(ViewManagementScreens, 'read')
  24.192 +    def read(self):
  24.193 +        self._updateFromFS()
  24.194 +        return self._source
  24.195 +
  24.196 +    security.declareProtected(ViewManagementScreens, 'document_src')
  24.197 +    def document_src(self, REQUEST=None, RESPONSE=None):
  24.198 +        """Return unprocessed document source."""
  24.199 +
  24.200 +        if RESPONSE is not None:
  24.201 +            RESPONSE.setHeader('Content-Type', 'text/plain')
  24.202 +        return self._source
  24.203 +
  24.204 +    security.declareProtected(ViewManagementScreens, 'PrincipiaSearchSource')
  24.205 +    def PrincipiaSearchSource(self):
  24.206 +        "Support for searching - the document's contents are searched."
  24.207 +        return "%s\n%s" % (self._params, self._body)
  24.208 +
  24.209 +    security.declareProtected(ViewManagementScreens, 'params')
  24.210 +    def params(self): return self._params
  24.211 +
  24.212 +    security.declareProtected(ViewManagementScreens, 'manage_haveProxy')
  24.213 +    manage_haveProxy = PythonScript.manage_haveProxy.im_func
  24.214 +
  24.215 +    security.declareProtected(ViewManagementScreens, 'body')
  24.216 +    def body(self): return self._body
  24.217 +
  24.218 +    security.declareProtected(ViewManagementScreens, 'get_size')
  24.219 +    def get_size(self): return len(self.read())
  24.220 +
  24.221 +    security.declareProtected(FTPAccess, 'manage_FTPget')
  24.222 +    def manage_FTPget(self):
  24.223 +        "Get source for FTP download"
  24.224 +        self.REQUEST.RESPONSE.setHeader('Content-Type', 'text/plain')
  24.225 +        return self.read()
  24.226 +
  24.227 +    def _write(self, text, compile):
  24.228 +        '''
  24.229 +        Parses the source, storing the body, params, title, bindings,
  24.230 +        and source in self.  If compile is set, compiles the
  24.231 +        function.
  24.232 +        '''
  24.233 +        ps = PythonScript(self.id)
  24.234 +        ps.write(text)
  24.235 +        if compile:
  24.236 +            ps._makeFunction(1)
  24.237 +            self._v_f = f = ps._v_f
  24.238 +            if f is not None:
  24.239 +                self.func_code = f.func_code
  24.240 +                self.func_defaults = f.func_defaults
  24.241 +            else:
  24.242 +                # There were errors in the compile.
  24.243 +                # No signature.
  24.244 +                self.func_code = bad_func_code()
  24.245 +                self.func_defaults = None
  24.246 +        self._body = ps._body
  24.247 +        self._params = ps._params
  24.248 +        self.title = ps.title
  24.249 +        self._setupBindings(ps.getBindingAssignments().getAssignedNames())
  24.250 +        self._source = ps.read()  # Find out what the script sees.
  24.251 +
  24.252 +    def func_defaults(self):
  24.253 +        # This ensures func_code and func_defaults are
  24.254 +        # set when the code hasn't been compiled yet,
  24.255 +        # just in time for mapply().  Truly odd, but so is mapply(). :P
  24.256 +        self._updateFromFS()
  24.257 +        return self.__dict__.get('func_defaults', None)
  24.258 +    func_defaults = ComputedAttribute(func_defaults, 1)
  24.259 +
  24.260 +    def func_code(self):
  24.261 +        # See func_defaults.
  24.262 +        self._updateFromFS()
  24.263 +        return self.__dict__.get('func_code', None)
  24.264 +    func_code = ComputedAttribute(func_code, 1)
  24.265 +
  24.266 +    def title(self):
  24.267 +        # See func_defaults.
  24.268 +        self._updateFromFS()
  24.269 +        return self.__dict__.get('title', None)
  24.270 +    title = ComputedAttribute(title, 1)
  24.271 +
  24.272 +    def getBindingAssignments(self):
  24.273 +        # Override of the version in Bindings.py.
  24.274 +        # This version ensures that bindings get loaded on demand.
  24.275 +        if not hasattr(self, '_bind_names'):
  24.276 +            # Set a default first to avoid recursion
  24.277 +            self._setupBindings()
  24.278 +            # Now do it for real
  24.279 +            self._updateFromFS()
  24.280 +        return self._bind_names
  24.281 +
  24.282 +InitializeClass(FSPythonScript)
  24.283 +
  24.284 +
  24.285 +class FSPythonScriptTracebackSupplement:
  24.286 +    """Implementation of ITracebackSupplement
  24.287 +
  24.288 +    Makes script-specific info available in exception tracebacks.
  24.289 +    """
  24.290 +    def __init__(self, script, line=-1):
  24.291 +        self.object = script
  24.292 +        # If line is set to -1, it means to use tb_lineno.
  24.293 +        self.line = line
  24.294 +
  24.295 +
  24.296 +registerFileExtension('py', FSPythonScript)
  24.297 +registerMetaType('Script (Python)', FSPythonScript)
    25.1 new file mode 100644
    25.2 --- /dev/null
    25.3 +++ b/FSSTXMethod.py
    25.4 @@ -0,0 +1,160 @@
    25.5 +##############################################################################
    25.6 +#
    25.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    25.8 +#
    25.9 +# This software is subject to the provisions of the Zope Public License,
   25.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   25.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   25.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   25.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   25.14 +# FOR A PARTICULAR PURPOSE.
   25.15 +#
   25.16 +##############################################################################
   25.17 +""" FSSTXMethod: Filesystem methodish Structured Text document.
   25.18 +
   25.19 +$Id$
   25.20 +"""
   25.21 +
   25.22 +import Globals
   25.23 +from AccessControl import ClassSecurityInfo
   25.24 +from StructuredText.StructuredText import HTML
   25.25 +
   25.26 +from permissions import FTPAccess
   25.27 +from permissions import View
   25.28 +from permissions import ViewManagementScreens
   25.29 +from DirectoryView import registerFileExtension
   25.30 +from DirectoryView import registerMetaType
   25.31 +from FSObject import FSObject
   25.32 +from utils import _dtmldir
   25.33 +from utils import expandpath
   25.34 +
   25.35 +
   25.36 +class FSSTXMethod( FSObject ):
   25.37 +    """
   25.38 +        A chunk of StructuredText, rendered as a skin method of a
   25.39 +        CMFSite.
   25.40 +    """
   25.41 +
   25.42 +    meta_type = 'Filesystem STX Method'
   25.43 +
   25.44 +    manage_options=( { 'label'      : 'Customize'
   25.45 +                     , 'action'     : 'manage_main'
   25.46 +                     }
   25.47 +                   , { 'label'      : 'View'
   25.48 +                     , 'action'     : ''
   25.49 +                     , 'help'       : ('OFSP'
   25.50 +                                      ,'DTML-DocumentOrMethod_View.stx'
   25.51 +                                      )
   25.52 +                     }
   25.53 +                   )
   25.54 +
   25.55 +    security = ClassSecurityInfo()
   25.56 +    security.declareObjectProtected( View )
   25.57 +
   25.58 +    security.declareProtected( ViewManagementScreens, 'manage_main')
   25.59 +    manage_main = Globals.DTMLFile( 'custstx', _dtmldir )
   25.60 +
   25.61 +    #
   25.62 +    #   FSObject interface
   25.63 +    #
   25.64 +    def _createZODBClone(self):
   25.65 +        """
   25.66 +            Create a ZODB (editable) equivalent of this object.
   25.67 +        """
   25.68 +        raise NotImplementedError, "See next week's model."
   25.69 +
   25.70 +    def _readFile( self, reparse ):
   25.71 +
   25.72 +        fp = expandpath( self._filepath )
   25.73 +        file = open( fp, 'r' )  # not binary, we want CRLF munging here.
   25.74 +
   25.75 +        try:
   25.76 +            data = file.read()
   25.77 +        finally:
   25.78 +            file.close()
   25.79 +
   25.80 +        self.raw = data
   25.81 +
   25.82 +        if reparse:
   25.83 +            self.cook()
   25.84 +
   25.85 +    #
   25.86 +    #   "Wesleyan" interface (we need to be "methodish").
   25.87 +    #
   25.88 +    class func_code:
   25.89 +        pass
   25.90 +
   25.91 +    func_code=func_code()
   25.92 +    func_code.co_varnames= ()
   25.93 +    func_code.co_argcount=0
   25.94 +    func_code.__roles__=()
   25.95 +
   25.96 +    func_defaults__roles__=()
   25.97 +    func_defaults=()
   25.98 +
   25.99 +    index_html = None   # No accidental acquisition
  25.100 +
  25.101 +    default_content_type = 'text/html'
  25.102 +
  25.103 +    def cook( self ):
  25.104 +        if not hasattr( self, '_v_cooked' ):
  25.105 +            self._v_cooked = HTML(self.raw, level=1, header=0)
  25.106 +        return self._v_cooked
  25.107 +
  25.108 +    _default_template = Globals.HTML( """\
  25.109 +<dtml-var standard_html_header>
  25.110 +<div class="Desktop">
  25.111 +<dtml-var cooked>
  25.112 +</div>
  25.113 +<dtml-var standard_html_footer>""" )
  25.114 +
  25.115 +    def __call__( self, REQUEST={}, RESPONSE=None, **kw ):
  25.116 +        """
  25.117 +            Return our rendered StructuredText.
  25.118 +        """
  25.119 +        self._updateFromFS()
  25.120 +
  25.121 +        if RESPONSE is not None:
  25.122 +            RESPONSE.setHeader( 'Content-Type', 'text/html' )
  25.123 +        return self._render(REQUEST, RESPONSE, **kw)
  25.124 +
  25.125 +    security.declarePrivate( '_render' )
  25.126 +    def _render( self, REQUEST={}, RESPONSE=None, **kw ):
  25.127 +        """
  25.128 +            Find the appropriate rendering template and use it to
  25.129 +            render us.
  25.130 +        """
  25.131 +        template = getattr( self, 'stxmethod_view', self._default_template )
  25.132 +
  25.133 +        if getattr( template, 'isDocTemp', 0 ):
  25.134 +            posargs = ( self, REQUEST, RESPONSE )
  25.135 +        else:
  25.136 +            posargs = ()
  25.137 +
  25.138 +        return template(*posargs, **{ 'cooked' : self.cook() } )
  25.139 +
  25.140 +    security.declareProtected( FTPAccess, 'manage_FTPget' )
  25.141 +    def manage_FTPget( self ):
  25.142 +        """
  25.143 +            Fetch our source for delivery via FTP.
  25.144 +        """
  25.145 +        return self.raw
  25.146 +
  25.147 +    security.declareProtected( ViewManagementScreens, 'PrincipiaSearchSource' )
  25.148 +    def PrincipiaSearchSource( self ):
  25.149 +        """
  25.150 +            Fetch our source for indexing in a catalog.
  25.151 +        """
  25.152 +        return self.raw
  25.153 +
  25.154 +    security.declareProtected( ViewManagementScreens, 'document_src' )
  25.155 +    def document_src( self ):
  25.156 +        """
  25.157 +            Fetch our source for indexing in a catalog.
  25.158 +        """
  25.159 +        return self.raw
  25.160 +
  25.161 +Globals.InitializeClass( FSSTXMethod )
  25.162 +
  25.163 +registerFileExtension( 'stx', FSSTXMethod )
  25.164 +registerMetaType( 'STX Method', FSSTXMethod )
    26.1 new file mode 100644
    26.2 --- /dev/null
    26.3 +++ b/FSZSQLMethod.py
    26.4 @@ -0,0 +1,149 @@
    26.5 +##############################################################################
    26.6 +#
    26.7 +# Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved.
    26.8 +#
    26.9 +# This software is subject to the provisions of the Zope Public License,
   26.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   26.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   26.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   26.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   26.14 +# FOR A PARTICULAR PURPOSE.
   26.15 +#
   26.16 +##############################################################################
   26.17 +""" Customizable ZSQL methods that come from the filesystem.
   26.18 +
   26.19 +$Id$
   26.20 +"""
   26.21 +
   26.22 +import logging
   26.23 +import Globals
   26.24 +from AccessControl import ClassSecurityInfo
   26.25 +from Acquisition import ImplicitAcquisitionWrapper
   26.26 +from Products.ZSQLMethods.SQL import SQL
   26.27 +
   26.28 +from DirectoryView import registerFileExtension
   26.29 +from DirectoryView import registerMetaType
   26.30 +from FSObject import FSObject
   26.31 +from permissions import View
   26.32 +from permissions import ViewManagementScreens
   26.33 +from utils import _dtmldir
   26.34 +from utils import expandpath
   26.35 +
   26.36 +
   26.37 +logger = logging.getLogger('CMFCore.FSZSQLMethod')
   26.38 +
   26.39 +
   26.40 +class FSZSQLMethod(SQL, FSObject):
   26.41 +    """FSZSQLMethods act like Z SQL Methods but are not directly
   26.42 +    modifiable from the management interface."""
   26.43 +
   26.44 +    meta_type = 'Filesystem Z SQL Method'
   26.45 +
   26.46 +    manage_options=(
   26.47 +        (
   26.48 +            {'label':'Customize', 'action':'manage_customise'},
   26.49 +            {'label':'Test', 'action':'manage_testForm',
   26.50 +             'help':('ZSQLMethods','Z-SQL-Method_Test.stx')},
   26.51 +            )
   26.52 +        )
   26.53 +
   26.54 +    # Use declarative security
   26.55 +    security = ClassSecurityInfo()
   26.56 +
   26.57 +    security.declareObjectProtected(View)
   26.58 +
   26.59 +    # Make mutators private
   26.60 +    security.declarePrivate('manage_main','manage_edit','manage_advanced','manage_advancedForm')
   26.61 +    manage=None
   26.62 +
   26.63 +    security.declareProtected(ViewManagementScreens, 'manage_customise')
   26.64 +    manage_customise = Globals.DTMLFile('custzsql', _dtmldir)
   26.65 +
   26.66 +    def __init__(self, id, filepath, fullname=None, properties=None):
   26.67 +        FSObject.__init__(self, id, filepath, fullname, properties)
   26.68 +
   26.69 +    def _createZODBClone(self):
   26.70 +        """Create a ZODB (editable) equivalent of this object."""
   26.71 +        # I guess it's bad to 'reach inside' ourselves like this,
   26.72 +        # but Z SQL Methods don't have accessor methdods ;-)
   26.73 +        s = SQL(self.id,
   26.74 +                self.title,
   26.75 +                self.connection_id,
   26.76 +                self.arguments_src,
   26.77 +                self.src)
   26.78 +        s.manage_advanced(self.max_rows_,
   26.79 +                          self.max_cache_,
   26.80 +                          self.cache_time_,
   26.81 +                          self.class_name_,
   26.82 +                          self.class_file_,
   26.83 +                          connection_hook=self.connection_hook,
   26.84 +                          direct=self.allow_simple_one_argument_traversal)
   26.85 +        return s
   26.86 +
   26.87 +    def _readFile(self, reparse):
   26.88 +        fp = expandpath(self._filepath)
   26.89 +        file = open(fp, 'r')    # not 'rb', as this is a text file!
   26.90 +        try:
   26.91 +            data = file.read()
   26.92 +        finally: file.close()
   26.93 +
   26.94 +        # parse parameters
   26.95 +        parameters={}
   26.96 +        start = data.find('<dtml-comment>')
   26.97 +        end   = data.find('</dtml-comment>')
   26.98 +        if start==-1 or end==-1 or start>end:
   26.99 +            raise ValueError,'Could not find parameter block'
  26.100 +        block = data[start+14:end]
  26.101 +
  26.102 +        for line in block.split('\n'):
  26.103 +            pair = line.split(':',1)
  26.104 +            if len(pair)!=2:
  26.105 +                continue
  26.106 +            parameters[pair[0].strip().lower()]=pair[1].strip()
  26.107 +        
  26.108 +        # check for required parameters
  26.109 +        try:
  26.110 +            connection_id =   ( parameters.get('connection id', '') or
  26.111 +                                parameters['connection_id'] )
  26.112 +        except KeyError,e:
  26.113 +            raise ValueError,"The '%s' parameter is required but was not supplied" % e
  26.114 +
  26.115 +        # Optional parameters
  26.116 +        title =           parameters.get('title','')
  26.117 +        arguments =       parameters.get('arguments','')
  26.118 +        max_rows =        parameters.get('max_rows',1000)
  26.119 +        max_cache =       parameters.get('max_cache',100)
  26.120 +        cache_time =      parameters.get('cache_time',0)
  26.121 +        class_name =      parameters.get('class_name','')
  26.122 +        class_file =      parameters.get('class_file','')
  26.123 +        connection_hook = parameters.get('connection_hook',None)
  26.124 +        direct = parameters.get('allow_simple_one_argument_traversal', None)
  26.125 +
  26.126 +        self.manage_edit(title, connection_id, arguments, template=data)
  26.127 +
  26.128 +        self.manage_advanced(max_rows,
  26.129 +                             max_cache,
  26.130 +                             cache_time,
  26.131 +                             class_name,
  26.132 +                             class_file,
  26.133 +                             connection_hook=connection_hook,
  26.134 +                             direct=direct)
  26.135 +
  26.136 +        # do we need to do anything on reparse?
  26.137 +
  26.138 +
  26.139 +    if Globals.DevelopmentMode:
  26.140 +        # Provide an opportunity to update the properties.
  26.141 +        def __of__(self, parent):
  26.142 +            try:
  26.143 +                self = ImplicitAcquisitionWrapper(self, parent)
  26.144 +                self._updateFromFS()
  26.145 +                return self
  26.146 +            except:
  26.147 +                logger.error("Error during __of__", exc_info=True)
  26.148 +                raise
  26.149 +
  26.150 +Globals.InitializeClass(FSZSQLMethod)
  26.151 +
  26.152 +registerFileExtension('zsql', FSZSQLMethod)
  26.153 +registerMetaType('Z SQL Method', FSZSQLMethod)
    27.1 new file mode 100644
    27.2 --- /dev/null
    27.3 +++ b/MemberDataTool.py
    27.4 @@ -0,0 +1,420 @@
    27.5 +##############################################################################
    27.6 +#
    27.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    27.8 +#
    27.9 +# This software is subject to the provisions of the Zope Public License,
   27.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   27.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   27.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   27.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   27.14 +# FOR A PARTICULAR PURPOSE.
   27.15 +#
   27.16 +##############################################################################
   27.17 +""" Basic member data tool.
   27.18 +
   27.19 +$Id$
   27.20 +"""
   27.21 +
   27.22 +from AccessControl import ClassSecurityInfo
   27.23 +from Acquisition import aq_inner, aq_parent, aq_base
   27.24 +from BTrees.OOBTree import OOBTree
   27.25 +from Globals import DTMLFile
   27.26 +from Globals import InitializeClass
   27.27 +from OFS.PropertyManager import PropertyManager
   27.28 +from OFS.SimpleItem import SimpleItem
   27.29 +from ZPublisher.Converters import type_converters
   27.30 +
   27.31 +from ActionProviderBase import ActionProviderBase
   27.32 +from exceptions import BadRequest
   27.33 +from interfaces.portal_memberdata import MemberData as IMemberData
   27.34 +from interfaces.portal_memberdata import portal_memberdata as IMemberDataTool
   27.35 +from permissions import ManagePortal
   27.36 +from permissions import SetOwnProperties
   27.37 +from permissions import ViewManagementScreens
   27.38 +from utils import _dtmldir
   27.39 +from utils import getToolByName
   27.40 +from utils import UniqueObject
   27.41 +
   27.42 +
   27.43 +_marker = []  # Create a new marker object.
   27.44 +
   27.45 +
   27.46 +class MemberDataTool (UniqueObject, SimpleItem, PropertyManager, ActionProviderBase):
   27.47 +    """ This tool wraps user objects, making them act as Member objects.
   27.48 +    """
   27.49 +
   27.50 +    __implements__ = (IMemberDataTool, ActionProviderBase.__implements__)
   27.51 +
   27.52 +    id = 'portal_memberdata'
   27.53 +    meta_type = 'CMF Member Data Tool'
   27.54 +    _actions = ()
   27.55 +
   27.56 +    _v_temps = None
   27.57 +    _properties = ()
   27.58 +
   27.59 +    security = ClassSecurityInfo()
   27.60 +
   27.61 +    manage_options=( ActionProviderBase.manage_options +
   27.62 +                     ({ 'label' : 'Overview'
   27.63 +                       , 'action' : 'manage_overview'
   27.64 +                       }
   27.65 +                     , { 'label' : 'Contents'
   27.66 +                       , 'action' : 'manage_showContents'
   27.67 +                       }
   27.68 +                     )
   27.69 +                   + PropertyManager.manage_options
   27.70 +                   + SimpleItem.manage_options
   27.71 +                   )
   27.72 +
   27.73 +    #
   27.74 +    #   ZMI methods
   27.75 +    #
   27.76 +    security.declareProtected(ManagePortal, 'manage_overview')
   27.77 +    manage_overview = DTMLFile( 'explainMemberDataTool', _dtmldir )
   27.78 +
   27.79 +    security.declareProtected(ViewManagementScreens, 'manage_showContents')
   27.80 +    manage_showContents = DTMLFile('memberdataContents', _dtmldir )
   27.81 +
   27.82 +
   27.83 +    def __init__(self):
   27.84 +        self._members = OOBTree()
   27.85 +        # Create the default properties.
   27.86 +        self._setProperty('email', '', 'string')
   27.87 +        self._setProperty('portal_skin', '', 'string')
   27.88 +        self._setProperty('listed', '', 'boolean')
   27.89 +        self._setProperty('login_time', '2000/01/01', 'date')
   27.90 +        self._setProperty('last_login_time', '2000/01/01', 'date')
   27.91 +
   27.92 +    #
   27.93 +    #   'portal_memberdata' interface methods
   27.94 +    #
   27.95 +    security.declarePrivate('getMemberDataContents')
   27.96 +    def getMemberDataContents(self):
   27.97 +        '''
   27.98 +        Return the number of members stored in the _members
   27.99 +        BTree and some other useful info
  27.100 +        '''
  27.101 +        membertool   = getToolByName(self, 'portal_membership')
  27.102 +        members      = self._members
  27.103 +        user_list    = membertool.listMemberIds()
  27.104 +        member_list  = members.keys()
  27.105 +        member_count = len(members)
  27.106 +        orphan_count = 0
  27.107 +
  27.108 +        for member in member_list:
  27.109 +            if member not in user_list:
  27.110 +                orphan_count = orphan_count + 1
  27.111 +
  27.112 +        return [{ 'member_count' : member_count,
  27.113 +                  'orphan_count' : orphan_count }]
  27.114 +
  27.115 +    security.declarePrivate('searchMemberData')
  27.116 +    def searchMemberData(self, search_param, search_term, attributes=()):
  27.117 +        """ Search members. """
  27.118 +        res = []
  27.119 +
  27.120 +        if not search_param:
  27.121 +            return res
  27.122 +
  27.123 +        membership = getToolByName(self, 'portal_membership')
  27.124 +
  27.125 +        if len(attributes) == 0:
  27.126 +            attributes = ('id', 'email')
  27.127 +
  27.128 +        if search_param == 'username':
  27.129 +            search_param = 'id'
  27.130 +
  27.131 +        for user_id in self._members.keys():
  27.132 +            u = membership.getMemberById(user_id)
  27.133 +
  27.134 +            if u is not None:
  27.135 +                memberProperty = u.getProperty
  27.136 +                searched = memberProperty(search_param, None)
  27.137 +
  27.138 +                if searched is not None and searched.find(search_term) != -1:
  27.139 +                    user_data = {}
  27.140 +
  27.141 +                    for desired in attributes:
  27.142 +                        if desired == 'id':
  27.143 +                            user_data['username'] = memberProperty(desired, '')
  27.144 +                        else:
  27.145 +                            user_data[desired] = memberProperty(desired, '')
  27.146 +
  27.147 +                    res.append(user_data)
  27.148 +
  27.149 +        return res
  27.150 +
  27.151 +    security.declarePrivate( 'searchMemberDataContents' )
  27.152 +    def searchMemberDataContents( self, search_param, search_term ):
  27.153 +        """ Search members. This method will be deprecated soon. """
  27.154 +        res = []
  27.155 +
  27.156 +        if search_param == 'username':
  27.157 +            search_param = 'id'
  27.158 +
  27.159 +        mtool   = getToolByName(self, 'portal_membership')
  27.160 +
  27.161 +        for member_id in self._members.keys():
  27.162 +
  27.163 +            user_wrapper = mtool.getMemberById( member_id )
  27.164 +
  27.165 +            if user_wrapper is not None:
  27.166 +                memberProperty = user_wrapper.getProperty
  27.167 +                searched = memberProperty( search_param, None )
  27.168 +
  27.169 +                if searched is not None and searched.find(search_term) != -1:
  27.170 +
  27.171 +                    res.append( { 'username': memberProperty( 'id' )
  27.172 +                                , 'email' : memberProperty( 'email', '' )
  27.173 +                                }
  27.174 +                            )
  27.175 +        return res
  27.176 +
  27.177 +    security.declarePrivate('pruneMemberDataContents')
  27.178 +    def pruneMemberDataContents(self):
  27.179 +        """ Delete data contents of all members not listet in acl_users.
  27.180 +        """
  27.181 +        membertool= getToolByName(self, 'portal_membership')
  27.182 +        members = self._members
  27.183 +        user_list = membertool.listMemberIds()
  27.184 +
  27.185 +        for member_id in list(members.keys()):
  27.186 +            if member_id not in user_list:
  27.187 +                del members[member_id]
  27.188 +
  27.189 +    security.declarePrivate('wrapUser')
  27.190 +    def wrapUser(self, u):
  27.191 +        '''
  27.192 +        If possible, returns the Member object that corresponds
  27.193 +        to the given User object.
  27.194 +        '''
  27.195 +        id = u.getId()
  27.196 +        members = self._members
  27.197 +        if not members.has_key(id):
  27.198 +            # Get a temporary member that might be
  27.199 +            # registered later via registerMemberData().
  27.200 +            temps = self._v_temps
  27.201 +            if temps is not None and temps.has_key(id):
  27.202 +                m = temps[id]
  27.203 +            else:
  27.204 +                base = aq_base(self)
  27.205 +                m = MemberData(base, id)
  27.206 +                if temps is None:
  27.207 +                    self._v_temps = {id:m}
  27.208 +                    if hasattr(self, 'REQUEST'):
  27.209 +                        # No REQUEST during tests.
  27.210 +                        self.REQUEST._hold(CleanupTemp(self))
  27.211 +                else:
  27.212 +                    temps[id] = m
  27.213 +        else:
  27.214 +            m = members[id]
  27.215 +        # Return a wrapper with self as containment and
  27.216 +        # the user as context.
  27.217 +        return m.__of__(self).__of__(u)
  27.218 +
  27.219 +    security.declarePrivate('registerMemberData')
  27.220 +    def registerMemberData(self, m, id):
  27.221 +        """ Add the given member data to the _members btree.
  27.222 +        """
  27.223 +        self._members[id] = aq_base(m)
  27.224 +
  27.225 +    security.declarePrivate('deleteMemberData')
  27.226 +    def deleteMemberData(self, member_id):
  27.227 +        """ Delete member data of specified member.
  27.228 +        """
  27.229 +        members = self._members
  27.230 +        if members.has_key(member_id):
  27.231 +            del members[member_id]
  27.232 +            return 1
  27.233 +        else:
  27.234 +            return 0
  27.235 +
  27.236 +InitializeClass(MemberDataTool)
  27.237 +
  27.238 +
  27.239 +class CleanupTemp:
  27.240 +    """Used to cleanup _v_temps at the end of the request."""
  27.241 +    def __init__(self, tool):
  27.242 +        self._tool = tool
  27.243 +    def __del__(self):
  27.244 +        try:
  27.245 +            del self._tool._v_temps
  27.246 +        except (AttributeError, KeyError):
  27.247 +            # The object has already been deactivated.
  27.248 +            pass
  27.249 +
  27.250 +
  27.251 +class MemberData (SimpleItem):
  27.252 +
  27.253 +    __implements__ = IMemberData
  27.254 +
  27.255 +    security = ClassSecurityInfo()
  27.256 +
  27.257 +    def __init__(self, tool, id):
  27.258 +        self.id = id
  27.259 +        # Make a temporary reference to the tool.
  27.260 +        # The reference will be removed by notifyModified().
  27.261 +        self._tool = tool
  27.262 +
  27.263 +    security.declarePrivate('notifyModified')
  27.264 +    def notifyModified(self):
  27.265 +        # Links self to parent for full persistence.
  27.266 +        tool = getattr(self, '_tool', None)
  27.267 +        if tool is not None:
  27.268 +            del self._tool
  27.269 +            tool.registerMemberData(self, self.getId())
  27.270 +
  27.271 +    security.declarePublic('getUser')
  27.272 +    def getUser(self):
  27.273 +        # The user object is our context, but it's possible for
  27.274 +        # restricted code to strip context while retaining
  27.275 +        # containment.  Therefore we need a simple security check.
  27.276 +        parent = aq_parent(self)
  27.277 +        bcontext = aq_base(parent)
  27.278 +        bcontainer = aq_base(aq_parent(aq_inner(self)))
  27.279 +        if bcontext is bcontainer or not hasattr(bcontext, 'getUserName'):
  27.280 +            raise 'MemberDataError', "Can't find user data"
  27.281 +        # Return the user object, which is our context.
  27.282 +        return parent
  27.283 +
  27.284 +    def getTool(self):
  27.285 +        return aq_parent(aq_inner(self))
  27.286 +
  27.287 +    security.declarePublic('getMemberId')
  27.288 +    def getMemberId(self):
  27.289 +        return self.getUser().getId()
  27.290 +
  27.291 +    security.declareProtected(SetOwnProperties, 'setProperties')
  27.292 +    def setProperties(self, properties=None, **kw):
  27.293 +        '''Allows the authenticated member to set his/her own properties.
  27.294 +        Accepts either keyword arguments or a mapping for the "properties"
  27.295 +        argument.
  27.296 +        '''
  27.297 +        if properties is None:
  27.298 +            properties = kw
  27.299 +        membership = getToolByName(self, 'portal_membership')
  27.300 +        registration = getToolByName(self, 'portal_registration', None)
  27.301 +        if not membership.isAnonymousUser():
  27.302 +            member = membership.getAuthenticatedMember()
  27.303 +            if registration:
  27.304 +                failMessage = registration.testPropertiesValidity(properties, member)
  27.305 +                if failMessage is not None:
  27.306 +                    raise BadRequest(failMessage)
  27.307 +            member.setMemberProperties(properties)
  27.308 +        else:
  27.309 +            raise BadRequest('Not logged in.')
  27.310 +
  27.311 +    security.declarePrivate('setMemberProperties')
  27.312 +    def setMemberProperties(self, mapping):
  27.313 +        '''Sets the properties of the member.
  27.314 +        '''
  27.315 +        # Sets the properties given in the MemberDataTool.
  27.316 +        tool = self.getTool()
  27.317 +        for id in tool.propertyIds():
  27.318 +            if mapping.has_key(id):
  27.319 +                if not self.__class__.__dict__.has_key(id):
  27.320 +                    value = mapping[id]
  27.321 +                    if type(value)==type(''):
  27.322 +                        proptype = tool.getPropertyType(id) or 'string'
  27.323 +                        if type_converters.has_key(proptype):
  27.324 +                            value = type_converters[proptype](value)
  27.325 +                    setattr(self, id, value)
  27.326 +        # Hopefully we can later make notifyModified() implicit.
  27.327 +        self.notifyModified()
  27.328 +
  27.329 +    # XXX: s.b., getPropertyForMember(member, id, default)?
  27.330 +
  27.331 +    security.declarePublic('getProperty')
  27.332 +    def getProperty(self, id, default=_marker):
  27.333 +
  27.334 +        tool = self.getTool()
  27.335 +        base = aq_base( self )
  27.336 +
  27.337 +        # First, check the wrapper (w/o acquisition).
  27.338 +        value = getattr( base, id, _marker )
  27.339 +        if value is not _marker:
  27.340 +            return value
  27.341 +
  27.342 +        # Then, check the tool and the user object for a value.
  27.343 +        tool_value = tool.getProperty( id, _marker )
  27.344 +        user_value = getattr( self.getUser(), id, _marker )
  27.345 +
  27.346 +        # If the tool doesn't have the property, use user_value or default
  27.347 +        if tool_value is _marker:
  27.348 +            if user_value is not _marker:
  27.349 +                return user_value
  27.350 +            elif default is not _marker:
  27.351 +                return default
  27.352 +            else:
  27.353 +                raise ValueError, 'The property %s does not exist' % id
  27.354 +
  27.355 +        # If the tool has an empty property and we have a user_value, use it
  27.356 +        if not tool_value and user_value is not _marker:
  27.357 +            return user_value
  27.358 +
  27.359 +        # Otherwise return the tool value
  27.360 +        return tool_value
  27.361 +
  27.362 +    security.declarePrivate('getPassword')
  27.363 +    def getPassword(self):
  27.364 +        """Return the password of the user."""
  27.365 +        return self.getUser()._getPassword()
  27.366 +
  27.367 +    security.declarePrivate('setSecurityProfile')
  27.368 +    def setSecurityProfile(self, password=None, roles=None, domains=None):
  27.369 +        """Set the user's basic security profile"""
  27.370 +        u = self.getUser()
  27.371 +
  27.372 +        # The Zope User API is stupid, it should check for None.
  27.373 +        if roles is None:
  27.374 +            roles = list(u.getRoles())
  27.375 +            if 'Authenticated' in roles:
  27.376 +                roles.remove('Authenticated')
  27.377 +        if domains is None:
  27.378 +            domains = u.getDomains()
  27.379 +
  27.380 +        u.userFolderEditUser(u.getUserName(), password, roles, domains)
  27.381 +
  27.382 +    def __str__(self):
  27.383 +        return self.getMemberId()
  27.384 +
  27.385 +    ### User object interface ###
  27.386 +
  27.387 +    security.declarePublic('getUserName')
  27.388 +    def getUserName(self):
  27.389 +        """Return the username of a user"""
  27.390 +        return self.getUser().getUserName()
  27.391 +
  27.392 +    security.declarePublic('getId')
  27.393 +    def getId(self):
  27.394 +        """Get the ID of the user. The ID can be used, at least from
  27.395 +        Python, to get the user from the user's
  27.396 +        UserDatabase"""
  27.397 +        return self.getUser().getId()
  27.398 +
  27.399 +    security.declarePublic('getRoles')
  27.400 +    def getRoles(self):
  27.401 +        """Return the list of roles assigned to a user."""
  27.402 +        return self.getUser().getRoles()
  27.403 +
  27.404 +    security.declarePublic('getRolesInContext')
  27.405 +    def getRolesInContext(self, object):
  27.406 +        """Return the list of roles assigned to the user,
  27.407 +           including local roles assigned in context of
  27.408 +           the passed in object."""
  27.409 +        return self.getUser().getRolesInContext(object)
  27.410 +
  27.411 +    security.declarePublic('getDomains')
  27.412 +    def getDomains(self):
  27.413 +        """Return the list of domain restrictions for a user"""
  27.414 +        return self.getUser().getDomains()
  27.415 +
  27.416 +    security.declarePublic('has_role')
  27.417 +    def has_role(self, roles, object=None):
  27.418 +        """Check to see if a user has a given role or roles."""
  27.419 +        return self.getUser().has_role(roles, object)
  27.420 +
  27.421 +    # There are other parts of the interface but they are
  27.422 +    # deprecated for use with CMF applications.
  27.423 +
  27.424 +InitializeClass(MemberData)
    28.1 new file mode 100644
    28.2 --- /dev/null
    28.3 +++ b/MembershipTool.py
    28.4 @@ -0,0 +1,531 @@
    28.5 +##############################################################################
    28.6 +#
    28.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    28.8 +#
    28.9 +# This software is subject to the provisions of the Zope Public License,
   28.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   28.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   28.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   28.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   28.14 +# FOR A PARTICULAR PURPOSE.
   28.15 +#
   28.16 +##############################################################################
   28.17 +""" Basic membership tool.
   28.18 +
   28.19 +$Id$
   28.20 +"""
   28.21 +
   28.22 +import logging
   28.23 +from AccessControl import ClassSecurityInfo
   28.24 +from AccessControl.User import nobody
   28.25 +from Acquisition import aq_base
   28.26 +from Acquisition import aq_inner
   28.27 +from Acquisition import aq_parent
   28.28 +from Globals import DTMLFile
   28.29 +from Globals import InitializeClass
   28.30 +from Globals import MessageDialog
   28.31 +from Globals import PersistentMapping
   28.32 +from OFS.Folder import Folder
   28.33 +from ZODB.POSException import ConflictError
   28.34 +
   28.35 +from ActionProviderBase import ActionProviderBase
   28.36 +from exceptions import AccessControl_Unauthorized
   28.37 +from exceptions import BadRequest
   28.38 +from interfaces.portal_membership \
   28.39 +        import portal_membership as IMembershipTool
   28.40 +from permissions import AccessContentsInformation
   28.41 +from permissions import ChangeLocalRoles
   28.42 +from permissions import ListPortalMembers
   28.43 +from permissions import ManagePortal
   28.44 +from permissions import ManageUsers
   28.45 +from permissions import SetOwnPassword
   28.46 +from permissions import View
   28.47 +from utils import _checkPermission
   28.48 +from utils import _dtmldir
   28.49 +from utils import _getAuthenticatedUser
   28.50 +from utils import getToolByName
   28.51 +from utils import UniqueObject
   28.52 +
   28.53 +
   28.54 +logger = logging.getLogger('CMFCore.MembershipTool')
   28.55 +
   28.56 +
   28.57 +class MembershipTool(UniqueObject, Folder, ActionProviderBase):
   28.58 +    """ This tool accesses member data through an acl_users object.
   28.59 +
   28.60 +    It can be replaced with something that accesses member data in a
   28.61 +    different way.
   28.62 +    """
   28.63 +
   28.64 +    __implements__ = (IMembershipTool, ActionProviderBase.__implements__)
   28.65 +
   28.66 +    id = 'portal_membership'
   28.67 +    meta_type = 'CMF Membership Tool'
   28.68 +    _actions = ()
   28.69 +
   28.70 +    memberareaCreationFlag = 1
   28.71 +
   28.72 +    security = ClassSecurityInfo()
   28.73 +
   28.74 +    manage_options=( ({ 'label' : 'Configuration'
   28.75 +                     , 'action' : 'manage_mapRoles'
   28.76 +                     },) +
   28.77 +                     ActionProviderBase.manage_options +
   28.78 +                   ( { 'label' : 'Overview'
   28.79 +                     , 'action' : 'manage_overview'
   28.80 +                     },
   28.81 +                   ) + Folder.manage_options)
   28.82 +
   28.83 +    #
   28.84 +    #   ZMI methods
   28.85 +    #
   28.86 +    security.declareProtected(ManagePortal, 'manage_overview')
   28.87 +    manage_overview = DTMLFile( 'explainMembershipTool', _dtmldir )
   28.88 +
   28.89 +    #
   28.90 +    #   'portal_membership' interface methods
   28.91 +    #
   28.92 +    security.declareProtected(ManagePortal, 'manage_mapRoles')
   28.93 +    manage_mapRoles = DTMLFile('membershipRolemapping', _dtmldir )
   28.94 +
   28.95 +    security.declareProtected(SetOwnPassword, 'setPassword')
   28.96 +    def setPassword(self, password, domains=None):
   28.97 +        '''Allows the authenticated member to set his/her own password.
   28.98 +        '''
   28.99 +        registration = getToolByName(self, 'portal_registration', None)
  28.100 +        if not self.isAnonymousUser():
  28.101 +            member = self.getAuthenticatedMember()
  28.102 +            if registration:
  28.103 +                failMessage = registration.testPasswordValidity(password)
  28.104 +                if failMessage is not None:
  28.105 +                    raise BadRequest(failMessage)
  28.106 +            member.setSecurityProfile(password=password, domains=domains)
  28.107 +        else:
  28.108 +            raise BadRequest('Not logged in.')
  28.109 +
  28.110 +    security.declarePublic('getAuthenticatedMember')
  28.111 +    def getAuthenticatedMember(self):
  28.112 +        '''
  28.113 +        Returns the currently authenticated member object
  28.114 +        or the Anonymous User.  Never returns None.
  28.115 +        '''
  28.116 +        u = _getAuthenticatedUser(self)
  28.117 +        if u is None:
  28.118 +            u = nobody
  28.119 +        return self.wrapUser(u)
  28.120 +
  28.121 +    security.declarePrivate('wrapUser')
  28.122 +    def wrapUser(self, u, wrap_anon=0):
  28.123 +        """ Set up the correct acquisition wrappers for a user object.
  28.124 +
  28.125 +        Provides an opportunity for a portal_memberdata tool to retrieve and
  28.126 +        store member data independently of the user object.
  28.127 +        """
  28.128 +        b = getattr(u, 'aq_base', None)
  28.129 +        if b is None:
  28.130 +            # u isn't wrapped at all.  Wrap it in self.acl_users.
  28.131 +            b = u
  28.132 +            u = u.__of__(self.acl_users)
  28.133 +        if (b is nobody and not wrap_anon) or hasattr(b, 'getMemberId'):
  28.134 +            # This user is either not recognized by acl_users or it is
  28.135 +            # already registered with something that implements the
  28.136 +            # member data tool at least partially.
  28.137 +            return u
  28.138 +
  28.139 +        # Apply any role mapping if we have it
  28.140 +        if hasattr(self, 'role_map'):
  28.141 +            for portal_role in self.role_map.keys():
  28.142 +                if (self.role_map.get(portal_role) in u.roles and
  28.143 +                        portal_role not in u.roles):
  28.144 +                    u.roles.append(portal_role)
  28.145 +
  28.146 +        mdtool = getToolByName(self, 'portal_memberdata', None)
  28.147 +        if mdtool is not None:
  28.148 +            try:
  28.149 +                u = mdtool.wrapUser(u)
  28.150 +            except ConflictError:
  28.151 +                raise
  28.152 +            except:
  28.153 +                logger.error("Error during wrapUser", exc_info=True)
  28.154 +        return u
  28.155 +
  28.156 +    security.declareProtected(ManagePortal, 'getPortalRoles')
  28.157 +    def getPortalRoles(self):
  28.158 +        """
  28.159 +        Return all local roles defined by the portal itself,
  28.160 +        which means roles that are useful and understood
  28.161 +        by the portal object
  28.162 +        """
  28.163 +        parent = self.aq_inner.aq_parent
  28.164 +        roles = list( parent.userdefined_roles() )
  28.165 +
  28.166 +        # This is *not* a local role in the portal but used by it
  28.167 +        roles.append('Manager')
  28.168 +        roles.append('Owner')
  28.169 +
  28.170 +        return roles
  28.171 +
  28.172 +    security.declareProtected(ManagePortal, 'setRoleMapping')
  28.173 +    def setRoleMapping(self, portal_role, userfolder_role):
  28.174 +        """
  28.175 +        set the mapping of roles between roles understood by
  28.176 +        the portal and roles coming from outside user sources
  28.177 +        """
  28.178 +        if not hasattr(self, 'role_map'): self.role_map = PersistentMapping()
  28.179 +
  28.180 +        if len(userfolder_role) < 1:
  28.181 +            del self.role_map[portal_role]
  28.182 +        else:
  28.183 +            self.role_map[portal_role] = userfolder_role
  28.184 +
  28.185 +        return MessageDialog(
  28.186 +               title  ='Mapping updated',
  28.187 +               message='The Role mappings have been updated',
  28.188 +               action ='manage_mapRoles')
  28.189 +
  28.190 +    security.declareProtected(ManagePortal, 'getMappedRole')
  28.191 +    def getMappedRole(self, portal_role):
  28.192 +        """
  28.193 +        returns a role name if the portal role is mapped to
  28.194 +        something else or an empty string if it is not
  28.195 +        """
  28.196 +        if hasattr(self, 'role_map'):
  28.197 +            return self.role_map.get(portal_role, '')
  28.198 +        else:
  28.199 +            return ''
  28.200 +
  28.201 +    security.declarePublic('getMembersFolder')
  28.202 +    def getMembersFolder(self):
  28.203 +        """ Get the members folder object.
  28.204 +        """
  28.205 +        parent = aq_parent( aq_inner(self) )
  28.206 +        members = getattr(parent, 'Members', None)
  28.207 +        return members
  28.208 +
  28.209 +    security.declareProtected(ManagePortal, 'getMemberareaCreationFlag')
  28.210 +    def getMemberareaCreationFlag(self):
  28.211 +        """
  28.212 +        Returns the flag indicating whether the membership tool
  28.213 +        will create a member area if an authenticated user from
  28.214 +        an underlying user folder logs in first without going
  28.215 +        through the join process
  28.216 +        """
  28.217 +        return self.memberareaCreationFlag
  28.218 +
  28.219 +    security.declareProtected(ManagePortal, 'setMemberareaCreationFlag')
  28.220 +    def setMemberareaCreationFlag(self):
  28.221 +        """
  28.222 +        sets the flag indicating whether the membership tool
  28.223 +        will create a member area if an authenticated user from
  28.224 +        an underlying user folder logs in first without going
  28.225 +        through the join process
  28.226 +        """
  28.227 +        if not hasattr(self, 'memberareaCreationFlag'):
  28.228 +            self.memberareaCreationFlag = 0
  28.229 +
  28.230 +        if self.memberareaCreationFlag == 0:
  28.231 +            self.memberareaCreationFlag = 1
  28.232 +        else:
  28.233 +            self.memberareaCreationFlag = 0
  28.234 +
  28.235 +        return MessageDialog(
  28.236 +               title  ='Member area creation flag changed',
  28.237 +               message='Member area creation flag has been updated',
  28.238 +               action ='manage_mapRoles')
  28.239 +
  28.240 +    security.declarePublic('createMemberArea')
  28.241 +    def createMemberArea(self, member_id=''):
  28.242 +        """ Create a member area for 'member_id' or authenticated user.
  28.243 +        """
  28.244 +        if not self.getMemberareaCreationFlag():
  28.245 +            return None
  28.246 +        members = self.getMembersFolder()
  28.247 +        if not members:
  28.248 +            return None
  28.249 +        if self.isAnonymousUser():
  28.250 +            return None
  28.251 +        # Note: We can't use getAuthenticatedMember() and getMemberById()
  28.252 +        # because they might be wrapped by MemberDataTool.
  28.253 +        user = _getAuthenticatedUser(self)
  28.254 +        user_id = user.getId()
  28.255 +        if member_id in ('', user_id):
  28.256 +            member = user
  28.257 +            member_id = user_id
  28.258 +        else:
  28.259 +            if _checkPermission(ManageUsers, self):
  28.260 +                member = self.acl_users.getUserById(member_id, None)
  28.261 +                if member:
  28.262 +                    member = member.__of__(self.acl_users)
  28.263 +                else:
  28.264 +                    raise ValueError('Member %s does not exist' % member_id)
  28.265 +            else:
  28.266 +                return None
  28.267 +        if hasattr( aq_base(members), member_id ):
  28.268 +            return None
  28.269 +        else:
  28.270 +            f_title = "%s's Home" % member_id
  28.271 +            members.manage_addPortalFolder( id=member_id, title=f_title )
  28.272 +            f=getattr(members, member_id)
  28.273 +
  28.274 +            f.manage_permission(View,
  28.275 +                                ['Owner','Manager','Reviewer'], 0)
  28.276 +            f.manage_permission(AccessContentsInformation,
  28.277 +                                ['Owner','Manager','Reviewer'], 0)
  28.278 +
  28.279 +            # Grant Ownership and Owner role to Member
  28.280 +            f.changeOwnership(member)
  28.281 +            f.__ac_local_roles__ = None
  28.282 +            f.manage_setLocalRoles(member_id, ['Owner'])
  28.283 +        return f
  28.284 +
  28.285 +    security.declarePublic('createMemberarea')
  28.286 +    createMemberarea = createMemberArea
  28.287 +
  28.288 +    security.declareProtected(ManageUsers, 'deleteMemberArea')
  28.289 +    def deleteMemberArea(self, member_id):
  28.290 +        """ Delete member area of member specified by member_id.
  28.291 +        """
  28.292 +        members = self.getMembersFolder()
  28.293 +        if not members:
  28.294 +            return 0
  28.295 +        if hasattr( aq_base(members), member_id ):
  28.296 +            members.manage_delObjects(member_id)
  28.297 +            return 1
  28.298 +        else:
  28.299 +            return 0
  28.300 +
  28.301 +    security.declarePublic('isAnonymousUser')
  28.302 +    def isAnonymousUser(self):
  28.303 +        '''
  28.304 +        Returns 1 if the user is not logged in.
  28.305 +        '''
  28.306 +        u = _getAuthenticatedUser(self)
  28.307 +        if u is None or u.getUserName() == 'Anonymous User':
  28.308 +            return 1
  28.309 +        return 0
  28.310 +
  28.311 +    security.declarePublic('checkPermission')
  28.312 +    def checkPermission(self, permissionName, object, subobjectName=None):
  28.313 +        '''
  28.314 +        Checks whether the current user has the given permission on
  28.315 +        the given object or subobject.
  28.316 +        '''
  28.317 +        if subobjectName is not None:
  28.318 +            object = getattr(object, subobjectName)
  28.319 +        return _checkPermission(permissionName, object)
  28.320 +
  28.321 +    security.declarePublic('credentialsChanged')
  28.322 +    def credentialsChanged(self, password):
  28.323 +        '''
  28.324 +        Notifies the authentication mechanism that this user has changed
  28.325 +        passwords.  This can be used to update the authentication cookie.
  28.326 +        Note that this call should *not* cause any change at all to user
  28.327 +        databases.
  28.328 +        '''
  28.329 +        if not self.isAnonymousUser():
  28.330 +            acl_users = self.acl_users
  28.331 +            user = _getAuthenticatedUser(self)
  28.332 +            name = user.getUserName()
  28.333 +            # this really does need to be the user name, and not the user id,
  28.334 +            # because we're dealing with authentication credentials
  28.335 +            if hasattr(acl_users.aq_base, 'credentialsChanged'):
  28.336 +                # Use an interface provided by LoginManager.
  28.337 +                acl_users.credentialsChanged(user, name, password)
  28.338 +            else:
  28.339 +                req = self.REQUEST
  28.340 +                p = getattr(req, '_credentials_changed_path', None)
  28.341 +                if p is not None:
  28.342 +                    # Use an interface provided by CookieCrumbler.
  28.343 +                    change = self.restrictedTraverse(p)
  28.344 +                    change(user, name, password)
  28.345 +
  28.346 +    security.declareProtected(ManageUsers, 'getMemberById')
  28.347 +    def getMemberById(self, id):
  28.348 +        '''
  28.349 +        Returns the given member.
  28.350 +        '''
  28.351 +        user = self._huntUser(id, self)
  28.352 +        if user is not None:
  28.353 +            user = self.wrapUser(user)
  28.354 +        return user
  28.355 +
  28.356 +    def _huntUser(self, username, context):
  28.357 +        """Find user in the hierarchy starting from bottom level 'start'.
  28.358 +        """
  28.359 +        uf = context.acl_users
  28.360 +        while uf is not None:
  28.361 +            user = uf.getUserById(username)
  28.362 +            if user is not None:
  28.363 +                return user
  28.364 +            container = aq_parent(aq_inner(uf))
  28.365 +            parent = aq_parent(aq_inner(container))
  28.366 +            uf = getattr(parent, 'acl_users', None)
  28.367 +        return None
  28.368 +
  28.369 +    def __getPUS(self):
  28.370 +        # Gets something we can call getUsers() and getUserNames() on.
  28.371 +        acl_users = self.acl_users
  28.372 +        if hasattr(acl_users, 'getUsers'):
  28.373 +            return acl_users
  28.374 +        else:
  28.375 +            # This hack works around the absence of getUsers() in LoginManager.
  28.376 +            # Gets the PersistentUserSource object that stores our users
  28.377 +            for us in acl_users.UserSourcesGroup.objectValues():
  28.378 +                if us.meta_type == 'Persistent User Source':
  28.379 +                    return us.__of__(acl_users)
  28.380 +
  28.381 +    security.declareProtected(ManageUsers, 'listMemberIds')
  28.382 +    def listMemberIds(self):
  28.383 +        '''Lists the ids of all members.  This may eventually be
  28.384 +        replaced with a set of methods for querying pieces of the
  28.385 +        list rather than the entire list at once.
  28.386 +        '''
  28.387 +        user_folder = self.__getPUS()
  28.388 +        return [ x.getId() for x in user_folder.getUsers() ]
  28.389 +
  28.390 +    security.declareProtected(ManageUsers, 'listMembers')
  28.391 +    def listMembers(self):
  28.392 +        '''Gets the list of all members.
  28.393 +        '''
  28.394 +        return map(self.wrapUser, self.__getPUS().getUsers())
  28.395 +
  28.396 +    security.declareProtected(ListPortalMembers, 'searchMembers')
  28.397 +    def searchMembers( self, search_param, search_term ):
  28.398 +        """ Search the membership """
  28.399 +        md = getToolByName( self, 'portal_memberdata' )
  28.400 +
  28.401 +        return md.searchMemberData( search_param, search_term )
  28.402 +
  28.403 +    security.declareProtected(View, 'getCandidateLocalRoles')
  28.404 +    def getCandidateLocalRoles(self, obj):
  28.405 +        """ What local roles can I assign?
  28.406 +        """
  28.407 +        member = self.getAuthenticatedMember()
  28.408 +        member_roles = member.getRolesInContext(obj)
  28.409 +        if _checkPermission(ManageUsers, obj):
  28.410 +            local_roles = self.getPortalRoles()
  28.411 +            if 'Manager' not in member_roles:
  28.412 +                 local_roles.remove('Manager')
  28.413 +        else:
  28.414 +            local_roles = [ role for role in member_roles
  28.415 +                            if role not in ('Member', 'Authenticated') ]
  28.416 +        local_roles.sort()
  28.417 +        return tuple(local_roles)
  28.418 +
  28.419 +    security.declareProtected(View, 'setLocalRoles')
  28.420 +    def setLocalRoles(self, obj, member_ids, member_role, reindex=1):
  28.421 +        """ Add local roles on an item.
  28.422 +        """
  28.423 +        if ( _checkPermission(ChangeLocalRoles, obj)
  28.424 +             and member_role in self.getCandidateLocalRoles(obj) ):
  28.425 +            for member_id in member_ids:
  28.426 +                roles = list(obj.get_local_roles_for_userid( userid=member_id ))
  28.427 +
  28.428 +                if member_role not in roles:
  28.429 +                    roles.append( member_role )
  28.430 +                    obj.manage_setLocalRoles( member_id, roles )
  28.431 +
  28.432 +        if reindex:
  28.433 +            # It is assumed that all objects have the method
  28.434 +            # reindexObjectSecurity, which is in CMFCatalogAware and
  28.435 +            # thus PortalContent and PortalFolder.
  28.436 +            obj.reindexObjectSecurity()
  28.437 +
  28.438 +    security.declareProtected(View, 'deleteLocalRoles')
  28.439 +    def deleteLocalRoles(self, obj, member_ids, reindex=1, recursive=0):
  28.440 +        """ Delete local roles of specified members.
  28.441 +        """
  28.442 +        if _checkPermission(ChangeLocalRoles, obj):
  28.443 +            for member_id in member_ids:
  28.444 +                if obj.get_local_roles_for_userid(userid=member_id):
  28.445 +                    obj.manage_delLocalRoles(userids=member_ids)
  28.446 +                    break
  28.447 +
  28.448 +        if recursive and hasattr( aq_base(obj), 'contentValues' ):
  28.449 +            for subobj in obj.contentValues():
  28.450 +                self.deleteLocalRoles(subobj, member_ids, 0, 1)
  28.451 +
  28.452 +        if reindex:
  28.453 +            # reindexObjectSecurity is always recursive
  28.454 +            obj.reindexObjectSecurity()
  28.455 +
  28.456 +    security.declarePrivate('addMember')
  28.457 +    def addMember(self, id, password, roles, domains, properties=None):
  28.458 +        '''Adds a new member to the user folder.  Security checks will have
  28.459 +        already been performed.  Called by portal_registration.
  28.460 +        '''
  28.461 +        acl_users = self.acl_users
  28.462 +        if hasattr(acl_users, '_doAddUser'):
  28.463 +            acl_users._doAddUser(id, password, roles, domains)
  28.464 +        else:
  28.465 +            # The acl_users folder is a LoginManager.  Search for a UserSource
  28.466 +            # with the needed support.
  28.467 +            for source in acl_users.UserSourcesGroup.objectValues():
  28.468 +                if hasattr(source, 'addUser'):
  28.469 +                    source.__of__(self).addUser(id, password, roles, domains)
  28.470 +            raise "Can't add Member", "No supported UserSources"
  28.471 +
  28.472 +        if properties is not None:
  28.473 +            member = self.getMemberById(id)
  28.474 +            member.setMemberProperties(properties)
  28.475 +
  28.476 +    security.declareProtected(ManageUsers, 'deleteMembers')
  28.477 +    def deleteMembers(self, member_ids, delete_memberareas=1,
  28.478 +                      delete_localroles=1):
  28.479 +        """ Delete members specified by member_ids.
  28.480 +        """
  28.481 +
  28.482 +        # Delete members in acl_users.
  28.483 +        acl_users = self.acl_users
  28.484 +        if _checkPermission(ManageUsers, acl_users):
  28.485 +            if isinstance(member_ids, basestring):
  28.486 +                member_ids = (member_ids,)
  28.487 +            member_ids = list(member_ids)
  28.488 +            for member_id in member_ids[:]:
  28.489 +                if not acl_users.getUserById(member_id, None):
  28.490 +                    member_ids.remove(member_id)
  28.491 +            try:
  28.492 +                acl_users.userFolderDelUsers(member_ids)
  28.493 +            except (NotImplementedError, 'NotImplemented'):
  28.494 +                raise NotImplementedError('The underlying User Folder '
  28.495 +                                         'doesn\'t support deleting members.')
  28.496 +        else:
  28.497 +            raise AccessControl_Unauthorized('You need the \'Manage users\' '
  28.498 +                                 'permission for the underlying User Folder.')
  28.499 +
  28.500 +        # Delete member data in portal_memberdata.
  28.501 +        mdtool = getToolByName(self, 'portal_memberdata', None)
  28.502 +        if mdtool is not None:
  28.503 +            for member_id in member_ids:
  28.504 +                mdtool.deleteMemberData(member_id)
  28.505 +
  28.506 +        # Delete members' home folders including all content items.
  28.507 +        if delete_memberareas:
  28.508 +            for member_id in member_ids:
  28.509 +                 self.deleteMemberArea(member_id)
  28.510 +
  28.511 +        # Delete members' local roles.
  28.512 +        if delete_localroles:
  28.513 +            utool = getToolByName(self, 'portal_url', None)
  28.514 +            self.deleteLocalRoles( utool.getPortalObject(), member_ids,
  28.515 +                                   reindex=1, recursive=1 )
  28.516 +
  28.517 +        return tuple(member_ids)
  28.518 +
  28.519 +    security.declarePublic('getHomeFolder')
  28.520 +    def getHomeFolder(self, id=None, verifyPermission=0):
  28.521 +        """Returns a member's home folder object or None.
  28.522 +        Set verifyPermission to 1 to return None when the user
  28.523 +        doesn't have the View permission on the folder.
  28.524 +        """
  28.525 +        return None
  28.526 +
  28.527 +    security.declarePublic('getHomeUrl')
  28.528 +    def getHomeUrl(self, id=None, verifyPermission=0):
  28.529 +        """Returns the URL to a member's home folder or None.
  28.530 +        Set verifyPermission to 1 to return None when the user
  28.531 +        doesn't have the View permission on the folder.
  28.532 +        """
  28.533 +        return None
  28.534 +
  28.535 +InitializeClass(MembershipTool)
    29.1 new file mode 100644
    29.2 --- /dev/null
    29.3 +++ b/PortalContent.py
    29.4 @@ -0,0 +1,118 @@
    29.5 +##############################################################################
    29.6 +#
    29.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    29.8 +#
    29.9 +# This software is subject to the provisions of the Zope Public License,
   29.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   29.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   29.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   29.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   29.14 +# FOR A PARTICULAR PURPOSE.
   29.15 +#
   29.16 +##############################################################################
   29.17 +""" PortalContent: Base class for all CMF content.
   29.18 +
   29.19 +$Id$
   29.20 +"""
   29.21 +
   29.22 +from Globals import InitializeClass
   29.23 +from Acquisition import aq_base
   29.24 +from OFS.SimpleItem import SimpleItem
   29.25 +from AccessControl import ClassSecurityInfo
   29.26 +from webdav.WriteLockInterface import WriteLockInterface
   29.27 +
   29.28 +from interfaces.Contentish import Contentish
   29.29 +from DynamicType import DynamicType
   29.30 +from utils import _getViewFor
   29.31 +from CMFCatalogAware import CMFCatalogAware
   29.32 +from exceptions import ResourceLockedError
   29.33 +from permissions import FTPAccess
   29.34 +from permissions import View
   29.35 +
   29.36 +
   29.37 +# Old names that some third-party packages may need.
   29.38 +NoWL = 0
   29.39 +
   29.40 +
   29.41 +class PortalContent(DynamicType, CMFCatalogAware, SimpleItem):
   29.42 +    """
   29.43 +        Base class for portal objects.
   29.44 +
   29.45 +        Provides hooks for reviewing, indexing, and CMF UI.
   29.46 +
   29.47 +        Derived classes must implement the interface described in
   29.48 +        interfaces/DublinCore.py.
   29.49 +    """
   29.50 +
   29.51 +    __implements__ = (Contentish,
   29.52 +                      WriteLockInterface,
   29.53 +                      DynamicType.__implements__)
   29.54 +
   29.55 +    isPortalContent = 1
   29.56 +    _isPortalContent = 1  # More reliable than 'isPortalContent'.
   29.57 +
   29.58 +    manage_options = ( ( { 'label'  : 'Dublin Core'
   29.59 +                         , 'action' : 'manage_metadata'
   29.60 +                         }
   29.61 +                       , { 'label'  : 'Edit'
   29.62 +                         , 'action' : 'manage_edit'
   29.63 +                         }
   29.64 +                       , { 'label'  : 'View'
   29.65 +                         , 'action' : 'view'
   29.66 +                         }
   29.67 +                       )
   29.68 +                     + CMFCatalogAware.manage_options
   29.69 +                     + SimpleItem.manage_options
   29.70 +                     )
   29.71 +
   29.72 +    security = ClassSecurityInfo()
   29.73 +
   29.74 +    security.declareObjectProtected(View)
   29.75 +
   29.76 +    # The security for FTP methods aren't set up by default in our
   29.77 +    # superclasses...  :(
   29.78 +    security.declareProtected(FTPAccess, 'manage_FTPstat')
   29.79 +    security.declareProtected(FTPAccess, 'manage_FTPlist')
   29.80 +
   29.81 +    def failIfLocked(self):
   29.82 +        """ Check if isLocked via webDav
   29.83 +        """
   29.84 +        if self.wl_isLocked():
   29.85 +            raise ResourceLockedError('This resource is locked via webDAV.')
   29.86 +        return 0
   29.87 +
   29.88 +    #
   29.89 +    #   Contentish interface methods
   29.90 +    #
   29.91 +    security.declareProtected(View, 'SearchableText')
   29.92 +    def SearchableText(self):
   29.93 +        """ Returns a concatination of all searchable text.
   29.94 +
   29.95 +        Should be overriden by portal objects.
   29.96 +        """
   29.97 +        return "%s %s" % (self.Title(), self.Description())
   29.98 +
   29.99 +    def __call__(self):
  29.100 +        """ Invokes the default view.
  29.101 +        """
  29.102 +        ti = self.getTypeInfo()
  29.103 +        method_id = ti and ti.queryMethodID('(Default)', context=self)
  29.104 +        if method_id and method_id!='(Default)':
  29.105 +            method = getattr(self, method_id)
  29.106 +        else:
  29.107 +            method = _getViewFor(self)
  29.108 +
  29.109 +        if getattr(aq_base(method), 'isDocTemp', 0):
  29.110 +            return method(self, self.REQUEST, self.REQUEST['RESPONSE'])
  29.111 +        else:
  29.112 +            return method()
  29.113 +
  29.114 +    index_html = None  # This special value informs ZPublisher to use __call__
  29.115 +
  29.116 +    security.declareProtected(View, 'view')
  29.117 +    def view(self):
  29.118 +        """ Returns the default view even if index_html is overridden.
  29.119 +        """
  29.120 +        return self()
  29.121 +
  29.122 +InitializeClass(PortalContent)
    30.1 new file mode 100644
    30.2 --- /dev/null
    30.3 +++ b/PortalFolder.py
    30.4 @@ -0,0 +1,720 @@
    30.5 +##############################################################################
    30.6 +#
    30.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    30.8 +#
    30.9 +# This software is subject to the provisions of the Zope Public License,
   30.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   30.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   30.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   30.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   30.14 +# FOR A PARTICULAR PURPOSE.
   30.15 +#
   30.16 +##############################################################################
   30.17 +""" PortalFolder: CMF-enabled Folder objects.
   30.18 +
   30.19 +$Id$
   30.20 +"""
   30.21 +
   30.22 +import base64
   30.23 +import marshal
   30.24 +import re
   30.25 +from warnings import warn
   30.26 +
   30.27 +from AccessControl import ClassSecurityInfo
   30.28 +from AccessControl import getSecurityManager
   30.29 +from Acquisition import aq_parent, aq_inner, aq_base
   30.30 +from Globals import DTMLFile
   30.31 +from Globals import InitializeClass
   30.32 +from OFS.OrderSupport import OrderSupport
   30.33 +from OFS.Folder import Folder
   30.34 +
   30.35 +from CMFCatalogAware import CMFCatalogAware
   30.36 +from DynamicType import DynamicType
   30.37 +from exceptions import AccessControl_Unauthorized
   30.38 +from exceptions import BadRequest
   30.39 +from exceptions import zExceptions_Unauthorized
   30.40 +from interfaces.Folderish import Folderish as IFolderish
   30.41 +from permissions import AddPortalContent
   30.42 +from permissions import AddPortalFolders
   30.43 +from permissions import ChangeLocalRoles
   30.44 +from permissions import DeleteObjects
   30.45 +from permissions import ListFolderContents
   30.46 +from permissions import ManagePortal
   30.47 +from permissions import ManageProperties
   30.48 +from permissions import View
   30.49 +from utils import _checkPermission
   30.50 +from utils import getToolByName
   30.51 +
   30.52 +
   30.53 +factory_type_information = (
   30.54 +  { 'id'             : 'Folder'
   30.55 +  , 'meta_type'      : 'Portal Folder'
   30.56 +  , 'description'    : """ Use folders to put content in categories."""
   30.57 +  , 'icon'           : 'folder_icon.gif'
   30.58 +  , 'product'        : 'CMFCore'
   30.59 +  , 'factory'        : 'manage_addPortalFolder'
   30.60 +  , 'filter_content_types' : 0
   30.61 +  , 'immediate_view' : 'folder_edit_form'
   30.62 +  , 'aliases'        : {'(Default)': 'index_html',
   30.63 +                        'view': 'index_html',
   30.64 +                        'index.html':'index_html'}
   30.65 +  , 'actions'        : ( { 'id'            : 'view'
   30.66 +                         , 'name'          : 'View'
   30.67 +                         , 'action': 'string:${object_url}'
   30.68 +                         , 'permissions'   : (View,)
   30.69 +                         }
   30.70 +                       , { 'id'            : 'edit'
   30.71 +                         , 'name'          : 'Edit'
   30.72 +                         , 'action': 'string:${object_url}/folder_edit_form'
   30.73 +                         , 'permissions'   : (ManageProperties,)
   30.74 +                         }
   30.75 +                       , { 'id'            : 'localroles'
   30.76 +                         , 'name'          : 'Local Roles'
   30.77 +                         , 'action':
   30.78 +                                  'string:${object_url}/folder_localrole_form'
   30.79 +                         , 'permissions'   : (ChangeLocalRoles,)
   30.80 +                         }
   30.81 +                       , { 'id'            : 'folderContents'
   30.82 +                         , 'name'          : 'Folder contents'
   30.83 +                         , 'action': 'string:${object_url}/folder_contents'
   30.84 +                         , 'permissions'   : (ListFolderContents,)
   30.85 +                         }
   30.86 +                       , { 'id'            : 'new'
   30.87 +                         , 'name'          : 'New...'
   30.88 +                         , 'action': 'string:${object_url}/folder_factories'
   30.89 +                         , 'permissions'   : (AddPortalContent,)
   30.90 +                         , 'visible'       : 0
   30.91 +                         }
   30.92 +                       , { 'id'            : 'rename_items'
   30.93 +                         , 'name'          : 'Rename items'
   30.94 +                         , 'action': 'string:${object_url}/folder_rename_form'
   30.95 +                         , 'permissions'   : (AddPortalContent,)
   30.96 +                         , 'visible'       : 0
   30.97 +                         }
   30.98 +                       )
   30.99 +  }
  30.100 +,
  30.101 +)
  30.102 +
  30.103 +
  30.104 +class PortalFolderBase(DynamicType, CMFCatalogAware, Folder):
  30.105 +    """Base class for portal folder
  30.106 +    """
  30.107 +    meta_type = 'Portal Folder Base'
  30.108 +
  30.109 +    __implements__ = (IFolderish, DynamicType.__implements__,
  30.110 +                      Folder.__implements__)
  30.111 +
  30.112 +    security = ClassSecurityInfo()
  30.113 +
  30.114 +    description = ''
  30.115 +
  30.116 +    manage_options = ( Folder.manage_options +
  30.117 +                       CMFCatalogAware.manage_options )
  30.118 +
  30.119 +    def __init__( self, id, title='' ):
  30.120 +        self.id = id
  30.121 +        self.title = title
  30.122 +
  30.123 +    #
  30.124 +    #   'MutableDublinCore' interface methods
  30.125 +    #
  30.126 +    security.declareProtected(ManageProperties, 'setTitle')
  30.127 +    def setTitle( self, title ):
  30.128 +        """ Set Dublin Core Title element - resource name.
  30.129 +        """
  30.130 +        self.title = title
  30.131 +
  30.132 +    security.declareProtected(ManageProperties, 'setDescription')
  30.133 +    def setDescription( self, description ):
  30.134 +        """ Set Dublin Core Description element - resource summary.
  30.135 +        """
  30.136 +        self.description = description
  30.137 +
  30.138 +    #
  30.139 +    #   other methods
  30.140 +    #
  30.141 +    security.declareProtected(ManageProperties, 'edit')
  30.142 +    def edit(self, title='', description=''):
  30.143 +        """
  30.144 +        Edit the folder title (and possibly other attributes later)
  30.145 +        """
  30.146 +        self.setTitle( title )
  30.147 +        self.setDescription( description )
  30.148 +        self.reindexObject()
  30.149 +
  30.150 +    security.declarePublic('allowedContentTypes')
  30.151 +    def allowedContentTypes( self ):
  30.152 +        """
  30.153 +            List type info objects for types which can be added in
  30.154 +            this folder.
  30.155 +        """
  30.156 +        result = []
  30.157 +        portal_types = getToolByName(self, 'portal_types')
  30.158 +        myType = portal_types.getTypeInfo(self)
  30.159 +
  30.160 +        if myType is not None:
  30.161 +            for contentType in portal_types.listTypeInfo(self):
  30.162 +                if myType.allowType( contentType.getId() ):
  30.163 +                    result.append( contentType )
  30.164 +        else:
  30.165 +            result = portal_types.listTypeInfo()
  30.166 +
  30.167 +        return filter( lambda typ, container=self:
  30.168 +                          typ.isConstructionAllowed( container )
  30.169 +                     , result )
  30.170 +
  30.171 +
  30.172 +    def _morphSpec(self, spec):
  30.173 +        '''
  30.174 +        spec is a sequence of meta_types, a string containing one meta type,
  30.175 +        or None.  If spec is empty or None, returns all contentish
  30.176 +        meta_types.  Otherwise ensures all of the given meta types are
  30.177 +        contentish.
  30.178 +        '''
  30.179 +        warn('Using the \'spec\' argument is deprecated. In CMF 2.0 '
  30.180 +             'contentItems(), contentIds(), contentValues() and '
  30.181 +             'listFolderContents() will no longer support \'spec\'. Use the '
  30.182 +             '\'filter\' argument with \'portal_type\' instead.',
  30.183 +             DeprecationWarning)
  30.184 +        new_spec = []
  30.185 +        types_tool = getToolByName(self, 'portal_types')
  30.186 +        types = types_tool.listContentTypes( by_metatype=1 )
  30.187 +        if spec is not None:
  30.188 +            if type(spec) == type(''):
  30.189 +                spec = [spec]
  30.190 +            for meta_type in spec:
  30.191 +                if not meta_type in types:
  30.192 +                    raise ValueError('%s is not a content type' % meta_type)
  30.193 +                new_spec.append(meta_type)
  30.194 +        return new_spec or types
  30.195 +
  30.196 +    def _filteredItems( self, ids, filt ):
  30.197 +        """
  30.198 +            Apply filter, a mapping, to child objects indicated by 'ids',
  30.199 +            returning a sequence of ( id, obj ) tuples.
  30.200 +        """
  30.201 +        # Restrict allowed content types
  30.202 +        if filt is None:
  30.203 +            filt = {}
  30.204 +        else:
  30.205 +            # We'll modify it, work on a copy.
  30.206 +            filt = filt.copy()
  30.207 +        pt = filt.get('portal_type', [])
  30.208 +        if type(pt) is type(''):
  30.209 +            pt = [pt]
  30.210 +        types_tool = getToolByName(self, 'portal_types')
  30.211 +        allowed_types = types_tool.listContentTypes()
  30.212 +        if not pt:
  30.213 +            pt = allowed_types
  30.214 +        else:
  30.215 +            pt = [t for t in pt if t in allowed_types]
  30.216 +        if not pt:
  30.217 +            # After filtering, no types remain, so nothing should be
  30.218 +            # returned.
  30.219 +            return []
  30.220 +        filt['portal_type'] = pt
  30.221 +
  30.222 +        query = ContentFilter(**filt)
  30.223 +        result = []
  30.224 +        append = result.append
  30.225 +        get = self._getOb
  30.226 +        for id in ids:
  30.227 +            obj = get( id )
  30.228 +            if query(obj):
  30.229 +                append( (id, obj) )
  30.230 +        return result
  30.231 +
  30.232 +    #
  30.233 +    #   'Folderish' interface methods
  30.234 +    #
  30.235 +    security.declarePublic('contentItems')
  30.236 +    def contentItems( self, spec=None, filter=None ):
  30.237 +        # List contentish and folderish sub-objects and their IDs.
  30.238 +        # (method is without docstring to disable publishing)
  30.239 +        #
  30.240 +        if spec is None:
  30.241 +            ids = self.objectIds()
  30.242 +        else:
  30.243 +            # spec is deprecated, use filter instead!
  30.244 +            spec = self._morphSpec(spec)
  30.245 +            ids = self.objectIds(spec)
  30.246 +        return self._filteredItems( ids, filter )
  30.247 +
  30.248 +    security.declarePublic('contentIds')
  30.249 +    def contentIds( self, spec=None, filter=None):
  30.250 +        # List IDs of contentish and folderish sub-objects.
  30.251 +        # (method is without docstring to disable publishing)
  30.252 +        #
  30.253 +        if spec is None:
  30.254 +            ids = self.objectIds()
  30.255 +        else:
  30.256 +            # spec is deprecated, use filter instead!
  30.257 +            spec = self._morphSpec(spec)
  30.258 +            ids = self.objectIds(spec)
  30.259 +        return map( lambda item: item[0],
  30.260 +                    self._filteredItems( ids, filter ) )
  30.261 +
  30.262 +    security.declarePublic('contentValues')
  30.263 +    def contentValues( self, spec=None, filter=None ):
  30.264 +        # List contentish and folderish sub-objects.
  30.265 +        # (method is without docstring to disable publishing)
  30.266 +        #
  30.267 +        if spec is None:
  30.268 +            ids = self.objectIds()
  30.269 +        else:
  30.270 +            # spec is deprecated, use filter instead!
  30.271 +            spec = self._morphSpec(spec)
  30.272 +            ids = self.objectIds(spec)
  30.273 +        return map( lambda item: item[1],
  30.274 +                    self._filteredItems( ids, filter ) )
  30.275 +
  30.276 +    security.declareProtected(ListFolderContents, 'listFolderContents')
  30.277 +    def listFolderContents( self, spec=None, contentFilter=None ):
  30.278 +        """ List viewable contentish and folderish sub-objects.
  30.279 +        """
  30.280 +        items = self.contentItems(spec=spec, filter=contentFilter)
  30.281 +        l = []
  30.282 +        for id, obj in items:
  30.283 +            # validate() can either raise Unauthorized or return 0 to
  30.284 +            # mean unauthorized.
  30.285 +            try:
  30.286 +                if getSecurityManager().validate(self, self, id, obj):
  30.287 +                    l.append(obj)
  30.288 +            except zExceptions_Unauthorized:  # Catch *all* Unauths!
  30.289 +                pass
  30.290 +        return l
  30.291 +
  30.292 +    #
  30.293 +    #   webdav Resource method
  30.294 +    #
  30.295 +
  30.296 +    # protected by 'WebDAV access'
  30.297 +    def listDAVObjects(self):
  30.298 +        # List sub-objects for PROPFIND requests.
  30.299 +        # (method is without docstring to disable publishing)
  30.300 +        #
  30.301 +        if _checkPermission(ManagePortal, self):
  30.302 +            return self.objectValues()
  30.303 +        else:
  30.304 +            return self.listFolderContents()
  30.305 +
  30.306 +    #
  30.307 +    #   'DublinCore' interface methods
  30.308 +    #
  30.309 +    security.declareProtected(View, 'Title')
  30.310 +    def Title( self ):
  30.311 +        """ Dublin Core Title element - resource name.
  30.312 +        """
  30.313 +        return self.title
  30.314 +
  30.315 +    security.declareProtected(View, 'Description')
  30.316 +    def Description( self ):
  30.317 +        """ Dublin Core Description element - resource summary.
  30.318 +        """
  30.319 +        return self.description
  30.320 +
  30.321 +    security.declareProtected(View, 'Type')
  30.322 +    def Type( self ):
  30.323 +        """ Dublin Core Type element - resource type.
  30.324 +        """
  30.325 +        if hasattr(aq_base(self), 'getTypeInfo'):
  30.326 +            ti = self.getTypeInfo()
  30.327 +            if ti is not None:
  30.328 +                return ti.Title()
  30.329 +        return self.meta_type
  30.330 +
  30.331 +    #
  30.332 +    #   other methods
  30.333 +    #
  30.334 +    security.declarePublic('encodeFolderFilter')
  30.335 +    def encodeFolderFilter(self, REQUEST):
  30.336 +        """
  30.337 +            Parse cookie string for using variables in dtml.
  30.338 +        """
  30.339 +        filter = {}
  30.340 +        for key, value in REQUEST.items():
  30.341 +            if key[:10] == 'filter_by_':
  30.342 +                filter[key[10:]] = value
  30.343 +        encoded = base64.encodestring( marshal.dumps(filter) ).strip()
  30.344 +        encoded = ''.join( encoded.split('\n') )
  30.345 +        return encoded
  30.346 +
  30.347 +    security.declarePublic('decodeFolderFilter')
  30.348 +    def decodeFolderFilter(self, encoded):
  30.349 +        """
  30.350 +            Parse cookie string for using variables in dtml.
  30.351 +        """
  30.352 +        filter = {}
  30.353 +        if encoded:
  30.354 +            filter.update(marshal.loads(base64.decodestring(encoded)))
  30.355 +        return filter
  30.356 +
  30.357 +    def content_type( self ):
  30.358 +        """
  30.359 +            WebDAV needs this to do the Right Thing (TM).
  30.360 +        """
  30.361 +        return None
  30.362 +
  30.363 +    # Ensure pure PortalFolders don't get cataloged.
  30.364 +    # XXX We may want to revisit this.
  30.365 +
  30.366 +    def indexObject(self):
  30.367 +        pass
  30.368 +
  30.369 +    def unindexObject(self):
  30.370 +        pass
  30.371 +
  30.372 +    def reindexObject(self, idxs=[]):
  30.373 +        pass
  30.374 +
  30.375 +    def reindexObjectSecurity(self):
  30.376 +        pass
  30.377 +
  30.378 +    def PUT_factory( self, name, typ, body ):
  30.379 +        """ Factory for PUT requests to objects which do not yet exist.
  30.380 +
  30.381 +        Used by NullResource.PUT.
  30.382 +
  30.383 +        Returns -- Bare and empty object of the appropriate type (or None, if
  30.384 +        we don't know what to do)
  30.385 +        """
  30.386 +        registry = getToolByName(self, 'content_type_registry', None)
  30.387 +        if registry is None:
  30.388 +            return None
  30.389 +
  30.390 +        typeObjectName = registry.findTypeName( name, typ, body )
  30.391 +        if typeObjectName is None:
  30.392 +            return None
  30.393 +
  30.394 +        self.invokeFactory( typeObjectName, name )
  30.395 +
  30.396 +        # invokeFactory does too much, so the object has to be removed again
  30.397 +        obj = aq_base( self._getOb( name ) )
  30.398 +        self._delObject( name )
  30.399 +        return obj
  30.400 +
  30.401 +    security.declareProtected(AddPortalContent, 'invokeFactory')
  30.402 +    def invokeFactory(self, type_name, id, RESPONSE=None, *args, **kw):
  30.403 +        """ Invokes the portal_types tool.
  30.404 +        """
  30.405 +        pt = getToolByName(self, 'portal_types')
  30.406 +        myType = pt.getTypeInfo(self)
  30.407 +
  30.408 +        if myType is not None:
  30.409 +            if not myType.allowType( type_name ):
  30.410 +                raise ValueError('Disallowed subobject type: %s' % type_name)
  30.411 +
  30.412 +        return pt.constructContent(type_name, self, id, RESPONSE, *args, **kw)
  30.413 +
  30.414 +    security.declareProtected(AddPortalContent, 'checkIdAvailable')
  30.415 +    def checkIdAvailable(self, id):
  30.416 +        try:
  30.417 +            self._checkId(id)
  30.418 +        except BadRequest:
  30.419 +            return False
  30.420 +        else:
  30.421 +            return True
  30.422 +
  30.423 +    def MKCOL_handler(self,id,REQUEST=None,RESPONSE=None):
  30.424 +        """
  30.425 +            Handle WebDAV MKCOL.
  30.426 +        """
  30.427 +        self.manage_addFolder( id=id, title='' )
  30.428 +
  30.429 +    def _checkId(self, id, allow_dup=0):
  30.430 +        PortalFolderBase.inheritedAttribute('_checkId')(self, id, allow_dup)
  30.431 +
  30.432 +        if allow_dup:
  30.433 +            return
  30.434 +
  30.435 +        # FIXME: needed to allow index_html for join code
  30.436 +        if id == 'index_html':
  30.437 +            return
  30.438 +
  30.439 +        # Another exception: Must allow "syndication_information" to enable
  30.440 +        # Syndication...
  30.441 +        if id == 'syndication_information':
  30.442 +            return
  30.443 +
  30.444 +        # This code prevents people other than the portal manager from
  30.445 +        # overriding skinned names and tools.
  30.446 +        if not getSecurityManager().checkPermission(ManagePortal, self):
  30.447 +            ob = self
  30.448 +            while ob is not None and not getattr(ob, '_isPortalRoot', False):
  30.449 +                ob = aq_parent( aq_inner(ob) )
  30.450 +            if ob is not None:
  30.451 +                # If the portal root has a non-contentish object by this name,
  30.452 +                # don't allow an override.
  30.453 +                if (hasattr(ob, id) and
  30.454 +                    id not in ob.contentIds() and
  30.455 +                    # Allow root doted prefixed object name overrides
  30.456 +                    not id.startswith('.')):
  30.457 +                    raise BadRequest('The id "%s" is reserved.' % id)
  30.458 +            # Don't allow ids used by Method Aliases.
  30.459 +            ti = self.getTypeInfo()
  30.460 +            if ti and ti.queryMethodID(id, context=self):
  30.461 +                raise BadRequest('The id "%s" is reserved.' % id)
  30.462 +        # Otherwise we're ok.
  30.463 +
  30.464 +    def _verifyObjectPaste(self, object, validate_src=1):
  30.465 +        # This assists the version in OFS.CopySupport.
  30.466 +        # It enables the clipboard to function correctly
  30.467 +        # with objects created by a multi-factory.
  30.468 +        securityChecksDone = False
  30.469 +        sm = getSecurityManager()
  30.470 +        parent = aq_parent(aq_inner(object))
  30.471 +        object_id = object.getId()
  30.472 +        mt = getattr(object, '__factory_meta_type__', None)
  30.473 +        meta_types = getattr(self, 'all_meta_types', None)
  30.474 +
  30.475 +        if mt is not None and meta_types is not None:
  30.476 +            method_name=None
  30.477 +            permission_name = None
  30.478 +
  30.479 +            if callable(meta_types):
  30.480 +                meta_types = meta_types()
  30.481 +
  30.482 +            for d in meta_types:
  30.483 +
  30.484 +                if d['name']==mt:
  30.485 +                    method_name=d['action']
  30.486 +                    permission_name = d.get('permission', None)
  30.487 +                    break
  30.488 +
  30.489 +            if permission_name is not None:
  30.490 +
  30.491 +                if not sm.checkPermission(permission_name,self):
  30.492 +                    raise AccessControl_Unauthorized, method_name
  30.493 +
  30.494 +                if validate_src:
  30.495 +
  30.496 +                    if not sm.validate(None, parent, None, object):
  30.497 +                        raise AccessControl_Unauthorized, object_id
  30.498 +
  30.499 +                if validate_src > 1:
  30.500 +                    if not sm.checkPermission(DeleteObjects, parent):
  30.501 +                        raise AccessControl_Unauthorized
  30.502 +
  30.503 +                # validation succeeded
  30.504 +                securityChecksDone = 1
  30.505 +
  30.506 +            #
  30.507 +            # Old validation for objects that may not have registered
  30.508 +            # themselves in the proper fashion.
  30.509 +            #
  30.510 +            elif method_name is not None:
  30.511 +
  30.512 +                meth = self.unrestrictedTraverse(method_name)
  30.513 +
  30.514 +                factory = getattr(meth, 'im_self', None)
  30.515 +
  30.516 +                if factory is None:
  30.517 +                    factory = aq_parent(aq_inner(meth))
  30.518 +
  30.519 +                if not sm.validate(None, factory, None, meth):
  30.520 +                    raise AccessControl_Unauthorized, method_name
  30.521 +
  30.522 +                # Ensure the user is allowed to access the object on the
  30.523 +                # clipboard.
  30.524 +                if validate_src:
  30.525 +
  30.526 +                    if not sm.validate(None, parent, None, object):
  30.527 +                        raise AccessControl_Unauthorized, object_id
  30.528 +
  30.529 +                if validate_src > 1: # moving
  30.530 +                    if not sm.checkPermission(DeleteObjects, parent):
  30.531 +                        raise AccessControl_Unauthorized
  30.532 +
  30.533 +                securityChecksDone = 1
  30.534 +
  30.535 +        # Call OFS' _verifyObjectPaste if necessary
  30.536 +        if not securityChecksDone:
  30.537 +            PortalFolderBase.inheritedAttribute(
  30.538 +                '_verifyObjectPaste')(self, object, validate_src)
  30.539 +
  30.540 +        # Finally, check allowed content types
  30.541 +        if hasattr(aq_base(object), 'getPortalTypeName'):
  30.542 +
  30.543 +            type_name = object.getPortalTypeName()
  30.544 +
  30.545 +            if type_name is not None:
  30.546 +
  30.547 +                pt = getToolByName(self, 'portal_types')
  30.548 +                myType = pt.getTypeInfo(self)
  30.549 +
  30.550 +                if myType is not None and not myType.allowType(type_name):
  30.551 +                    raise ValueError('Disallowed subobject type: %s'
  30.552 +                                        % type_name)
  30.553 +
  30.554 +    security.setPermissionDefault(AddPortalContent, ('Owner','Manager'))
  30.555 +
  30.556 +    security.declareProtected(AddPortalFolders, 'manage_addFolder')
  30.557 +    def manage_addFolder( self
  30.558 +                        , id
  30.559 +                        , title=''
  30.560 +                        , REQUEST=None
  30.561 +                        ):
  30.562 +        """ Add a new folder-like object with id *id*.
  30.563 +
  30.564 +        IF present, use the parent object's 'mkdir' alias; otherwise, just add
  30.565 +        a PortalFolder.
  30.566 +        """
  30.567 +        ti = self.getTypeInfo()
  30.568 +        method_id = ti and ti.queryMethodID('mkdir', context=self)
  30.569 +        if method_id:
  30.570 +            # call it
  30.571 +            getattr(self, method_id)(id=id)
  30.572 +        else:
  30.573 +            self.invokeFactory( type_name='Folder', id=id )
  30.574 +
  30.575 +        ob = self._getOb( id )
  30.576 +        ob.setTitle( title )
  30.577 +        try:
  30.578 +            ob.reindexObject()
  30.579 +        except AttributeError:
  30.580 +            pass
  30.581 +
  30.582 +        if REQUEST is not None:
  30.583 +            return self.manage_main(self, REQUEST, update_menu=1)
  30.584 +
  30.585 +InitializeClass(PortalFolderBase)
  30.586 +
  30.587 +
  30.588 +class PortalFolder(OrderSupport, PortalFolderBase):
  30.589 +    """
  30.590 +        Implements portal content management, but not UI details.
  30.591 +    """
  30.592 +    meta_type = 'Portal Folder'
  30.593 +    portal_type = 'Folder'
  30.594