1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # Copyright (C) 2005-2007 BenoƮt PIN <benoit.pin@ensmp.fr> #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program; if not, write to the Free Software #
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
19 #######################################################################################
20 """ Plinn portal folder implementation
26 from OFS
.CopySupport
import CopyError
, eNoData
, _cb_decode
, eInvalid
, eNotFound
,\
27 eNotSupported
, sanity_check
, cookie_path
28 from App
.Dialogs
import MessageDialog
29 from zExceptions
import BadRequest
30 from zExceptions
import Unauthorized
33 from cgi
import escape
34 from OFS
import Moniker
35 from ZODB
.POSException
import ConflictError
36 import OFS
.subscribers
37 from zope
.event
import notify
38 from zope
.lifecycleevent
import ObjectCopiedEvent
40 from zope
.app
.container
.contained
import notifyContainerModified
41 from zope
.app
.container
.contained
import ObjectMovedEvent
44 from zope
.container
.contained
import notifyContainerModified
45 from zope
.container
.contained
import ObjectMovedEvent
46 from OFS
.event
import ObjectClonedEvent
47 from OFS
.event
import ObjectWillBeMovedEvent
48 from zope
.component
.factory
import Factory
49 from Acquisition
import aq_base
, aq_inner
, aq_parent
51 from types
import StringType
, NoneType
52 from Products
.CMFCore
.permissions
import ListFolderContents
, View
, ViewManagementScreens
,\
53 ManageProperties
, AddPortalFolders
, AddPortalContent
,\
54 ManagePortal
, ModifyPortalContent
55 from permissions
import DeletePortalContents
, DeleteObjects
, DeleteOwnedObjects
, SetLocalRoles
, CheckMemberPermission
56 from Products
.CMFCore
.utils
import _checkPermission
, getToolByName
57 from Products
.CMFCore
.utils
import getUtilityByInterfaceName
58 from Products
.CMFCore
.CMFCatalogAware
import CMFCatalogAware
59 from Products
.CMFCore
.PortalFolder
import PortalFolder
, ContentFilter
60 from Products
.CMFDefault
.DublinCore
import DefaultDublinCoreImpl
62 from zope
.interface
import implements
63 from Products
.CMFCore
.interfaces
import IContentish
65 from utils
import _checkMemberPermission
66 from utils
import Message
as _
67 from utils
import makeValidId
68 from Globals
import InitializeClass
69 from AccessControl
import ClassSecurityInfo
70 from ZServer
import LARGE_FILE_THRESHOLD
71 from webdav
.interfaces
import IWriteLock
72 from webdav
.common
import Locked
73 from webdav
.common
import PreconditionFailed
74 from zope
.contenttype
import guess_content_type
77 class PlinnFolder(CMFCatalogAware
, PortalFolder
, DefaultDublinCoreImpl
) :
80 implements(IContentish
)
82 security
= ClassSecurityInfo()
84 manage_options
= PortalFolder
.manage_options
86 ## change security for inherited methods
87 security
.declareProtected(AddPortalContent
, 'manage_pasteObjects')
89 def __init__( self
, id, title
='' ) :
90 PortalFolder
.__init
__(self
, id)
91 DefaultDublinCoreImpl
.__init
__(self
, title
= title
)
93 security
.declarePublic('allowedContentTypes')
94 def allowedContentTypes(self
):
96 List type info objects for types which can be added in this folder.
97 Types can be filtered using the localContentTypes attribute.
99 allowedTypes
= PortalFolder
.allowedContentTypes(self
)
100 if hasattr(self
, 'localContentTypes'):
101 allowedTypes
= [t
for t
in allowedTypes
if t
.title
in self
.localContentTypes
]
104 security
.declareProtected(View
, 'objectIdCanBeDeleted')
105 def objectIdCanBeDeleted(self
, id) :
106 """ Check permissions and ownership and return True
107 if current user can delete object id.
109 if _checkPermission(DeleteObjects
, self
) : # std zope perm
112 elif _checkPermission(DeletePortalContents
, self
):
113 mtool
= getToolByName(self
, 'portal_membership')
114 authMember
= mtool
.getAuthenticatedMember()
115 ob
= getattr(self
, id)
116 if authMember
.allowed(ob
, object_roles
=['Owner'] ) and \
117 _checkPermission(DeleteOwnedObjects
, ob
) : return True
123 security
.declareProtected(DeletePortalContents
, 'manage_delObjects')
124 def manage_delObjects(self
, ids
=[], REQUEST
=None):
125 """Delete subordinate objects.
126 A member can delete his owned contents (if he has the 'Delete Portal Contents' permission)
127 without 'Delete objects' permission in this folder.
128 Return skipped object ids.
131 if _checkPermission(DeleteObjects
, self
) : # std zope perm
132 PortalFolder
.manage_delObjects(self
, ids
=ids
, REQUEST
=REQUEST
)
134 mtool
= getToolByName(self
, 'portal_membership')
135 authMember
= mtool
.getAuthenticatedMember()
137 if type(ids
) == StringType
:
141 if authMember
.allowed(ob
, object_roles
=['Owner'] ) and \
142 _checkPermission(DeleteOwnedObjects
, ob
) : owned
.append(id)
143 else : notOwned
.append(id)
145 PortalFolder
.manage_delObjects(self
, ids
=owned
, REQUEST
=REQUEST
)
147 if REQUEST
is not None:
148 return self
.manage_main(
150 manage_tabs_message
='Object(s) deleted.',
155 security
.declareProtected(AddPortalContent
, 'manage_renameObjects')
156 def manage_renameObjects(self
, ids
=[], new_ids
=[], REQUEST
=None) :
157 """ Rename subordinate objects
158 A member can rename his owned contents if he has the 'Modify Portal Content' permission.
159 Returns skippend object ids.
161 if len(ids
) != len(new_ids
):
162 raise BadRequest(_('Please rename each listed object.'))
164 if _checkPermission(ViewManagementScreens
, self
) : # std zope perm
165 return super(PlinnFolder
, self
).manage_renameObjects(ids
, new_ids
, REQUEST
)
167 mtool
= getToolByName(self
, 'portal_membership')
168 authMember
= mtool
.getAuthenticatedMember()
170 for id, new_id
in zip(ids
, new_ids
) :
171 if id == new_id
: continue
174 if authMember
.allowed(ob
, object_roles
=['Owner'] ) and \
175 _checkPermission(ModifyPortalContent
, ob
) :
176 self
.manage_renameObject(id, new_id
)
180 if REQUEST
is not None :
181 return self
.manage_main(self
, REQUEST
, update_menu
=1)
186 security
.declareProtected(ListFolderContents
, 'listFolderContents')
187 def listFolderContents( self
, contentFilter
=None ):
188 """ List viewable contentish and folderish sub-objects.
190 items
= self
.contentItems(filter=contentFilter
)
192 for id, obj
in items
:
193 if _checkPermission(View
, obj
) :
199 security
.declareProtected(ListFolderContents
, 'listNearestFolderContents')
200 def listNearestFolderContents(self
, contentFilter
=None, userid
=None, sorted=False) :
201 """ Return folder contents and traverse
202 recursively unaccessfull sub folders to find
208 filt
= contentFilter
.copy()
209 ctool
= getToolByName(self
, 'portal_catalog')
210 mtool
= getToolByName(self
, 'portal_membership')
212 if userid
and _checkPermission(CheckMemberPermission
, getToolByName(self
, 'portal_url').getPortalObject()) :
213 checkFunc
= lambda perm
, ob
: _checkMemberPermission(userid
, View
, ob
)
214 filt
['allowedRolesAndUsers'] = ctool
._listAllowedRolesAndUsers
( mtool
.getMemberById(userid
) )
216 checkFunc
= _checkPermission
217 filt
['allowedRolesAndUsers'] = ctool
._listAllowedRolesAndUsers
( mtool
.getAuthenticatedMember() )
220 # copy from CMFCore.PortalFolder.PortalFolder._filteredItems
221 pt
= filt
.get('portal_type', [])
222 if type(pt
) is type(''):
224 types_tool
= getToolByName(self
, 'portal_types')
225 allowed_types
= types_tool
.listContentTypes()
229 pt
= [t
for t
in pt
if t
in allowed_types
]
231 # After filtering, no types remain, so nothing should be
234 filt
['portal_type'] = pt
237 query
= ContentFilter(**filt
)
240 for o
in self
.objectValues() :
242 if checkFunc(View
, o
):
243 nearestObjects
.append(o
)
244 elif getattr(o
.aq_self
,'isAnObjectManager', False):
245 nearestObjects
.extend(_getDeepObjects(self
, ctool
, o
, filter=filt
))
247 if sorted and len(nearestObjects
) > 0 :
248 key
, reverse
= self
.getDefaultSorting()
249 if key
!= 'position' :
250 indexCallable
= callable(getattr(nearestObjects
[0], key
))
252 sortfunc
= lambda a
, b
: cmp(getattr(a
, key
)(), getattr(b
, key
)())
254 sortfunc
= lambda a
, b
: cmp(getattr(a
, key
), getattr(b
, key
))
255 nearestObjects
.sort(cmp=sortfunc
, reverse
=reverse
)
257 return nearestObjects
259 security
.declareProtected(ListFolderContents
, 'listCatalogedContents')
260 def listCatalogedContents(self
, contentFilter
={}):
261 """ query catalog and returns brains of contents.
262 Requires ExtendedPathIndex
264 ctool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.ICatalogTool')
265 contentFilter
['path'] = {'query':'/'.join(self
.getPhysicalPath()),
267 return ctool(sort_on
='position', **contentFilter
)
269 security
.declarePublic('synContentValues')
270 def synContentValues(self
):
271 # value for syndication
272 return self
.listNearestFolderContents()
274 security
.declareProtected(View
, 'SearchableText')
275 def SearchableText(self
) :
276 """ for full text indexation
278 return '%s %s' % (self
.title
, self
.description
)
280 security
.declareProtected(AddPortalFolders
, 'manage_addPlinnFolder')
281 def manage_addPlinnFolder(self
, id, title
='', REQUEST
=None):
282 """Add a new PortalFolder object with id *id*.
284 ob
=PlinnFolder(id, title
)
285 # from CMFCore.PortalFolder.PortalFolder :-)
286 self
._setObject
(id, ob
)
287 if REQUEST
is not None:
288 return self
.folder_contents( # XXX: ick!
289 self
, REQUEST
, portal_status_message
="Folder added")
292 security
.declareProtected(AddPortalContent
, 'put_upload')
293 def put_upload(self
, REQUEST
, RESPONSE
):
294 """ Upload a content thru webdav put method.
295 The default behavior (NullRessource.PUT + PortalFolder.PUT_factory)
296 disallow files names with '_' at the begining.
299 self
.dav__init(REQUEST
, RESPONSE
)
301 fileName
= REQUEST
.getHeader('X-File-Name', '')
302 validId
= makeValidId(self
, fileName
, allow_dup
=True)
304 ifhdr
= REQUEST
.get_header('If', '')
305 if self
.wl_isLocked():
307 self
.dav__simpleifhandler(REQUEST
, RESPONSE
, col
=1)
311 raise PreconditionFailed
313 if int(REQUEST
.get('CONTENT_LENGTH') or 0) > LARGE_FILE_THRESHOLD
:
314 file = REQUEST
['BODYFILE']
315 body
= file.read(LARGE_FILE_THRESHOLD
)
318 body
= REQUEST
.get('BODY', '')
320 typ
=REQUEST
.get_header('content-type', None)
322 typ
, enc
=guess_content_type(validId
, body
)
324 if self
.checkIdAvailable(validId
) :
325 ob
= self
.PUT_factory(validId
, typ
, body
)
326 self
._setObject
(validId
, ob
)
327 ob
= self
._getOb
(validId
)
329 ob
= self
._getOb
(validId
)
331 # We call _verifyObjectPaste with verify_src=0, to see if the
332 # user can create this type of object (and we don't need to
333 # check the clipboard.
335 self
._verifyObjectPaste
(ob
.__of
__(self
), 0)
337 sMsg
= 'Unable to create object of class %s in %s: %s' % \
338 (ob
.__class
__, repr(self
), sys
.exc_info()[1],)
339 raise Unauthorized
, sMsg
341 ob
.PUT(REQUEST
, RESPONSE
)
342 ob
.orig_name
= fileName
344 RESPONSE
.setStatus(201)
349 # ## overload to maintain ownership if authenticated user has 'Manage portal' permission
350 # def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
351 # """Paste previously copied objects into the current object.
353 # If calling manage_pasteObjects from python code, pass the result of a
354 # previous call to manage_cutObjects or manage_copyObjects as the first
357 # Also sends IObjectCopiedEvent and IObjectClonedEvent
358 # or IObjectWillBeMovedEvent and IObjectMovedEvent.
360 # if cb_copy_data is not None:
362 # elif REQUEST is not None and REQUEST.has_key('__cp'):
363 # cp = REQUEST['__cp']
367 # raise CopyError, eNoData
370 # op, mdatas = _cb_decode(cp)
372 # raise CopyError, eInvalid
375 # app = self.getPhysicalRoot()
376 # for mdata in mdatas:
377 # m = Moniker.loadMoniker(mdata)
380 # except ConflictError:
383 # raise CopyError, eNotFound
384 # self._verifyObjectPaste(ob, validate_src=op+1)
390 # mtool = getToolByName(self, 'portal_membership')
391 # utool = getToolByName(self, 'portal_url')
392 # portal = utool.getPortalObject()
393 # userIsPortalManager = mtool.checkPermission(ManagePortal, portal)
396 # orig_id = ob.getId()
397 # if not ob.cb_isCopyable():
398 # raise CopyError, eNotSupported % escape(orig_id)
401 # ob._notifyOfCopyTo(self, op=0)
402 # except ConflictError:
405 # raise CopyError, MessageDialog(
406 # title="Copy Error",
407 # message=sys.exc_info()[1],
408 # action='manage_main')
410 # id = self._get_id(orig_id)
411 # result.append({'id': orig_id, 'new_id': id})
414 # ob = ob._getCopy(self)
416 # notify(ObjectCopiedEvent(ob, orig_ob))
418 # if not userIsPortalManager :
419 # self._setObject(id, ob, suppress_events=True)
421 # self._setObject(id, ob, suppress_events=True, set_owner=0)
422 # ob = self._getOb(id)
425 # ob._postCopy(self, op=0)
427 # OFS.subscribers.compatibilityCall('manage_afterClone', ob, ob)
429 # notify(ObjectClonedEvent(ob))
431 # if REQUEST is not None:
432 # return self.manage_main(self, REQUEST, update_menu=1,
438 # orig_id = ob.getId()
439 # if not ob.cb_isMoveable():
440 # raise CopyError, eNotSupported % escape(orig_id)
443 # ob._notifyOfCopyTo(self, op=1)
444 # except ConflictError:
447 # raise CopyError, MessageDialog(
448 # title="Move Error",
449 # message=sys.exc_info()[1],
450 # action='manage_main')
452 # if not sanity_check(self, ob):
453 # raise CopyError, "This object cannot be pasted into itself"
455 # orig_container = aq_parent(aq_inner(ob))
456 # if aq_base(orig_container) is aq_base(self):
459 # id = self._get_id(orig_id)
460 # result.append({'id': orig_id, 'new_id': id})
462 # notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
465 # # try to make ownership explicit so that it gets carried
466 # # along to the new location if needed.
467 # ob.manage_changeOwnershipType(explicit=1)
470 # orig_container._delObject(orig_id, suppress_events=True)
472 # orig_container._delObject(orig_id)
474 # "%s._delObject without suppress_events is discouraged."
475 # % orig_container.__class__.__name__,
476 # DeprecationWarning)
481 # self._setObject(id, ob, set_owner=0, suppress_events=True)
483 # self._setObject(id, ob, set_owner=0)
485 # "%s._setObject without suppress_events is discouraged."
486 # % self.__class__.__name__, DeprecationWarning)
487 # ob = self._getOb(id)
489 # notify(ObjectMovedEvent(ob, orig_container, orig_id, self, id))
490 # notifyContainerModified(orig_container)
491 # if aq_base(orig_container) is not aq_base(self):
492 # notifyContainerModified(self)
494 # ob._postCopy(self, op=1)
495 # # try to make ownership implicit if possible
496 # ob.manage_changeOwnershipType(explicit=0)
498 # if REQUEST is not None:
499 # REQUEST['RESPONSE'].setCookie('__cp', 'deleted',
500 # path='%s' % cookie_path(REQUEST),
501 # expires='Wed, 31-Dec-97 23:59:59 GMT')
502 # REQUEST['__cp'] = None
503 # return self.manage_main(self, REQUEST, update_menu=1,
509 InitializeClass(PlinnFolder
)
510 PlinnFolderFactory
= Factory(PlinnFolder
)
512 def _getDeepObjects(self
, ctool
, o
, filter={}):
513 res
= ctool
.unrestrictedSearchResults(path
= '/'.join(o
.getPhysicalPath()), **filter)
520 res
.sort(lambda a
, b
: cmp(a
.getPath(), b
.getPath()))
521 previousPath
= res
[0].getPath()
523 deepObjects
.append(res
[0].getObject())
525 currentPath
= b
.getPath()
526 if currentPath
.startswith(previousPath
) and len(currentPath
) > len(previousPath
):
529 deepObjects
.append(b
.getObject())
530 previousPath
= currentPath
535 manage_addPlinnFolder
= PlinnFolder
.manage_addPlinnFolder
.im_func