Dans le cas (courant) où l’objet créé implémente DublinCore, alors on initialise...
[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 ob = self.PUT_factory(validId, typ, body)
327 self._setObject(validId, ob)
328 ob = self._getOb(validId)
329 if IDublinCore.providedBy(ob) :
330 ob.editMetadata(title=fileName,
331 format=typ)
332 httpRespCode = 201
333 else :
334 httpRespCode = 200
335 ob = self._getOb(validId)
336
337 # We call _verifyObjectPaste with verify_src=0, to see if the
338 # user can create this type of object (and we don't need to
339 # check the clipboard.
340 try:
341 self._verifyObjectPaste(ob.__of__(self), 0)
342 except CopyError:
343 sMsg = 'Unable to create object of class %s in %s: %s' % \
344 (ob.__class__, repr(self), sys.exc_info()[1],)
345 raise Unauthorized, sMsg
346
347 ob.PUT(REQUEST, RESPONSE)
348 ob.orig_name = fileName
349
350 # get method from ob created / refreshed
351 ti = ob.getTypeInfo()
352 method_id = ti.queryMethodID('jsupload_snippet')
353 meth = getattr(ob, method_id) if method_id else None
354 if not meth :
355 # get method from container that receive uploaded content
356 ti = self.getTypeInfo()
357 method_id = ti.queryMethodID('jsupload_snippet')
358 meth = getattr(self, method_id) if method_id else lambda : 'Not implemented'
359
360 RESPONSE.setStatus(httpRespCode)
361 RESPONSE.setHeader('Content-Type', 'text/xml;;charset=utf-8')
362 return '<fragment>%s</fragment>' % meth(ob).strip()
363
364
365 # ## overload to maintain ownership if authenticated user has 'Manage portal' permission
366 # def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
367 # """Paste previously copied objects into the current object.
368 #
369 # If calling manage_pasteObjects from python code, pass the result of a
370 # previous call to manage_cutObjects or manage_copyObjects as the first
371 # argument.
372 #
373 # Also sends IObjectCopiedEvent and IObjectClonedEvent
374 # or IObjectWillBeMovedEvent and IObjectMovedEvent.
375 # """
376 # if cb_copy_data is not None:
377 # cp = cb_copy_data
378 # elif REQUEST is not None and REQUEST.has_key('__cp'):
379 # cp = REQUEST['__cp']
380 # else:
381 # cp = None
382 # if cp is None:
383 # raise CopyError, eNoData
384 #
385 # try:
386 # op, mdatas = _cb_decode(cp)
387 # except:
388 # raise CopyError, eInvalid
389 #
390 # oblist = []
391 # app = self.getPhysicalRoot()
392 # for mdata in mdatas:
393 # m = Moniker.loadMoniker(mdata)
394 # try:
395 # ob = m.bind(app)
396 # except ConflictError:
397 # raise
398 # except:
399 # raise CopyError, eNotFound
400 # self._verifyObjectPaste(ob, validate_src=op+1)
401 # oblist.append(ob)
402 #
403 # result = []
404 # if op == 0:
405 # # Copy operation
406 # mtool = getToolByName(self, 'portal_membership')
407 # utool = getToolByName(self, 'portal_url')
408 # portal = utool.getPortalObject()
409 # userIsPortalManager = mtool.checkPermission(ManagePortal, portal)
410 #
411 # for ob in oblist:
412 # orig_id = ob.getId()
413 # if not ob.cb_isCopyable():
414 # raise CopyError, eNotSupported % escape(orig_id)
415 #
416 # try:
417 # ob._notifyOfCopyTo(self, op=0)
418 # except ConflictError:
419 # raise
420 # except:
421 # raise CopyError, MessageDialog(
422 # title="Copy Error",
423 # message=sys.exc_info()[1],
424 # action='manage_main')
425 #
426 # id = self._get_id(orig_id)
427 # result.append({'id': orig_id, 'new_id': id})
428 #
429 # orig_ob = ob
430 # ob = ob._getCopy(self)
431 # ob._setId(id)
432 # notify(ObjectCopiedEvent(ob, orig_ob))
433 #
434 # if not userIsPortalManager :
435 # self._setObject(id, ob, suppress_events=True)
436 # else :
437 # self._setObject(id, ob, suppress_events=True, set_owner=0)
438 # ob = self._getOb(id)
439 # ob.wl_clearLocks()
440 #
441 # ob._postCopy(self, op=0)
442 #
443 # OFS.subscribers.compatibilityCall('manage_afterClone', ob, ob)
444 #
445 # notify(ObjectClonedEvent(ob))
446 #
447 # if REQUEST is not None:
448 # return self.manage_main(self, REQUEST, update_menu=1,
449 # cb_dataValid=1)
450 #
451 # elif op == 1:
452 # # Move operation
453 # for ob in oblist:
454 # orig_id = ob.getId()
455 # if not ob.cb_isMoveable():
456 # raise CopyError, eNotSupported % escape(orig_id)
457 #
458 # try:
459 # ob._notifyOfCopyTo(self, op=1)
460 # except ConflictError:
461 # raise
462 # except:
463 # raise CopyError, MessageDialog(
464 # title="Move Error",
465 # message=sys.exc_info()[1],
466 # action='manage_main')
467 #
468 # if not sanity_check(self, ob):
469 # raise CopyError, "This object cannot be pasted into itself"
470 #
471 # orig_container = aq_parent(aq_inner(ob))
472 # if aq_base(orig_container) is aq_base(self):
473 # id = orig_id
474 # else:
475 # id = self._get_id(orig_id)
476 # result.append({'id': orig_id, 'new_id': id})
477 #
478 # notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
479 # self, id))
480 #
481 # # try to make ownership explicit so that it gets carried
482 # # along to the new location if needed.
483 # ob.manage_changeOwnershipType(explicit=1)
484 #
485 # try:
486 # orig_container._delObject(orig_id, suppress_events=True)
487 # except TypeError:
488 # orig_container._delObject(orig_id)
489 # warnings.warn(
490 # "%s._delObject without suppress_events is discouraged."
491 # % orig_container.__class__.__name__,
492 # DeprecationWarning)
493 # ob = aq_base(ob)
494 # ob._setId(id)
495 #
496 # try:
497 # self._setObject(id, ob, set_owner=0, suppress_events=True)
498 # except TypeError:
499 # self._setObject(id, ob, set_owner=0)
500 # warnings.warn(
501 # "%s._setObject without suppress_events is discouraged."
502 # % self.__class__.__name__, DeprecationWarning)
503 # ob = self._getOb(id)
504 #
505 # notify(ObjectMovedEvent(ob, orig_container, orig_id, self, id))
506 # notifyContainerModified(orig_container)
507 # if aq_base(orig_container) is not aq_base(self):
508 # notifyContainerModified(self)
509 #
510 # ob._postCopy(self, op=1)
511 # # try to make ownership implicit if possible
512 # ob.manage_changeOwnershipType(explicit=0)
513 #
514 # if REQUEST is not None:
515 # REQUEST['RESPONSE'].setCookie('__cp', 'deleted',
516 # path='%s' % cookie_path(REQUEST),
517 # expires='Wed, 31-Dec-97 23:59:59 GMT')
518 # REQUEST['__cp'] = None
519 # return self.manage_main(self, REQUEST, update_menu=1,
520 # cb_dataValid=0)
521 #
522 # return result
523
524
525 InitializeClass(PlinnFolder)
526 PlinnFolderFactory = Factory(PlinnFolder)
527
528 def _getDeepObjects(self, ctool, o, filter={}):
529 res = ctool.unrestrictedSearchResults(path = '/'.join(o.getPhysicalPath()), **filter)
530
531 if not res :
532 return []
533 else :
534 deepObjects = []
535 res = list(res)
536 res.sort(lambda a, b: cmp(a.getPath(), b.getPath()))
537 previousPath = res[0].getPath()
538
539 deepObjects.append(res[0].getObject())
540 for b in res[1:] :
541 currentPath = b.getPath()
542 if currentPath.startswith(previousPath) and len(currentPath) > len(previousPath):
543 continue
544 else :
545 deepObjects.append(b.getObject())
546 previousPath = currentPath
547
548 return deepObjects
549
550
551 manage_addPlinnFolder = PlinnFolder.manage_addPlinnFolder.im_func