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