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
) :
326 ob
= self
.PUT_factory(validId
, typ
, body
)
327 self
._setObject
(validId
, ob
)
328 ob
= self
._getOb
(validId
)
329 if IDublinCore
.providedBy(ob
) :
330 ob
.editMetadata(title
=fileName
,
335 ob
= self
._getOb
(validId
)
337 # We call _verifyObjectPaste with verify_src=0, to see if the
338 # user can create this type of object (and we don't need to
339 # check the clipboard.
341 self
._verifyObjectPaste
(ob
.__of
__(self
), 0)
343 sMsg
= 'Unable to create object of class %s in %s: %s' % \
344 (ob
.__class
__, repr(self
), sys
.exc_info()[1],)
345 raise Unauthorized
, sMsg
347 ob
.PUT(REQUEST
, RESPONSE
)
348 ob
.orig_name
= fileName
350 # get method from ob created / refreshed
351 ti
= ob
.getTypeInfo()
352 method_id
= ti
.queryMethodID('jsupload_snippet')
353 meth
= getattr(ob
, method_id
) if method_id
else None
355 # get method from container that receive uploaded content
356 ti
= self
.getTypeInfo()
357 method_id
= ti
.queryMethodID('jsupload_snippet')
358 meth
= getattr(self
, method_id
) if method_id
else lambda : 'Not implemented'
360 RESPONSE
.setStatus(httpRespCode
)
361 RESPONSE
.setHeader('Content-Type', 'text/xml;;charset=utf-8')
362 return '<fragment>%s</fragment>' % meth(ob
).strip()
365 # ## overload to maintain ownership if authenticated user has 'Manage portal' permission
366 # def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
367 # """Paste previously copied objects into the current object.
369 # If calling manage_pasteObjects from python code, pass the result of a
370 # previous call to manage_cutObjects or manage_copyObjects as the first
373 # Also sends IObjectCopiedEvent and IObjectClonedEvent
374 # or IObjectWillBeMovedEvent and IObjectMovedEvent.
376 # if cb_copy_data is not None:
378 # elif REQUEST is not None and REQUEST.has_key('__cp'):
379 # cp = REQUEST['__cp']
383 # raise CopyError, eNoData
386 # op, mdatas = _cb_decode(cp)
388 # raise CopyError, eInvalid
391 # app = self.getPhysicalRoot()
392 # for mdata in mdatas:
393 # m = Moniker.loadMoniker(mdata)
396 # except ConflictError:
399 # raise CopyError, eNotFound
400 # self._verifyObjectPaste(ob, validate_src=op+1)
406 # mtool = getToolByName(self, 'portal_membership')
407 # utool = getToolByName(self, 'portal_url')
408 # portal = utool.getPortalObject()
409 # userIsPortalManager = mtool.checkPermission(ManagePortal, portal)
412 # orig_id = ob.getId()
413 # if not ob.cb_isCopyable():
414 # raise CopyError, eNotSupported % escape(orig_id)
417 # ob._notifyOfCopyTo(self, op=0)
418 # except ConflictError:
421 # raise CopyError, MessageDialog(
422 # title="Copy Error",
423 # message=sys.exc_info()[1],
424 # action='manage_main')
426 # id = self._get_id(orig_id)
427 # result.append({'id': orig_id, 'new_id': id})
430 # ob = ob._getCopy(self)
432 # notify(ObjectCopiedEvent(ob, orig_ob))
434 # if not userIsPortalManager :
435 # self._setObject(id, ob, suppress_events=True)
437 # self._setObject(id, ob, suppress_events=True, set_owner=0)
438 # ob = self._getOb(id)
441 # ob._postCopy(self, op=0)
443 # OFS.subscribers.compatibilityCall('manage_afterClone', ob, ob)
445 # notify(ObjectClonedEvent(ob))
447 # if REQUEST is not None:
448 # return self.manage_main(self, REQUEST, update_menu=1,
454 # orig_id = ob.getId()
455 # if not ob.cb_isMoveable():
456 # raise CopyError, eNotSupported % escape(orig_id)
459 # ob._notifyOfCopyTo(self, op=1)
460 # except ConflictError:
463 # raise CopyError, MessageDialog(
464 # title="Move Error",
465 # message=sys.exc_info()[1],
466 # action='manage_main')
468 # if not sanity_check(self, ob):
469 # raise CopyError, "This object cannot be pasted into itself"
471 # orig_container = aq_parent(aq_inner(ob))
472 # if aq_base(orig_container) is aq_base(self):
475 # id = self._get_id(orig_id)
476 # result.append({'id': orig_id, 'new_id': id})
478 # notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
481 # # try to make ownership explicit so that it gets carried
482 # # along to the new location if needed.
483 # ob.manage_changeOwnershipType(explicit=1)
486 # orig_container._delObject(orig_id, suppress_events=True)
488 # orig_container._delObject(orig_id)
490 # "%s._delObject without suppress_events is discouraged."
491 # % orig_container.__class__.__name__,
492 # DeprecationWarning)
497 # self._setObject(id, ob, set_owner=0, suppress_events=True)
499 # self._setObject(id, ob, set_owner=0)
501 # "%s._setObject without suppress_events is discouraged."
502 # % self.__class__.__name__, DeprecationWarning)
503 # ob = self._getOb(id)
505 # notify(ObjectMovedEvent(ob, orig_container, orig_id, self, id))
506 # notifyContainerModified(orig_container)
507 # if aq_base(orig_container) is not aq_base(self):
508 # notifyContainerModified(self)
510 # ob._postCopy(self, op=1)
511 # # try to make ownership implicit if possible
512 # ob.manage_changeOwnershipType(explicit=0)
514 # if REQUEST is not None:
515 # REQUEST['RESPONSE'].setCookie('__cp', 'deleted',
516 # path='%s' % cookie_path(REQUEST),
517 # expires='Wed, 31-Dec-97 23:59:59 GMT')
518 # REQUEST['__cp'] = None
519 # return self.manage_main(self, REQUEST, update_menu=1,
525 InitializeClass(PlinnFolder
)
526 PlinnFolderFactory
= Factory(PlinnFolder
)
528 def _getDeepObjects(self
, ctool
, o
, filter={}):
529 res
= ctool
.unrestrictedSearchResults(path
= '/'.join(o
.getPhysicalPath()), **filter)
536 res
.sort(lambda a
, b
: cmp(a
.getPath(), b
.getPath()))
537 previousPath
= res
[0].getPath()
539 deepObjects
.append(res
[0].getObject())
541 currentPath
= b
.getPath()
542 if currentPath
.startswith(previousPath
) and len(currentPath
) > len(previousPath
):
545 deepObjects
.append(b
.getObject())
546 previousPath
= currentPath
551 manage_addPlinnFolder
= PlinnFolder
.manage_addPlinnFolder
.im_func