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 urllib
import unquote
35 from OFS
import Moniker
36 from ZODB
.POSException
import ConflictError
37 import OFS
.subscribers
38 from zope
.event
import notify
39 from zope
.lifecycleevent
import ObjectCopiedEvent
41 from zope
.app
.container
.contained
import notifyContainerModified
42 from zope
.app
.container
.contained
import ObjectMovedEvent
45 from zope
.container
.contained
import notifyContainerModified
46 from zope
.container
.contained
import ObjectMovedEvent
47 from OFS
.event
import ObjectClonedEvent
48 from OFS
.event
import ObjectWillBeMovedEvent
49 from zope
.component
.factory
import Factory
50 from Acquisition
import aq_base
, aq_inner
, aq_parent
52 from types
import StringType
, NoneType
53 from Products
.CMFCore
.permissions
import ListFolderContents
, View
, ViewManagementScreens
,\
54 ManageProperties
, AddPortalFolders
, AddPortalContent
,\
55 ManagePortal
, ModifyPortalContent
56 from permissions
import DeletePortalContents
, DeleteObjects
, DeleteOwnedObjects
, SetLocalRoles
, CheckMemberPermission
57 from Products
.CMFCore
.utils
import _checkPermission
, getToolByName
58 from Products
.CMFCore
.utils
import getUtilityByInterfaceName
59 from Products
.CMFCore
.CMFCatalogAware
import CMFCatalogAware
60 from Products
.CMFCore
.PortalFolder
import PortalFolder
, ContentFilter
61 from Products
.CMFCore
.interfaces
import IDublinCore
62 from Products
.CMFDefault
.DublinCore
import DefaultDublinCoreImpl
64 from zope
.interface
import implements
65 from Products
.CMFCore
.interfaces
import IContentish
67 from utils
import _checkMemberPermission
68 from utils
import Message
as _
69 from utils
import makeValidId
70 from Globals
import InitializeClass
71 from AccessControl
import ClassSecurityInfo
72 from ZServer
import LARGE_FILE_THRESHOLD
73 from webdav
.interfaces
import IWriteLock
74 from webdav
.common
import Locked
75 from webdav
.common
import PreconditionFailed
76 from zope
.contenttype
import guess_content_type
79 class PlinnFolder(CMFCatalogAware
, PortalFolder
, DefaultDublinCoreImpl
) :
82 implements(IContentish
)
84 security
= ClassSecurityInfo()
86 manage_options
= PortalFolder
.manage_options
88 ## change security for inherited methods
89 security
.declareProtected(AddPortalContent
, 'manage_pasteObjects')
91 def __init__( self
, id, title
='' ) :
92 PortalFolder
.__init
__(self
, id)
93 DefaultDublinCoreImpl
.__init
__(self
, title
= title
)
95 security
.declarePublic('allowedContentTypes')
96 def allowedContentTypes(self
):
98 List type info objects for types which can be added in this folder.
99 Types can be filtered using the localContentTypes attribute.
101 allowedTypes
= PortalFolder
.allowedContentTypes(self
)
102 if hasattr(self
, 'localContentTypes'):
103 allowedTypes
= [t
for t
in allowedTypes
if t
.title
in self
.localContentTypes
]
106 security
.declareProtected(View
, 'objectIdCanBeDeleted')
107 def objectIdCanBeDeleted(self
, id) :
108 """ Check permissions and ownership and return True
109 if current user can delete object id.
111 if _checkPermission(DeleteObjects
, self
) : # std zope perm
114 elif _checkPermission(DeletePortalContents
, self
):
115 mtool
= getToolByName(self
, 'portal_membership')
116 authMember
= mtool
.getAuthenticatedMember()
117 ob
= getattr(self
, id)
118 if authMember
.allowed(ob
, object_roles
=['Owner'] ) and \
119 _checkPermission(DeleteOwnedObjects
, ob
) : return True
125 security
.declareProtected(DeletePortalContents
, 'manage_delObjects')
126 def manage_delObjects(self
, ids
=[], REQUEST
=None):
127 """Delete subordinate objects.
128 A member can delete his owned contents (if he has the 'Delete Portal Contents' permission)
129 without 'Delete objects' permission in this folder.
130 Return skipped object ids.
133 if _checkPermission(DeleteObjects
, self
) : # std zope perm
134 PortalFolder
.manage_delObjects(self
, ids
=ids
, REQUEST
=REQUEST
)
136 mtool
= getToolByName(self
, 'portal_membership')
137 authMember
= mtool
.getAuthenticatedMember()
139 if type(ids
) == StringType
:
143 if authMember
.allowed(ob
, object_roles
=['Owner'] ) and \
144 _checkPermission(DeleteOwnedObjects
, ob
) : owned
.append(id)
145 else : notOwned
.append(id)
147 PortalFolder
.manage_delObjects(self
, ids
=owned
, REQUEST
=REQUEST
)
149 if REQUEST
is not None:
150 return self
.manage_main(
152 manage_tabs_message
='Object(s) deleted.',
157 security
.declareProtected(AddPortalContent
, 'manage_renameObjects')
158 def manage_renameObjects(self
, ids
=[], new_ids
=[], REQUEST
=None) :
159 """ Rename subordinate objects
160 A member can rename his owned contents if he has the 'Modify Portal Content' permission.
161 Returns skippend object ids.
163 if len(ids
) != len(new_ids
):
164 raise BadRequest(_('Please rename each listed object.'))
166 if _checkPermission(ViewManagementScreens
, self
) : # std zope perm
167 return super(PlinnFolder
, self
).manage_renameObjects(ids
, new_ids
, REQUEST
)
169 mtool
= getToolByName(self
, 'portal_membership')
170 authMember
= mtool
.getAuthenticatedMember()
172 for id, new_id
in zip(ids
, new_ids
) :
173 if id == new_id
: continue
176 if authMember
.allowed(ob
, object_roles
=['Owner'] ) and \
177 _checkPermission(ModifyPortalContent
, ob
) :
178 self
.manage_renameObject(id, new_id
)
182 if REQUEST
is not None :
183 return self
.manage_main(self
, REQUEST
, update_menu
=1)
188 security
.declareProtected(ListFolderContents
, 'listFolderContents')
189 def listFolderContents( self
, contentFilter
=None ):
190 """ List viewable contentish and folderish sub-objects.
192 items
= self
.contentItems(filter=contentFilter
)
194 for id, obj
in items
:
195 if _checkPermission(View
, obj
) :
201 security
.declareProtected(ListFolderContents
, 'listNearestFolderContents')
202 def listNearestFolderContents(self
, contentFilter
=None, userid
=None, sorted=False) :
203 """ Return folder contents and traverse
204 recursively unaccessfull sub folders to find
210 filt
= contentFilter
.copy()
211 ctool
= getToolByName(self
, 'portal_catalog')
212 mtool
= getToolByName(self
, 'portal_membership')
214 if userid
and _checkPermission(CheckMemberPermission
, getToolByName(self
, 'portal_url').getPortalObject()) :
215 checkFunc
= lambda perm
, ob
: _checkMemberPermission(userid
, View
, ob
)
216 filt
['allowedRolesAndUsers'] = ctool
._listAllowedRolesAndUsers
( mtool
.getMemberById(userid
) )
218 checkFunc
= _checkPermission
219 filt
['allowedRolesAndUsers'] = ctool
._listAllowedRolesAndUsers
( mtool
.getAuthenticatedMember() )
222 # copy from CMFCore.PortalFolder.PortalFolder._filteredItems
223 pt
= filt
.get('portal_type', [])
224 if type(pt
) is type(''):
226 types_tool
= getToolByName(self
, 'portal_types')
227 allowed_types
= types_tool
.listContentTypes()
231 pt
= [t
for t
in pt
if t
in allowed_types
]
233 # After filtering, no types remain, so nothing should be
236 filt
['portal_type'] = pt
239 query
= ContentFilter(**filt
)
242 for o
in self
.objectValues() :
244 if checkFunc(View
, o
):
245 nearestObjects
.append(o
)
246 elif getattr(o
.aq_self
,'isAnObjectManager', False):
247 nearestObjects
.extend(_getDeepObjects(self
, ctool
, o
, filter=filt
))
249 if sorted and len(nearestObjects
) > 0 :
250 key
, reverse
= self
.getDefaultSorting()
251 if key
!= 'position' :
252 indexCallable
= callable(getattr(nearestObjects
[0], key
))
254 sortfunc
= lambda a
, b
: cmp(getattr(a
, key
)(), getattr(b
, key
)())
256 sortfunc
= lambda a
, b
: cmp(getattr(a
, key
), getattr(b
, key
))
257 nearestObjects
.sort(cmp=sortfunc
, reverse
=reverse
)
259 return nearestObjects
261 security
.declareProtected(ListFolderContents
, 'listCatalogedContents')
262 def listCatalogedContents(self
, contentFilter
={}):
263 """ query catalog and returns brains of contents.
264 Requires ExtendedPathIndex
266 ctool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.ICatalogTool')
267 contentFilter
['path'] = {'query':'/'.join(self
.getPhysicalPath()),
269 if not contentFilter
.has_key('sort_on') :
270 contentFilter
['sort_index'] = 'position'
271 return ctool(**contentFilter
)
273 security
.declarePublic('synContentValues')
274 def synContentValues(self
):
275 # value for syndication
276 return self
.listNearestFolderContents()
278 security
.declareProtected(View
, 'SearchableText')
279 def SearchableText(self
) :
280 """ for full text indexation
282 return '%s %s' % (self
.title
, self
.description
)
284 security
.declareProtected(AddPortalFolders
, 'manage_addPlinnFolder')
285 def manage_addPlinnFolder(self
, id, title
='', REQUEST
=None):
286 """Add a new PortalFolder object with id *id*.
288 ob
=PlinnFolder(id, title
)
289 # from CMFCore.PortalFolder.PortalFolder :-)
290 self
._setObject
(id, ob
)
291 if REQUEST
is not None:
292 return self
.folder_contents( # XXX: ick!
293 self
, REQUEST
, portal_status_message
="Folder added")
296 security
.declareProtected(AddPortalContent
, 'put_upload')
297 def put_upload(self
, REQUEST
, RESPONSE
):
298 """ Upload a content thru webdav put method.
299 The default behavior (NullRessource.PUT + PortalFolder.PUT_factory)
300 disallow files names with '_' at the begining.
303 self
.dav__init(REQUEST
, RESPONSE
)
304 fileName
= unquote(REQUEST
.getHeader('X-File-Name', ''))
305 validId
= makeValidId(self
, fileName
, allow_dup
=True)
307 ifhdr
= REQUEST
.get_header('If', '')
308 if self
.wl_isLocked():
310 self
.dav__simpleifhandler(REQUEST
, RESPONSE
, col
=1)
314 raise PreconditionFailed
316 if int(REQUEST
.get('CONTENT_LENGTH') or 0) > LARGE_FILE_THRESHOLD
:
317 file = REQUEST
['BODYFILE']
318 body
= file.read(LARGE_FILE_THRESHOLD
)
321 body
= REQUEST
.get('BODY', '')
323 typ
=REQUEST
.get_header('content-type', None)
325 typ
, enc
=guess_content_type(validId
, body
)
327 if self
.checkIdAvailable(validId
) :
329 ob
= self
.PUT_factory(validId
, typ
, body
)
330 self
._setObject
(validId
, ob
)
331 ob
= self
._getOb
(validId
)
332 except ValueError : # maybe "Disallowed subobject type". Fallback to file type.
333 validId
= self
.invokeFactory('File', validId
)
334 ob
= self
._getOb
(validId
)
335 if IDublinCore
.providedBy(ob
) :
336 ob
.editMetadata(title
=fileName
,
341 ob
= self
._getOb
(validId
)
343 # We call _verifyObjectPaste with verify_src=0, to see if the
344 # user can create this type of object (and we don't need to
345 # check the clipboard.
347 self
._verifyObjectPaste
(ob
.__of
__(self
), 0)
349 sMsg
= 'Unable to create object of class %s in %s: %s' % \
350 (ob
.__class
__, repr(self
), sys
.exc_info()[1],)
351 raise Unauthorized
, sMsg
353 ob
.PUT(REQUEST
, RESPONSE
)
354 ob
.orig_name
= fileName
356 # get method from ob created / refreshed
357 ti
= ob
.getTypeInfo()
358 method_id
= ti
.queryMethodID('jsupload_snippet')
359 meth
= getattr(ob
, method_id
) if method_id
else None
361 # get method from container that receive uploaded content
362 ti
= self
.getTypeInfo()
363 method_id
= ti
.queryMethodID('jsupload_snippet')
364 meth
= getattr(self
, method_id
) if method_id
else lambda ob
: 'Not implemented'
366 RESPONSE
.setStatus(httpRespCode
)
367 RESPONSE
.setHeader('Content-Type', 'text/xml;;charset=utf-8')
368 return '<fragment>%s</fragment>' % meth(ob
).strip()
371 # ## overload to maintain ownership if authenticated user has 'Manage portal' permission
372 # def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
373 # """Paste previously copied objects into the current object.
375 # If calling manage_pasteObjects from python code, pass the result of a
376 # previous call to manage_cutObjects or manage_copyObjects as the first
379 # Also sends IObjectCopiedEvent and IObjectClonedEvent
380 # or IObjectWillBeMovedEvent and IObjectMovedEvent.
382 # if cb_copy_data is not None:
384 # elif REQUEST is not None and REQUEST.has_key('__cp'):
385 # cp = REQUEST['__cp']
389 # raise CopyError, eNoData
392 # op, mdatas = _cb_decode(cp)
394 # raise CopyError, eInvalid
397 # app = self.getPhysicalRoot()
398 # for mdata in mdatas:
399 # m = Moniker.loadMoniker(mdata)
402 # except ConflictError:
405 # raise CopyError, eNotFound
406 # self._verifyObjectPaste(ob, validate_src=op+1)
412 # mtool = getToolByName(self, 'portal_membership')
413 # utool = getToolByName(self, 'portal_url')
414 # portal = utool.getPortalObject()
415 # userIsPortalManager = mtool.checkPermission(ManagePortal, portal)
418 # orig_id = ob.getId()
419 # if not ob.cb_isCopyable():
420 # raise CopyError, eNotSupported % escape(orig_id)
423 # ob._notifyOfCopyTo(self, op=0)
424 # except ConflictError:
427 # raise CopyError, MessageDialog(
428 # title="Copy Error",
429 # message=sys.exc_info()[1],
430 # action='manage_main')
432 # id = self._get_id(orig_id)
433 # result.append({'id': orig_id, 'new_id': id})
436 # ob = ob._getCopy(self)
438 # notify(ObjectCopiedEvent(ob, orig_ob))
440 # if not userIsPortalManager :
441 # self._setObject(id, ob, suppress_events=True)
443 # self._setObject(id, ob, suppress_events=True, set_owner=0)
444 # ob = self._getOb(id)
447 # ob._postCopy(self, op=0)
449 # OFS.subscribers.compatibilityCall('manage_afterClone', ob, ob)
451 # notify(ObjectClonedEvent(ob))
453 # if REQUEST is not None:
454 # return self.manage_main(self, REQUEST, update_menu=1,
460 # orig_id = ob.getId()
461 # if not ob.cb_isMoveable():
462 # raise CopyError, eNotSupported % escape(orig_id)
465 # ob._notifyOfCopyTo(self, op=1)
466 # except ConflictError:
469 # raise CopyError, MessageDialog(
470 # title="Move Error",
471 # message=sys.exc_info()[1],
472 # action='manage_main')
474 # if not sanity_check(self, ob):
475 # raise CopyError, "This object cannot be pasted into itself"
477 # orig_container = aq_parent(aq_inner(ob))
478 # if aq_base(orig_container) is aq_base(self):
481 # id = self._get_id(orig_id)
482 # result.append({'id': orig_id, 'new_id': id})
484 # notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
487 # # try to make ownership explicit so that it gets carried
488 # # along to the new location if needed.
489 # ob.manage_changeOwnershipType(explicit=1)
492 # orig_container._delObject(orig_id, suppress_events=True)
494 # orig_container._delObject(orig_id)
496 # "%s._delObject without suppress_events is discouraged."
497 # % orig_container.__class__.__name__,
498 # DeprecationWarning)
503 # self._setObject(id, ob, set_owner=0, suppress_events=True)
505 # self._setObject(id, ob, set_owner=0)
507 # "%s._setObject without suppress_events is discouraged."
508 # % self.__class__.__name__, DeprecationWarning)
509 # ob = self._getOb(id)
511 # notify(ObjectMovedEvent(ob, orig_container, orig_id, self, id))
512 # notifyContainerModified(orig_container)
513 # if aq_base(orig_container) is not aq_base(self):
514 # notifyContainerModified(self)
516 # ob._postCopy(self, op=1)
517 # # try to make ownership implicit if possible
518 # ob.manage_changeOwnershipType(explicit=0)
520 # if REQUEST is not None:
521 # REQUEST['RESPONSE'].setCookie('__cp', 'deleted',
522 # path='%s' % cookie_path(REQUEST),
523 # expires='Wed, 31-Dec-97 23:59:59 GMT')
524 # REQUEST['__cp'] = None
525 # return self.manage_main(self, REQUEST, update_menu=1,
531 InitializeClass(PlinnFolder
)
532 PlinnFolderFactory
= Factory(PlinnFolder
)
534 def _getDeepObjects(self
, ctool
, o
, filter={}):
535 res
= ctool
.unrestrictedSearchResults(path
= '/'.join(o
.getPhysicalPath()), **filter)
542 res
.sort(lambda a
, b
: cmp(a
.getPath(), b
.getPath()))
543 previousPath
= res
[0].getPath()
545 deepObjects
.append(res
[0].getObject())
547 currentPath
= b
.getPath()
548 if currentPath
.startswith(previousPath
) and len(currentPath
) > len(previousPath
):
551 deepObjects
.append(b
.getObject())
552 previousPath
= currentPath
557 manage_addPlinnFolder
= PlinnFolder
.manage_addPlinnFolder
.im_func