9b30e5365505901402eab34529c11c4e8c003aad
[Plinn.git] / Folder.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # Copyright (C) 2005-2007 BenoƮt PIN <benoit.pin@ensmp.fr> #
5 # #
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. #
10 # #
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. #
15 # #
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
21
22
23
24 """
25
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
31 import sys
32 import warnings
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
40 try :
41 from zope.app.container.contained import notifyContainerModified
42 from zope.app.container.contained import ObjectMovedEvent
43 except ImportError :
44 ## Zope-2.13 compat
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
51
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
63
64 from zope.interface import implements
65 from Products.CMFCore.interfaces import IContentish
66
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
77
78
79 class PlinnFolder(CMFCatalogAware, PortalFolder, DefaultDublinCoreImpl) :
80 """ Plinn Folder """
81
82 implements(IContentish)
83
84 security = ClassSecurityInfo()
85
86 manage_options = PortalFolder.manage_options
87
88 ## change security for inherited methods
89 security.declareProtected(AddPortalContent, 'manage_pasteObjects')
90
91 def __init__( self, id, title='' ) :
92 PortalFolder.__init__(self, id)
93 DefaultDublinCoreImpl.__init__(self, title = title)
94
95 security.declarePublic('allowedContentTypes')
96 def allowedContentTypes(self):
97 """
98 List type info objects for types which can be added in this folder.
99 Types can be filtered using the localContentTypes attribute.
100 """
101 allowedTypes = PortalFolder.allowedContentTypes(self)
102 if hasattr(self, 'localContentTypes'):
103 allowedTypes = [t for t in allowedTypes if t.title in self.localContentTypes]
104 return allowedTypes
105
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.
110 """
111 if _checkPermission(DeleteObjects, self) : # std zope perm
112 return True
113
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
120
121 else :
122 return False
123
124
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.
131 """
132 notOwned = []
133 if _checkPermission(DeleteObjects, self) : # std zope perm
134 PortalFolder.manage_delObjects(self, ids=ids, REQUEST=REQUEST)
135 else :
136 mtool = getToolByName(self, 'portal_membership')
137 authMember = mtool.getAuthenticatedMember()
138 owned = []
139 if type(ids) == StringType :
140 ids = [ids]
141 for id in ids :
142 ob = self._getOb(id)
143 if authMember.allowed(ob, object_roles=['Owner'] ) and \
144 _checkPermission(DeleteOwnedObjects, ob) : owned.append(id)
145 else : notOwned.append(id)
146 if owned :
147 PortalFolder.manage_delObjects(self, ids=owned, REQUEST=REQUEST)
148
149 if REQUEST is not None:
150 return self.manage_main(
151 self, REQUEST,
152 manage_tabs_message='Object(s) deleted.',
153 update_menu=1)
154 return notOwned
155
156
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.
162 """
163 if len(ids) != len(new_ids):
164 raise BadRequest(_('Please rename each listed object.'))
165
166 if _checkPermission(ViewManagementScreens, self) : # std zope perm
167 return super(PlinnFolder, self).manage_renameObjects(ids, new_ids, REQUEST)
168
169 mtool = getToolByName(self, 'portal_membership')
170 authMember = mtool.getAuthenticatedMember()
171 skiped = []
172 for id, new_id in zip(ids, new_ids) :
173 if id == new_id : continue
174
175 ob = self._getOb(id)
176 if authMember.allowed(ob, object_roles=['Owner'] ) and \
177 _checkPermission(ModifyPortalContent, ob) :
178 self.manage_renameObject(id, new_id)
179 else :
180 skiped.append(id)
181
182 if REQUEST is not None :
183 return self.manage_main(self, REQUEST, update_menu=1)
184
185 return skiped
186
187
188 security.declareProtected(ListFolderContents, 'listFolderContents')
189 def listFolderContents( self, contentFilter=None ):
190 """ List viewable contentish and folderish sub-objects.
191 """
192 items = self.contentItems(filter=contentFilter)
193 l = []
194 for id, obj in items:
195 if _checkPermission(View, obj) :
196 l.append(obj)
197
198 return l
199
200
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
205 accessible contents.
206 """
207
208 filt = {}
209 if contentFilter :
210 filt = contentFilter.copy()
211 ctool = getToolByName(self, 'portal_catalog')
212 mtool = getToolByName(self, 'portal_membership')
213
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) )
217 else :
218 checkFunc = _checkPermission
219 filt['allowedRolesAndUsers'] = ctool._listAllowedRolesAndUsers( mtool.getAuthenticatedMember() )
220
221
222 # copy from CMFCore.PortalFolder.PortalFolder._filteredItems
223 pt = filt.get('portal_type', [])
224 if type(pt) is type(''):
225 pt = [pt]
226 types_tool = getToolByName(self, 'portal_types')
227 allowed_types = types_tool.listContentTypes()
228 if not pt:
229 pt = allowed_types
230 else:
231 pt = [t for t in pt if t in allowed_types]
232 if not pt:
233 # After filtering, no types remain, so nothing should be
234 # returned.
235 return []
236 filt['portal_type'] = pt
237 #---
238
239 query = ContentFilter(**filt)
240 nearestObjects = []
241
242 for o in self.objectValues() :
243 if query(o) :
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))
248
249 if sorted and len(nearestObjects) > 0 :
250 key, reverse = self.getDefaultSorting()
251 if key != 'position' :
252 indexCallable = callable(getattr(nearestObjects[0], key))
253 if indexCallable :
254 sortfunc = lambda a, b : cmp(getattr(a, key)(), getattr(b, key)())
255 else :
256 sortfunc = lambda a, b : cmp(getattr(a, key), getattr(b, key))
257 nearestObjects.sort(cmp=sortfunc, reverse=reverse)
258
259 return nearestObjects
260
261 security.declareProtected(ListFolderContents, 'listCatalogedContents')
262 def listCatalogedContents(self, contentFilter={}):
263 """ query catalog and returns brains of contents.
264 Requires ExtendedPathIndex
265 """
266 ctool = getUtilityByInterfaceName('Products.CMFCore.interfaces.ICatalogTool')
267 contentFilter['path'] = {'query':'/'.join(self.getPhysicalPath()),
268 'depth':1}
269 if not contentFilter.has_key('sort_on') :
270 contentFilter['sort_index'] = 'position'
271 return ctool(**contentFilter)
272
273 security.declarePublic('synContentValues')
274 def synContentValues(self):
275 # value for syndication
276 return self.listNearestFolderContents()
277
278 security.declareProtected(View, 'SearchableText')
279 def SearchableText(self) :
280 """ for full text indexation
281 """
282 return '%s %s' % (self.title, self.description)
283
284 security.declareProtected(AddPortalFolders, 'manage_addPlinnFolder')
285 def manage_addPlinnFolder(self, id, title='', REQUEST=None):
286 """Add a new PortalFolder object with id *id*.
287 """
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")
294
295
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.
301 """
302
303 self.dav__init(REQUEST, RESPONSE)
304 fileName = unquote(REQUEST.getHeader('X-File-Name', ''))
305 validId = makeValidId(self, fileName, allow_dup=True)
306
307 ifhdr = REQUEST.get_header('If', '')
308 if self.wl_isLocked():
309 if ifhdr:
310 self.dav__simpleifhandler(REQUEST, RESPONSE, col=1)
311 else:
312 raise Locked
313 elif ifhdr:
314 raise PreconditionFailed
315
316 if int(REQUEST.get('CONTENT_LENGTH') or 0) > LARGE_FILE_THRESHOLD:
317 file = REQUEST['BODYFILE']
318 body = file.read(LARGE_FILE_THRESHOLD)
319 file.seek(0)
320 else:
321 body = REQUEST.get('BODY', '')
322
323 typ=REQUEST.get_header('content-type', None)
324 if typ is None:
325 typ, enc=guess_content_type(validId, body)
326
327 if self.checkIdAvailable(validId) :
328 try :
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,
337 format=typ)
338 httpRespCode = 201
339 else :
340 httpRespCode = 200
341 ob = self._getOb(validId)
342
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.
346 try:
347 self._verifyObjectPaste(ob.__of__(self), 0)
348 except CopyError:
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
352
353 ob.PUT(REQUEST, RESPONSE)
354 ob.orig_name = fileName
355
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
360 if not meth :
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'
365
366 RESPONSE.setStatus(httpRespCode)
367 RESPONSE.setHeader('Content-Type', 'text/xml;;charset=utf-8')
368 return '<fragment>%s</fragment>' % meth(ob).strip()
369
370
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.
374 #
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
377 # argument.
378 #
379 # Also sends IObjectCopiedEvent and IObjectClonedEvent
380 # or IObjectWillBeMovedEvent and IObjectMovedEvent.
381 # """
382 # if cb_copy_data is not None:
383 # cp = cb_copy_data
384 # elif REQUEST is not None and REQUEST.has_key('__cp'):
385 # cp = REQUEST['__cp']
386 # else:
387 # cp = None
388 # if cp is None:
389 # raise CopyError, eNoData
390 #
391 # try:
392 # op, mdatas = _cb_decode(cp)
393 # except:
394 # raise CopyError, eInvalid
395 #
396 # oblist = []
397 # app = self.getPhysicalRoot()
398 # for mdata in mdatas:
399 # m = Moniker.loadMoniker(mdata)
400 # try:
401 # ob = m.bind(app)
402 # except ConflictError:
403 # raise
404 # except:
405 # raise CopyError, eNotFound
406 # self._verifyObjectPaste(ob, validate_src=op+1)
407 # oblist.append(ob)
408 #
409 # result = []
410 # if op == 0:
411 # # Copy operation
412 # mtool = getToolByName(self, 'portal_membership')
413 # utool = getToolByName(self, 'portal_url')
414 # portal = utool.getPortalObject()
415 # userIsPortalManager = mtool.checkPermission(ManagePortal, portal)
416 #
417 # for ob in oblist:
418 # orig_id = ob.getId()
419 # if not ob.cb_isCopyable():
420 # raise CopyError, eNotSupported % escape(orig_id)
421 #
422 # try:
423 # ob._notifyOfCopyTo(self, op=0)
424 # except ConflictError:
425 # raise
426 # except:
427 # raise CopyError, MessageDialog(
428 # title="Copy Error",
429 # message=sys.exc_info()[1],
430 # action='manage_main')
431 #
432 # id = self._get_id(orig_id)
433 # result.append({'id': orig_id, 'new_id': id})
434 #
435 # orig_ob = ob
436 # ob = ob._getCopy(self)
437 # ob._setId(id)
438 # notify(ObjectCopiedEvent(ob, orig_ob))
439 #
440 # if not userIsPortalManager :
441 # self._setObject(id, ob, suppress_events=True)
442 # else :
443 # self._setObject(id, ob, suppress_events=True, set_owner=0)
444 # ob = self._getOb(id)
445 # ob.wl_clearLocks()
446 #
447 # ob._postCopy(self, op=0)
448 #
449 # OFS.subscribers.compatibilityCall('manage_afterClone', ob, ob)
450 #
451 # notify(ObjectClonedEvent(ob))
452 #
453 # if REQUEST is not None:
454 # return self.manage_main(self, REQUEST, update_menu=1,
455 # cb_dataValid=1)
456 #
457 # elif op == 1:
458 # # Move operation
459 # for ob in oblist:
460 # orig_id = ob.getId()
461 # if not ob.cb_isMoveable():
462 # raise CopyError, eNotSupported % escape(orig_id)
463 #
464 # try:
465 # ob._notifyOfCopyTo(self, op=1)
466 # except ConflictError:
467 # raise
468 # except:
469 # raise CopyError, MessageDialog(
470 # title="Move Error",
471 # message=sys.exc_info()[1],
472 # action='manage_main')
473 #
474 # if not sanity_check(self, ob):
475 # raise CopyError, "This object cannot be pasted into itself"
476 #
477 # orig_container = aq_parent(aq_inner(ob))
478 # if aq_base(orig_container) is aq_base(self):
479 # id = orig_id
480 # else:
481 # id = self._get_id(orig_id)
482 # result.append({'id': orig_id, 'new_id': id})
483 #
484 # notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
485 # self, id))
486 #
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)
490 #
491 # try:
492 # orig_container._delObject(orig_id, suppress_events=True)
493 # except TypeError:
494 # orig_container._delObject(orig_id)
495 # warnings.warn(
496 # "%s._delObject without suppress_events is discouraged."
497 # % orig_container.__class__.__name__,
498 # DeprecationWarning)
499 # ob = aq_base(ob)
500 # ob._setId(id)
501 #
502 # try:
503 # self._setObject(id, ob, set_owner=0, suppress_events=True)
504 # except TypeError:
505 # self._setObject(id, ob, set_owner=0)
506 # warnings.warn(
507 # "%s._setObject without suppress_events is discouraged."
508 # % self.__class__.__name__, DeprecationWarning)
509 # ob = self._getOb(id)
510 #
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)
515 #
516 # ob._postCopy(self, op=1)
517 # # try to make ownership implicit if possible
518 # ob.manage_changeOwnershipType(explicit=0)
519 #
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,
526 # cb_dataValid=0)
527 #
528 # return result
529
530
531 InitializeClass(PlinnFolder)
532 PlinnFolderFactory = Factory(PlinnFolder)
533
534 def _getDeepObjects(self, ctool, o, filter={}):
535 res = ctool.unrestrictedSearchResults(path = '/'.join(o.getPhysicalPath()), **filter)
536
537 if not res :
538 return []
539 else :
540 deepObjects = []
541 res = list(res)
542 res.sort(lambda a, b: cmp(a.getPath(), b.getPath()))
543 previousPath = res[0].getPath()
544
545 deepObjects.append(res[0].getObject())
546 for b in res[1:] :
547 currentPath = b.getPath()
548 if currentPath.startswith(previousPath) and len(currentPath) > len(previousPath):
549 continue
550 else :
551 deepObjects.append(b.getObject())
552 previousPath = currentPath
553
554 return deepObjects
555
556
557 manage_addPlinnFolder = PlinnFolder.manage_addPlinnFolder.im_func