980881060a4709dfb76bdc720b5241a5c20e8ae7
[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 import sys
31 import warnings
32 from cgi import escape
33 from OFS import Moniker
34 from ZODB.POSException import ConflictError
35 import OFS.subscribers
36 from webdav.NullResource import NullResource
37 from zope.event import notify
38 from zope.lifecycleevent import ObjectCopiedEvent
39 try :
40 from zope.app.container.contained import notifyContainerModified
41 from zope.app.container.contained import ObjectMovedEvent
42 except ImportError :
43 ## Zope-2.13 compat
44 from zope.container.contained import notifyContainerModified
45 from zope.container.contained import ObjectMovedEvent
46 from OFS.event import ObjectClonedEvent
47 from OFS.event import ObjectWillBeMovedEvent
48 from zope.component.factory import Factory
49 from Acquisition import aq_base, aq_inner, aq_parent
50
51 from types import StringType
52 from Products.CMFCore.permissions import ListFolderContents, View, ViewManagementScreens,\
53 ManageProperties, AddPortalFolders, AddPortalContent,\
54 ManagePortal, ModifyPortalContent
55 from permissions import DeletePortalContents, DeleteObjects, DeleteOwnedObjects, SetLocalRoles, CheckMemberPermission
56 from Products.CMFCore.utils import _checkPermission, getToolByName
57 from Products.CMFCore.CMFCatalogAware import CMFCatalogAware
58 from Products.CMFCore.PortalFolder import PortalFolder, ContentFilter
59 from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
60
61 from zope.interface import implements
62 from Products.CMFCore.interfaces import IContentish
63
64 from utils import _checkMemberPermission
65 from utils import Message as _
66 from Globals import InitializeClass
67 from AccessControl import ClassSecurityInfo
68
69
70 class PlinnFolder(CMFCatalogAware, PortalFolder, DefaultDublinCoreImpl) :
71 """ Plinn Folder """
72
73 implements(IContentish)
74
75 security = ClassSecurityInfo()
76
77 manage_options = PortalFolder.manage_options
78
79 ## change security for inherited methods
80 security.declareProtected(AddPortalContent, 'manage_pasteObjects')
81
82 def __init__( self, id, title='' ) :
83 PortalFolder.__init__(self, id)
84 DefaultDublinCoreImpl.__init__(self, title = title)
85
86 def __getitem__(self, key):
87 if key in self:
88 return self._getOb(key, None)
89 request = getattr(self, 'REQUEST', None)
90 if not isinstance(request, (str, NoneType)):
91 method=request.get('REQUEST_METHOD', 'GET')
92 if (request.maybe_webdav_client and
93 method not in ('GET', 'POST')):
94 return NullResource(self, key, request).__of__(self)
95 raise KeyError, key
96
97
98 security.declarePublic('allowedContentTypes')
99 def allowedContentTypes(self):
100 """
101 List type info objects for types which can be added in this folder.
102 Types can be filtered using the localContentTypes attribute.
103 """
104 allowedTypes = PortalFolder.allowedContentTypes(self)
105 if hasattr(self, 'localContentTypes'):
106 allowedTypes = [t for t in allowedTypes if t.title in self.localContentTypes]
107 return allowedTypes
108
109 security.declareProtected(View, 'objectIdCanBeDeleted')
110 def objectIdCanBeDeleted(self, id) :
111 """ Check permissions and ownership and return True
112 if current user can delete object id.
113 """
114 if _checkPermission(DeleteObjects, self) : # std zope perm
115 return True
116
117 elif _checkPermission(DeletePortalContents, self):
118 mtool = getToolByName(self, 'portal_membership')
119 authMember = mtool.getAuthenticatedMember()
120 ob = getattr(self, id)
121 if authMember.allowed(ob, object_roles=['Owner'] ) and \
122 _checkPermission(DeleteOwnedObjects, ob) : return True
123
124 else :
125 return False
126
127
128 security.declareProtected(DeletePortalContents, 'manage_delObjects')
129 def manage_delObjects(self, ids=[], REQUEST=None):
130 """Delete subordinate objects.
131 A member can delete his owned contents (if he has the 'Delete Portal Contents' permission)
132 without 'Delete objects' permission in this folder.
133 Return skipped object ids.
134 """
135 notOwned = []
136 if _checkPermission(DeleteObjects, self) : # std zope perm
137 PortalFolder.manage_delObjects(self, ids=ids, REQUEST=REQUEST)
138 else :
139 mtool = getToolByName(self, 'portal_membership')
140 authMember = mtool.getAuthenticatedMember()
141 owned = []
142 if type(ids) == StringType :
143 ids = [ids]
144 for id in ids :
145 ob = self._getOb(id)
146 if authMember.allowed(ob, object_roles=['Owner'] ) and \
147 _checkPermission(DeleteOwnedObjects, ob) : owned.append(id)
148 else : notOwned.append(id)
149 if owned :
150 PortalFolder.manage_delObjects(self, ids=owned, REQUEST=REQUEST)
151
152 if REQUEST is not None:
153 return self.manage_main(
154 self, REQUEST,
155 manage_tabs_message='Object(s) deleted.',
156 update_menu=1)
157 return notOwned
158
159
160 security.declareProtected(AddPortalContent, 'manage_renameObjects')
161 def manage_renameObjects(self, ids=[], new_ids=[], REQUEST=None) :
162 """ Rename subordinate objects
163 A member can rename his owned contents if he has the 'Modify Portal Content' permission.
164 Returns skippend object ids.
165 """
166 if len(ids) != len(new_ids):
167 raise BadRequest(_('Please rename each listed object.'))
168
169 if _checkPermission(ViewManagementScreens, self) : # std zope perm
170 return super(PlinnFolder, self).manage_renameObjects(ids, new_ids, REQUEST)
171
172 mtool = getToolByName(self, 'portal_membership')
173 authMember = mtool.getAuthenticatedMember()
174 skiped = []
175 for id, new_id in zip(ids, new_ids) :
176 if id == new_id : continue
177
178 ob = self._getOb(id)
179 if authMember.allowed(ob, object_roles=['Owner'] ) and \
180 _checkPermission(ModifyPortalContent, ob) :
181 self.manage_renameObject(id, new_id)
182 else :
183 skiped.append(id)
184
185 if REQUEST is not None :
186 return self.manage_main(self, REQUEST, update_menu=1)
187
188 return skiped
189
190
191 security.declareProtected(ListFolderContents, 'listFolderContents')
192 def listFolderContents( self, contentFilter=None ):
193 """ List viewable contentish and folderish sub-objects.
194 """
195 items = self.contentItems(filter=contentFilter)
196 l = []
197 for id, obj in items:
198 if _checkPermission(View, obj) :
199 l.append(obj)
200
201 return l
202
203
204 security.declareProtected(ListFolderContents, 'listNearestFolderContents')
205 def listNearestFolderContents(self, contentFilter=None, userid=None, sorted=False) :
206 """ Return folder contents and traverse
207 recursively unaccessfull sub folders to find
208 accessible contents.
209 """
210
211 filt = {}
212 if contentFilter :
213 filt = contentFilter.copy()
214 ctool = getToolByName(self, 'portal_catalog')
215 mtool = getToolByName(self, 'portal_membership')
216
217 if userid and _checkPermission(CheckMemberPermission, getToolByName(self, 'portal_url').getPortalObject()) :
218 checkFunc = lambda perm, ob : _checkMemberPermission(userid, View, ob)
219 filt['allowedRolesAndUsers'] = ctool._listAllowedRolesAndUsers( mtool.getMemberById(userid) )
220 else :
221 checkFunc = _checkPermission
222 filt['allowedRolesAndUsers'] = ctool._listAllowedRolesAndUsers( mtool.getAuthenticatedMember() )
223
224
225 # copy from CMFCore.PortalFolder.PortalFolder._filteredItems
226 pt = filt.get('portal_type', [])
227 if type(pt) is type(''):
228 pt = [pt]
229 types_tool = getToolByName(self, 'portal_types')
230 allowed_types = types_tool.listContentTypes()
231 if not pt:
232 pt = allowed_types
233 else:
234 pt = [t for t in pt if t in allowed_types]
235 if not pt:
236 # After filtering, no types remain, so nothing should be
237 # returned.
238 return []
239 filt['portal_type'] = pt
240 #---
241
242 query = ContentFilter(**filt)
243 nearestObjects = []
244
245 for o in self.objectValues() :
246 if query(o) :
247 if checkFunc(View, o):
248 nearestObjects.append(o)
249 elif getattr(o.aq_self,'isAnObjectManager', False):
250 nearestObjects.extend(_getDeepObjects(self, ctool, o, filter=filt))
251
252 if sorted and len(nearestObjects) > 0 :
253 key, reverse = self.getDefaultSorting()
254 if key != 'position' :
255 indexCallable = callable(getattr(nearestObjects[0], key))
256 if indexCallable :
257 sortfunc = lambda a, b : cmp(getattr(a, key)(), getattr(b, key)())
258 else :
259 sortfunc = lambda a, b : cmp(getattr(a, key), getattr(b, key))
260 nearestObjects.sort(cmp=sortfunc, reverse=reverse)
261
262 return nearestObjects
263
264 security.declareProtected(ListFolderContents, 'listCatalogedContents')
265 def listCatalogedContents(self, contentFilter={}):
266 """ query catalog and returns brains of contents.
267 Requires ExtendedPathIndex
268 """
269 ctool = getToolByName(self, 'portal_catalog')
270 contentFilter['path'] = {'query':'/'.join(self.getPhysicalPath()),
271 'depth':1}
272 return ctool(sort_on='position', **contentFilter)
273
274
275 security.declarePublic('synContentValues')
276 def synContentValues(self):
277 # value for syndication
278 return self.listNearestFolderContents()
279
280 security.declareProtected(View, 'SearchableText')
281 def SearchableText(self) :
282 """ for full text indexation
283 """
284 return '%s %s' % (self.title, self.description)
285
286 security.declareProtected(AddPortalFolders, 'manage_addPlinnFolder')
287 def manage_addPlinnFolder(self, id, title='', REQUEST=None):
288 """Add a new PortalFolder object with id *id*.
289 """
290 ob=PlinnFolder(id, title)
291 # from CMFCore.PortalFolder.PortalFolder :-)
292 self._setObject(id, ob)
293 if REQUEST is not None:
294 return self.folder_contents( # XXX: ick!
295 self, REQUEST, portal_status_message="Folder added")
296
297
298 # ## overload to maintain ownership if authenticated user has 'Manage portal' permission
299 # def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
300 # """Paste previously copied objects into the current object.
301 #
302 # If calling manage_pasteObjects from python code, pass the result of a
303 # previous call to manage_cutObjects or manage_copyObjects as the first
304 # argument.
305 #
306 # Also sends IObjectCopiedEvent and IObjectClonedEvent
307 # or IObjectWillBeMovedEvent and IObjectMovedEvent.
308 # """
309 # if cb_copy_data is not None:
310 # cp = cb_copy_data
311 # elif REQUEST is not None and REQUEST.has_key('__cp'):
312 # cp = REQUEST['__cp']
313 # else:
314 # cp = None
315 # if cp is None:
316 # raise CopyError, eNoData
317 #
318 # try:
319 # op, mdatas = _cb_decode(cp)
320 # except:
321 # raise CopyError, eInvalid
322 #
323 # oblist = []
324 # app = self.getPhysicalRoot()
325 # for mdata in mdatas:
326 # m = Moniker.loadMoniker(mdata)
327 # try:
328 # ob = m.bind(app)
329 # except ConflictError:
330 # raise
331 # except:
332 # raise CopyError, eNotFound
333 # self._verifyObjectPaste(ob, validate_src=op+1)
334 # oblist.append(ob)
335 #
336 # result = []
337 # if op == 0:
338 # # Copy operation
339 # mtool = getToolByName(self, 'portal_membership')
340 # utool = getToolByName(self, 'portal_url')
341 # portal = utool.getPortalObject()
342 # userIsPortalManager = mtool.checkPermission(ManagePortal, portal)
343 #
344 # for ob in oblist:
345 # orig_id = ob.getId()
346 # if not ob.cb_isCopyable():
347 # raise CopyError, eNotSupported % escape(orig_id)
348 #
349 # try:
350 # ob._notifyOfCopyTo(self, op=0)
351 # except ConflictError:
352 # raise
353 # except:
354 # raise CopyError, MessageDialog(
355 # title="Copy Error",
356 # message=sys.exc_info()[1],
357 # action='manage_main')
358 #
359 # id = self._get_id(orig_id)
360 # result.append({'id': orig_id, 'new_id': id})
361 #
362 # orig_ob = ob
363 # ob = ob._getCopy(self)
364 # ob._setId(id)
365 # notify(ObjectCopiedEvent(ob, orig_ob))
366 #
367 # if not userIsPortalManager :
368 # self._setObject(id, ob, suppress_events=True)
369 # else :
370 # self._setObject(id, ob, suppress_events=True, set_owner=0)
371 # ob = self._getOb(id)
372 # ob.wl_clearLocks()
373 #
374 # ob._postCopy(self, op=0)
375 #
376 # OFS.subscribers.compatibilityCall('manage_afterClone', ob, ob)
377 #
378 # notify(ObjectClonedEvent(ob))
379 #
380 # if REQUEST is not None:
381 # return self.manage_main(self, REQUEST, update_menu=1,
382 # cb_dataValid=1)
383 #
384 # elif op == 1:
385 # # Move operation
386 # for ob in oblist:
387 # orig_id = ob.getId()
388 # if not ob.cb_isMoveable():
389 # raise CopyError, eNotSupported % escape(orig_id)
390 #
391 # try:
392 # ob._notifyOfCopyTo(self, op=1)
393 # except ConflictError:
394 # raise
395 # except:
396 # raise CopyError, MessageDialog(
397 # title="Move Error",
398 # message=sys.exc_info()[1],
399 # action='manage_main')
400 #
401 # if not sanity_check(self, ob):
402 # raise CopyError, "This object cannot be pasted into itself"
403 #
404 # orig_container = aq_parent(aq_inner(ob))
405 # if aq_base(orig_container) is aq_base(self):
406 # id = orig_id
407 # else:
408 # id = self._get_id(orig_id)
409 # result.append({'id': orig_id, 'new_id': id})
410 #
411 # notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
412 # self, id))
413 #
414 # # try to make ownership explicit so that it gets carried
415 # # along to the new location if needed.
416 # ob.manage_changeOwnershipType(explicit=1)
417 #
418 # try:
419 # orig_container._delObject(orig_id, suppress_events=True)
420 # except TypeError:
421 # orig_container._delObject(orig_id)
422 # warnings.warn(
423 # "%s._delObject without suppress_events is discouraged."
424 # % orig_container.__class__.__name__,
425 # DeprecationWarning)
426 # ob = aq_base(ob)
427 # ob._setId(id)
428 #
429 # try:
430 # self._setObject(id, ob, set_owner=0, suppress_events=True)
431 # except TypeError:
432 # self._setObject(id, ob, set_owner=0)
433 # warnings.warn(
434 # "%s._setObject without suppress_events is discouraged."
435 # % self.__class__.__name__, DeprecationWarning)
436 # ob = self._getOb(id)
437 #
438 # notify(ObjectMovedEvent(ob, orig_container, orig_id, self, id))
439 # notifyContainerModified(orig_container)
440 # if aq_base(orig_container) is not aq_base(self):
441 # notifyContainerModified(self)
442 #
443 # ob._postCopy(self, op=1)
444 # # try to make ownership implicit if possible
445 # ob.manage_changeOwnershipType(explicit=0)
446 #
447 # if REQUEST is not None:
448 # REQUEST['RESPONSE'].setCookie('__cp', 'deleted',
449 # path='%s' % cookie_path(REQUEST),
450 # expires='Wed, 31-Dec-97 23:59:59 GMT')
451 # REQUEST['__cp'] = None
452 # return self.manage_main(self, REQUEST, update_menu=1,
453 # cb_dataValid=0)
454 #
455 # return result
456
457
458 InitializeClass(PlinnFolder)
459 PlinnFolderFactory = Factory(PlinnFolder)
460
461 def _getDeepObjects(self, ctool, o, filter={}):
462 res = ctool.unrestrictedSearchResults(path = '/'.join(o.getPhysicalPath()), **filter)
463
464 if not res :
465 return []
466 else :
467 deepObjects = []
468 res = list(res)
469 res.sort(lambda a, b: cmp(a.getPath(), b.getPath()))
470 previousPath = res[0].getPath()
471
472 deepObjects.append(res[0].getObject())
473 for b in res[1:] :
474 currentPath = b.getPath()
475 if currentPath.startswith(previousPath) and len(currentPath) > len(previousPath):
476 continue
477 else :
478 deepObjects.append(b.getObject())
479 previousPath = currentPath
480
481 return deepObjects
482
483
484 manage_addPlinnFolder = PlinnFolder.manage_addPlinnFolder.im_func