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