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