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 return ctool(sort_on
='position', **contentFilter
)
271 security
.declarePublic('synContentValues')
272 def synContentValues(self
):
273 # value for syndication
274 return self
.listNearestFolderContents()
276 security
.declareProtected(View
, 'SearchableText')
277 def SearchableText(self
) :
278 """ for full text indexation
280 return '%s %s' % (self
.title
, self
.description
)
282 security
.declareProtected(AddPortalFolders
, 'manage_addPlinnFolder')
283 def manage_addPlinnFolder(self
, id, title
='', REQUEST
=None):
284 """Add a new PortalFolder object with id *id*.
286 ob
=PlinnFolder(id, title
)
287 # from CMFCore.PortalFolder.PortalFolder :-)
288 self
._setObject
(id, ob
)
289 if REQUEST
is not None:
290 return self
.folder_contents( # XXX: ick!
291 self
, REQUEST
, portal_status_message
="Folder added")
294 security
.declareProtected(AddPortalContent
, 'put_upload')
295 def put_upload(self
, REQUEST
, RESPONSE
):
296 """ Upload a content thru webdav put method.
297 The default behavior (NullRessource.PUT + PortalFolder.PUT_factory)
298 disallow files names with '_' at the begining.
301 self
.dav__init(REQUEST
, RESPONSE
)
302 fileName
= unquote(REQUEST
.getHeader('X-File-Name', ''))
303 validId
= makeValidId(self
, fileName
, allow_dup
=True)
305 ifhdr
= REQUEST
.get_header('If', '')
306 if self
.wl_isLocked():
308 self
.dav__simpleifhandler(REQUEST
, RESPONSE
, col
=1)
312 raise PreconditionFailed
314 if int(REQUEST
.get('CONTENT_LENGTH') or 0) > LARGE_FILE_THRESHOLD
:
315 file = REQUEST
['BODYFILE']
316 body
= file.read(LARGE_FILE_THRESHOLD
)
319 body
= REQUEST
.get('BODY', '')
321 typ
=REQUEST
.get_header('content-type', None)
323 typ
, enc
=guess_content_type(validId
, body
)
325 if self
.checkIdAvailable(validId
) :
327 ob
= self
.PUT_factory(validId
, typ
, body
)
328 self
._setObject
(validId
, ob
)
329 ob
= self
._getOb
(validId
)
330 except ValueError : # maybe "Disallowed subobject type". Fallback to file type.
331 validId
= self
.invokeFactory('File', validId
)
332 ob
= self
._getOb
(validId
)
333 if IDublinCore
.providedBy(ob
) :
334 ob
.editMetadata(title
=fileName
,
339 ob
= self
._getOb
(validId
)
341 # We call _verifyObjectPaste with verify_src=0, to see if the
342 # user can create this type of object (and we don't need to
343 # check the clipboard.
345 self
._verifyObjectPaste
(ob
.__of
__(self
), 0)
347 sMsg
= 'Unable to create object of class %s in %s: %s' % \
348 (ob
.__class
__, repr(self
), sys
.exc_info()[1],)
349 raise Unauthorized
, sMsg
351 ob
.PUT(REQUEST
, RESPONSE
)
352 ob
.orig_name
= fileName
354 # get method from ob created / refreshed
355 ti
= ob
.getTypeInfo()
356 method_id
= ti
.queryMethodID('jsupload_snippet')
357 meth
= getattr(ob
, method_id
) if method_id
else None
359 # get method from container that receive uploaded content
360 ti
= self
.getTypeInfo()
361 method_id
= ti
.queryMethodID('jsupload_snippet')
362 meth
= getattr(self
, method_id
) if method_id
else lambda ob
: 'Not implemented'
364 RESPONSE
.setStatus(httpRespCode
)
365 RESPONSE
.setHeader('Content-Type', 'text/xml;;charset=utf-8')
366 return '<fragment>%s</fragment>' % meth(ob
).strip()
369 # ## overload to maintain ownership if authenticated user has 'Manage portal' permission
370 # def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
371 # """Paste previously copied objects into the current object.
373 # If calling manage_pasteObjects from python code, pass the result of a
374 # previous call to manage_cutObjects or manage_copyObjects as the first
377 # Also sends IObjectCopiedEvent and IObjectClonedEvent
378 # or IObjectWillBeMovedEvent and IObjectMovedEvent.
380 # if cb_copy_data is not None:
382 # elif REQUEST is not None and REQUEST.has_key('__cp'):
383 # cp = REQUEST['__cp']
387 # raise CopyError, eNoData
390 # op, mdatas = _cb_decode(cp)
392 # raise CopyError, eInvalid
395 # app = self.getPhysicalRoot()
396 # for mdata in mdatas:
397 # m = Moniker.loadMoniker(mdata)
400 # except ConflictError:
403 # raise CopyError, eNotFound
404 # self._verifyObjectPaste(ob, validate_src=op+1)
410 # mtool = getToolByName(self, 'portal_membership')
411 # utool = getToolByName(self, 'portal_url')
412 # portal = utool.getPortalObject()
413 # userIsPortalManager = mtool.checkPermission(ManagePortal, portal)
416 # orig_id = ob.getId()
417 # if not ob.cb_isCopyable():
418 # raise CopyError, eNotSupported % escape(orig_id)
421 # ob._notifyOfCopyTo(self, op=0)
422 # except ConflictError:
425 # raise CopyError, MessageDialog(
426 # title="Copy Error",
427 # message=sys.exc_info()[1],
428 # action='manage_main')
430 # id = self._get_id(orig_id)
431 # result.append({'id': orig_id, 'new_id': id})
434 # ob = ob._getCopy(self)
436 # notify(ObjectCopiedEvent(ob, orig_ob))
438 # if not userIsPortalManager :
439 # self._setObject(id, ob, suppress_events=True)
441 # self._setObject(id, ob, suppress_events=True, set_owner=0)
442 # ob = self._getOb(id)
445 # ob._postCopy(self, op=0)
447 # OFS.subscribers.compatibilityCall('manage_afterClone', ob, ob)
449 # notify(ObjectClonedEvent(ob))
451 # if REQUEST is not None:
452 # return self.manage_main(self, REQUEST, update_menu=1,
458 # orig_id = ob.getId()
459 # if not ob.cb_isMoveable():
460 # raise CopyError, eNotSupported % escape(orig_id)
463 # ob._notifyOfCopyTo(self, op=1)
464 # except ConflictError:
467 # raise CopyError, MessageDialog(
468 # title="Move Error",
469 # message=sys.exc_info()[1],
470 # action='manage_main')
472 # if not sanity_check(self, ob):
473 # raise CopyError, "This object cannot be pasted into itself"
475 # orig_container = aq_parent(aq_inner(ob))
476 # if aq_base(orig_container) is aq_base(self):
479 # id = self._get_id(orig_id)
480 # result.append({'id': orig_id, 'new_id': id})
482 # notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
485 # # try to make ownership explicit so that it gets carried
486 # # along to the new location if needed.
487 # ob.manage_changeOwnershipType(explicit=1)
490 # orig_container._delObject(orig_id, suppress_events=True)
492 # orig_container._delObject(orig_id)
494 # "%s._delObject without suppress_events is discouraged."
495 # % orig_container.__class__.__name__,
496 # DeprecationWarning)
501 # self._setObject(id, ob, set_owner=0, suppress_events=True)
503 # self._setObject(id, ob, set_owner=0)
505 # "%s._setObject without suppress_events is discouraged."
506 # % self.__class__.__name__, DeprecationWarning)
507 # ob = self._getOb(id)
509 # notify(ObjectMovedEvent(ob, orig_container, orig_id, self, id))
510 # notifyContainerModified(orig_container)
511 # if aq_base(orig_container) is not aq_base(self):
512 # notifyContainerModified(self)
514 # ob._postCopy(self, op=1)
515 # # try to make ownership implicit if possible
516 # ob.manage_changeOwnershipType(explicit=0)
518 # if REQUEST is not None:
519 # REQUEST['RESPONSE'].setCookie('__cp', 'deleted',
520 # path='%s' % cookie_path(REQUEST),
521 # expires='Wed, 31-Dec-97 23:59:59 GMT')
522 # REQUEST['__cp'] = None
523 # return self.manage_main(self, REQUEST, update_menu=1,
529 InitializeClass(PlinnFolder
)
530 PlinnFolderFactory
= Factory(PlinnFolder
)
532 def _getDeepObjects(self
, ctool
, o
, filter={}):
533 res
= ctool
.unrestrictedSearchResults(path
= '/'.join(o
.getPhysicalPath()), **filter)
540 res
.sort(lambda a
, b
: cmp(a
.getPath(), b
.getPath()))
541 previousPath
= res
[0].getPath()
543 deepObjects
.append(res
[0].getObject())
545 currentPath
= b
.getPath()
546 if currentPath
.startswith(previousPath
) and len(currentPath
) > len(previousPath
):
549 deepObjects
.append(b
.getObject())
550 previousPath
= currentPath
555 manage_addPlinnFolder
= PlinnFolder
.manage_addPlinnFolder
.im_func