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
.CMFDefault
.DublinCore
import DefaultDublinCoreImpl
63 from zope
.interface
import implements
64 from Products
.CMFCore
.interfaces
import IContentish
66 from utils
import _checkMemberPermission
67 from utils
import Message
as _
68 from utils
import makeValidId
69 from Globals
import InitializeClass
70 from AccessControl
import ClassSecurityInfo
71 from ZServer
import LARGE_FILE_THRESHOLD
72 from webdav
.interfaces
import IWriteLock
73 from webdav
.common
import Locked
74 from webdav
.common
import PreconditionFailed
75 from zope
.contenttype
import guess_content_type
78 class PlinnFolder(CMFCatalogAware
, PortalFolder
, DefaultDublinCoreImpl
) :
81 implements(IContentish
)
83 security
= ClassSecurityInfo()
85 manage_options
= PortalFolder
.manage_options
87 ## change security for inherited methods
88 security
.declareProtected(AddPortalContent
, 'manage_pasteObjects')
90 def __init__( self
, id, title
='' ) :
91 PortalFolder
.__init
__(self
, id)
92 DefaultDublinCoreImpl
.__init
__(self
, title
= title
)
94 security
.declarePublic('allowedContentTypes')
95 def allowedContentTypes(self
):
97 List type info objects for types which can be added in this folder.
98 Types can be filtered using the localContentTypes attribute.
100 allowedTypes
= PortalFolder
.allowedContentTypes(self
)
101 if hasattr(self
, 'localContentTypes'):
102 allowedTypes
= [t
for t
in allowedTypes
if t
.title
in self
.localContentTypes
]
105 security
.declareProtected(View
, 'objectIdCanBeDeleted')
106 def objectIdCanBeDeleted(self
, id) :
107 """ Check permissions and ownership and return True
108 if current user can delete object id.
110 if _checkPermission(DeleteObjects
, self
) : # std zope perm
113 elif _checkPermission(DeletePortalContents
, self
):
114 mtool
= getToolByName(self
, 'portal_membership')
115 authMember
= mtool
.getAuthenticatedMember()
116 ob
= getattr(self
, id)
117 if authMember
.allowed(ob
, object_roles
=['Owner'] ) and \
118 _checkPermission(DeleteOwnedObjects
, ob
) : return True
124 security
.declareProtected(DeletePortalContents
, 'manage_delObjects')
125 def manage_delObjects(self
, ids
=[], REQUEST
=None):
126 """Delete subordinate objects.
127 A member can delete his owned contents (if he has the 'Delete Portal Contents' permission)
128 without 'Delete objects' permission in this folder.
129 Return skipped object ids.
132 if _checkPermission(DeleteObjects
, self
) : # std zope perm
133 PortalFolder
.manage_delObjects(self
, ids
=ids
, REQUEST
=REQUEST
)
135 mtool
= getToolByName(self
, 'portal_membership')
136 authMember
= mtool
.getAuthenticatedMember()
138 if type(ids
) == StringType
:
142 if authMember
.allowed(ob
, object_roles
=['Owner'] ) and \
143 _checkPermission(DeleteOwnedObjects
, ob
) : owned
.append(id)
144 else : notOwned
.append(id)
146 PortalFolder
.manage_delObjects(self
, ids
=owned
, REQUEST
=REQUEST
)
148 if REQUEST
is not None:
149 return self
.manage_main(
151 manage_tabs_message
='Object(s) deleted.',
156 security
.declareProtected(AddPortalContent
, 'manage_renameObjects')
157 def manage_renameObjects(self
, ids
=[], new_ids
=[], REQUEST
=None) :
158 """ Rename subordinate objects
159 A member can rename his owned contents if he has the 'Modify Portal Content' permission.
160 Returns skippend object ids.
162 if len(ids
) != len(new_ids
):
163 raise BadRequest(_('Please rename each listed object.'))
165 if _checkPermission(ViewManagementScreens
, self
) : # std zope perm
166 return super(PlinnFolder
, self
).manage_renameObjects(ids
, new_ids
, REQUEST
)
168 mtool
= getToolByName(self
, 'portal_membership')
169 authMember
= mtool
.getAuthenticatedMember()
171 for id, new_id
in zip(ids
, new_ids
) :
172 if id == new_id
: continue
175 if authMember
.allowed(ob
, object_roles
=['Owner'] ) and \
176 _checkPermission(ModifyPortalContent
, ob
) :
177 self
.manage_renameObject(id, new_id
)
181 if REQUEST
is not None :
182 return self
.manage_main(self
, REQUEST
, update_menu
=1)
187 security
.declareProtected(ListFolderContents
, 'listFolderContents')
188 def listFolderContents( self
, contentFilter
=None ):
189 """ List viewable contentish and folderish sub-objects.
191 items
= self
.contentItems(filter=contentFilter
)
193 for id, obj
in items
:
194 if _checkPermission(View
, obj
) :
200 security
.declareProtected(ListFolderContents
, 'listNearestFolderContents')
201 def listNearestFolderContents(self
, contentFilter
=None, userid
=None, sorted=False) :
202 """ Return folder contents and traverse
203 recursively unaccessfull sub folders to find
209 filt
= contentFilter
.copy()
210 ctool
= getToolByName(self
, 'portal_catalog')
211 mtool
= getToolByName(self
, 'portal_membership')
213 if userid
and _checkPermission(CheckMemberPermission
, getToolByName(self
, 'portal_url').getPortalObject()) :
214 checkFunc
= lambda perm
, ob
: _checkMemberPermission(userid
, View
, ob
)
215 filt
['allowedRolesAndUsers'] = ctool
._listAllowedRolesAndUsers
( mtool
.getMemberById(userid
) )
217 checkFunc
= _checkPermission
218 filt
['allowedRolesAndUsers'] = ctool
._listAllowedRolesAndUsers
( mtool
.getAuthenticatedMember() )
221 # copy from CMFCore.PortalFolder.PortalFolder._filteredItems
222 pt
= filt
.get('portal_type', [])
223 if type(pt
) is type(''):
225 types_tool
= getToolByName(self
, 'portal_types')
226 allowed_types
= types_tool
.listContentTypes()
230 pt
= [t
for t
in pt
if t
in allowed_types
]
232 # After filtering, no types remain, so nothing should be
235 filt
['portal_type'] = pt
238 query
= ContentFilter(**filt
)
241 for o
in self
.objectValues() :
243 if checkFunc(View
, o
):
244 nearestObjects
.append(o
)
245 elif getattr(o
.aq_self
,'isAnObjectManager', False):
246 nearestObjects
.extend(_getDeepObjects(self
, ctool
, o
, filter=filt
))
248 if sorted and len(nearestObjects
) > 0 :
249 key
, reverse
= self
.getDefaultSorting()
250 if key
!= 'position' :
251 indexCallable
= callable(getattr(nearestObjects
[0], key
))
253 sortfunc
= lambda a
, b
: cmp(getattr(a
, key
)(), getattr(b
, key
)())
255 sortfunc
= lambda a
, b
: cmp(getattr(a
, key
), getattr(b
, key
))
256 nearestObjects
.sort(cmp=sortfunc
, reverse
=reverse
)
258 return nearestObjects
260 security
.declareProtected(ListFolderContents
, 'listCatalogedContents')
261 def listCatalogedContents(self
, contentFilter
={}):
262 """ query catalog and returns brains of contents.
263 Requires ExtendedPathIndex
265 ctool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.ICatalogTool')
266 contentFilter
['path'] = {'query':'/'.join(self
.getPhysicalPath()),
268 return ctool(sort_on
='position', **contentFilter
)
270 security
.declarePublic('synContentValues')
271 def synContentValues(self
):
272 # value for syndication
273 return self
.listNearestFolderContents()
275 security
.declareProtected(View
, 'SearchableText')
276 def SearchableText(self
) :
277 """ for full text indexation
279 return '%s %s' % (self
.title
, self
.description
)
281 security
.declareProtected(AddPortalFolders
, 'manage_addPlinnFolder')
282 def manage_addPlinnFolder(self
, id, title
='', REQUEST
=None):
283 """Add a new PortalFolder object with id *id*.
285 ob
=PlinnFolder(id, title
)
286 # from CMFCore.PortalFolder.PortalFolder :-)
287 self
._setObject
(id, ob
)
288 if REQUEST
is not None:
289 return self
.folder_contents( # XXX: ick!
290 self
, REQUEST
, portal_status_message
="Folder added")
293 security
.declareProtected(AddPortalContent
, 'put_upload')
294 def put_upload(self
, REQUEST
, RESPONSE
):
295 """ Upload a content thru webdav put method.
296 The default behavior (NullRessource.PUT + PortalFolder.PUT_factory)
297 disallow files names with '_' at the begining.
300 self
.dav__init(REQUEST
, RESPONSE
)
301 fileName
= unquote(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
)
331 ob
= self
._getOb
(validId
)
333 # We call _verifyObjectPaste with verify_src=0, to see if the
334 # user can create this type of object (and we don't need to
335 # check the clipboard.
337 self
._verifyObjectPaste
(ob
.__of
__(self
), 0)
339 sMsg
= 'Unable to create object of class %s in %s: %s' % \
340 (ob
.__class
__, repr(self
), sys
.exc_info()[1],)
341 raise Unauthorized
, sMsg
343 ob
.PUT(REQUEST
, RESPONSE
)
344 ob
.orig_name
= fileName
346 # get method from ob created / refreshed
347 ti
= ob
.getTypeInfo()
348 method_id
= ti
.queryMethodID('jsupload_snippet')
349 meth
= getattr(ob
, method_id
) if method_id
else None
351 # get method from container that receive uploaded content
352 ti
= self
.getTypeInfo()
353 method_id
= ti
.queryMethodID('jsupload_snippet')
354 meth
= getattr(self
, method_id
) if method_id
else lambda : 'Not implemented'
356 RESPONSE
.setStatus(httpRespCode
)
357 RESPONSE
.setHeader('Content-Type', 'text/xml;;charset=utf-8')
358 return '<fragment>%s</fragment>' % meth(ob
).strip()
361 # ## overload to maintain ownership if authenticated user has 'Manage portal' permission
362 # def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
363 # """Paste previously copied objects into the current object.
365 # If calling manage_pasteObjects from python code, pass the result of a
366 # previous call to manage_cutObjects or manage_copyObjects as the first
369 # Also sends IObjectCopiedEvent and IObjectClonedEvent
370 # or IObjectWillBeMovedEvent and IObjectMovedEvent.
372 # if cb_copy_data is not None:
374 # elif REQUEST is not None and REQUEST.has_key('__cp'):
375 # cp = REQUEST['__cp']
379 # raise CopyError, eNoData
382 # op, mdatas = _cb_decode(cp)
384 # raise CopyError, eInvalid
387 # app = self.getPhysicalRoot()
388 # for mdata in mdatas:
389 # m = Moniker.loadMoniker(mdata)
392 # except ConflictError:
395 # raise CopyError, eNotFound
396 # self._verifyObjectPaste(ob, validate_src=op+1)
402 # mtool = getToolByName(self, 'portal_membership')
403 # utool = getToolByName(self, 'portal_url')
404 # portal = utool.getPortalObject()
405 # userIsPortalManager = mtool.checkPermission(ManagePortal, portal)
408 # orig_id = ob.getId()
409 # if not ob.cb_isCopyable():
410 # raise CopyError, eNotSupported % escape(orig_id)
413 # ob._notifyOfCopyTo(self, op=0)
414 # except ConflictError:
417 # raise CopyError, MessageDialog(
418 # title="Copy Error",
419 # message=sys.exc_info()[1],
420 # action='manage_main')
422 # id = self._get_id(orig_id)
423 # result.append({'id': orig_id, 'new_id': id})
426 # ob = ob._getCopy(self)
428 # notify(ObjectCopiedEvent(ob, orig_ob))
430 # if not userIsPortalManager :
431 # self._setObject(id, ob, suppress_events=True)
433 # self._setObject(id, ob, suppress_events=True, set_owner=0)
434 # ob = self._getOb(id)
437 # ob._postCopy(self, op=0)
439 # OFS.subscribers.compatibilityCall('manage_afterClone', ob, ob)
441 # notify(ObjectClonedEvent(ob))
443 # if REQUEST is not None:
444 # return self.manage_main(self, REQUEST, update_menu=1,
450 # orig_id = ob.getId()
451 # if not ob.cb_isMoveable():
452 # raise CopyError, eNotSupported % escape(orig_id)
455 # ob._notifyOfCopyTo(self, op=1)
456 # except ConflictError:
459 # raise CopyError, MessageDialog(
460 # title="Move Error",
461 # message=sys.exc_info()[1],
462 # action='manage_main')
464 # if not sanity_check(self, ob):
465 # raise CopyError, "This object cannot be pasted into itself"
467 # orig_container = aq_parent(aq_inner(ob))
468 # if aq_base(orig_container) is aq_base(self):
471 # id = self._get_id(orig_id)
472 # result.append({'id': orig_id, 'new_id': id})
474 # notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
477 # # try to make ownership explicit so that it gets carried
478 # # along to the new location if needed.
479 # ob.manage_changeOwnershipType(explicit=1)
482 # orig_container._delObject(orig_id, suppress_events=True)
484 # orig_container._delObject(orig_id)
486 # "%s._delObject without suppress_events is discouraged."
487 # % orig_container.__class__.__name__,
488 # DeprecationWarning)
493 # self._setObject(id, ob, set_owner=0, suppress_events=True)
495 # self._setObject(id, ob, set_owner=0)
497 # "%s._setObject without suppress_events is discouraged."
498 # % self.__class__.__name__, DeprecationWarning)
499 # ob = self._getOb(id)
501 # notify(ObjectMovedEvent(ob, orig_container, orig_id, self, id))
502 # notifyContainerModified(orig_container)
503 # if aq_base(orig_container) is not aq_base(self):
504 # notifyContainerModified(self)
506 # ob._postCopy(self, op=1)
507 # # try to make ownership implicit if possible
508 # ob.manage_changeOwnershipType(explicit=0)
510 # if REQUEST is not None:
511 # REQUEST['RESPONSE'].setCookie('__cp', 'deleted',
512 # path='%s' % cookie_path(REQUEST),
513 # expires='Wed, 31-Dec-97 23:59:59 GMT')
514 # REQUEST['__cp'] = None
515 # return self.manage_main(self, REQUEST, update_menu=1,
521 InitializeClass(PlinnFolder
)
522 PlinnFolderFactory
= Factory(PlinnFolder
)
524 def _getDeepObjects(self
, ctool
, o
, filter={}):
525 res
= ctool
.unrestrictedSearchResults(path
= '/'.join(o
.getPhysicalPath()), **filter)
532 res
.sort(lambda a
, b
: cmp(a
.getPath(), b
.getPath()))
533 previousPath
= res
[0].getPath()
535 deepObjects
.append(res
[0].getObject())
537 currentPath
= b
.getPath()
538 if currentPath
.startswith(previousPath
) and len(currentPath
) > len(previousPath
):
541 deepObjects
.append(b
.getObject())
542 previousPath
= currentPath
547 manage_addPlinnFolder
= PlinnFolder
.manage_addPlinnFolder
.im_func