Finitions.
[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 return ctool(sort_on='position', **contentFilter)
270
271 security.declarePublic('synContentValues')
272 def synContentValues(self):
273 # value for syndication
274 return self.listNearestFolderContents()
275
276 security.declareProtected(View, 'SearchableText')
277 def SearchableText(self) :
278 """ for full text indexation
279 """
280 return '%s %s' % (self.title, self.description)
281
282 security.declareProtected(AddPortalFolders, 'manage_addPlinnFolder')
283 def manage_addPlinnFolder(self, id, title='', REQUEST=None):
284 """Add a new PortalFolder object with id *id*.
285 """
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")
292
293
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.
299 """
300
301 self.dav__init(REQUEST, RESPONSE)
302 fileName = unquote(REQUEST.getHeader('X-File-Name', ''))
303 validId = makeValidId(self, fileName, allow_dup=True)
304
305 ifhdr = REQUEST.get_header('If', '')
306 if self.wl_isLocked():
307 if ifhdr:
308 self.dav__simpleifhandler(REQUEST, RESPONSE, col=1)
309 else:
310 raise Locked
311 elif ifhdr:
312 raise PreconditionFailed
313
314 if int(REQUEST.get('CONTENT_LENGTH') or 0) > LARGE_FILE_THRESHOLD:
315 file = REQUEST['BODYFILE']
316 body = file.read(LARGE_FILE_THRESHOLD)
317 file.seek(0)
318 else:
319 body = REQUEST.get('BODY', '')
320
321 typ=REQUEST.get_header('content-type', None)
322 if typ is None:
323 typ, enc=guess_content_type(validId, body)
324
325 if self.checkIdAvailable(validId) :
326 try :
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,
335 format=typ)
336 httpRespCode = 201
337 else :
338 httpRespCode = 200
339 ob = self._getOb(validId)
340
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.
344 try:
345 self._verifyObjectPaste(ob.__of__(self), 0)
346 except CopyError:
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
350
351 ob.PUT(REQUEST, RESPONSE)
352 ob.orig_name = fileName
353
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
358 if not meth :
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'
363
364 RESPONSE.setStatus(httpRespCode)
365 RESPONSE.setHeader('Content-Type', 'text/xml;;charset=utf-8')
366 return '<fragment>%s</fragment>' % meth(ob).strip()
367
368
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.
372 #
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
375 # argument.
376 #
377 # Also sends IObjectCopiedEvent and IObjectClonedEvent
378 # or IObjectWillBeMovedEvent and IObjectMovedEvent.
379 # """
380 # if cb_copy_data is not None:
381 # cp = cb_copy_data
382 # elif REQUEST is not None and REQUEST.has_key('__cp'):
383 # cp = REQUEST['__cp']
384 # else:
385 # cp = None
386 # if cp is None:
387 # raise CopyError, eNoData
388 #
389 # try:
390 # op, mdatas = _cb_decode(cp)
391 # except:
392 # raise CopyError, eInvalid
393 #
394 # oblist = []
395 # app = self.getPhysicalRoot()
396 # for mdata in mdatas:
397 # m = Moniker.loadMoniker(mdata)
398 # try:
399 # ob = m.bind(app)
400 # except ConflictError:
401 # raise
402 # except:
403 # raise CopyError, eNotFound
404 # self._verifyObjectPaste(ob, validate_src=op+1)
405 # oblist.append(ob)
406 #
407 # result = []
408 # if op == 0:
409 # # Copy operation
410 # mtool = getToolByName(self, 'portal_membership')
411 # utool = getToolByName(self, 'portal_url')
412 # portal = utool.getPortalObject()
413 # userIsPortalManager = mtool.checkPermission(ManagePortal, portal)
414 #
415 # for ob in oblist:
416 # orig_id = ob.getId()
417 # if not ob.cb_isCopyable():
418 # raise CopyError, eNotSupported % escape(orig_id)
419 #
420 # try:
421 # ob._notifyOfCopyTo(self, op=0)
422 # except ConflictError:
423 # raise
424 # except:
425 # raise CopyError, MessageDialog(
426 # title="Copy Error",
427 # message=sys.exc_info()[1],
428 # action='manage_main')
429 #
430 # id = self._get_id(orig_id)
431 # result.append({'id': orig_id, 'new_id': id})
432 #
433 # orig_ob = ob
434 # ob = ob._getCopy(self)
435 # ob._setId(id)
436 # notify(ObjectCopiedEvent(ob, orig_ob))
437 #
438 # if not userIsPortalManager :
439 # self._setObject(id, ob, suppress_events=True)
440 # else :
441 # self._setObject(id, ob, suppress_events=True, set_owner=0)
442 # ob = self._getOb(id)
443 # ob.wl_clearLocks()
444 #
445 # ob._postCopy(self, op=0)
446 #
447 # OFS.subscribers.compatibilityCall('manage_afterClone', ob, ob)
448 #
449 # notify(ObjectClonedEvent(ob))
450 #
451 # if REQUEST is not None:
452 # return self.manage_main(self, REQUEST, update_menu=1,
453 # cb_dataValid=1)
454 #
455 # elif op == 1:
456 # # Move operation
457 # for ob in oblist:
458 # orig_id = ob.getId()
459 # if not ob.cb_isMoveable():
460 # raise CopyError, eNotSupported % escape(orig_id)
461 #
462 # try:
463 # ob._notifyOfCopyTo(self, op=1)
464 # except ConflictError:
465 # raise
466 # except:
467 # raise CopyError, MessageDialog(
468 # title="Move Error",
469 # message=sys.exc_info()[1],
470 # action='manage_main')
471 #
472 # if not sanity_check(self, ob):
473 # raise CopyError, "This object cannot be pasted into itself"
474 #
475 # orig_container = aq_parent(aq_inner(ob))
476 # if aq_base(orig_container) is aq_base(self):
477 # id = orig_id
478 # else:
479 # id = self._get_id(orig_id)
480 # result.append({'id': orig_id, 'new_id': id})
481 #
482 # notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
483 # self, id))
484 #
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)
488 #
489 # try:
490 # orig_container._delObject(orig_id, suppress_events=True)
491 # except TypeError:
492 # orig_container._delObject(orig_id)
493 # warnings.warn(
494 # "%s._delObject without suppress_events is discouraged."
495 # % orig_container.__class__.__name__,
496 # DeprecationWarning)
497 # ob = aq_base(ob)
498 # ob._setId(id)
499 #
500 # try:
501 # self._setObject(id, ob, set_owner=0, suppress_events=True)
502 # except TypeError:
503 # self._setObject(id, ob, set_owner=0)
504 # warnings.warn(
505 # "%s._setObject without suppress_events is discouraged."
506 # % self.__class__.__name__, DeprecationWarning)
507 # ob = self._getOb(id)
508 #
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)
513 #
514 # ob._postCopy(self, op=1)
515 # # try to make ownership implicit if possible
516 # ob.manage_changeOwnershipType(explicit=0)
517 #
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,
524 # cb_dataValid=0)
525 #
526 # return result
527
528
529 InitializeClass(PlinnFolder)
530 PlinnFolderFactory = Factory(PlinnFolder)
531
532 def _getDeepObjects(self, ctool, o, filter={}):
533 res = ctool.unrestrictedSearchResults(path = '/'.join(o.getPhysicalPath()), **filter)
534
535 if not res :
536 return []
537 else :
538 deepObjects = []
539 res = list(res)
540 res.sort(lambda a, b: cmp(a.getPath(), b.getPath()))
541 previousPath = res[0].getPath()
542
543 deepObjects.append(res[0].getObject())
544 for b in res[1:] :
545 currentPath = b.getPath()
546 if currentPath.startswith(previousPath) and len(currentPath) > len(previousPath):
547 continue
548 else :
549 deepObjects.append(b.getObject())
550 previousPath = currentPath
551
552 return deepObjects
553
554
555 manage_addPlinnFolder = PlinnFolder.manage_addPlinnFolder.im_func