Strip pour éviter que le firstChild soit un nœud text.
[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 OFS import Moniker
35 from ZODB.POSException import ConflictError
36 import OFS.subscribers
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.utils import getUtilityByInterfaceName
58 from Products.CMFCore.CMFCatalogAware import CMFCatalogAware
59 from Products.CMFCore.PortalFolder import PortalFolder, ContentFilter
60 from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
61
62 from zope.interface import implements
63 from Products.CMFCore.interfaces import IContentish
64
65 from utils import _checkMemberPermission
66 from utils import Message as _
67 from utils import makeValidId
68 from Globals import InitializeClass
69 from AccessControl import ClassSecurityInfo
70 from ZServer import LARGE_FILE_THRESHOLD
71 from webdav.interfaces import IWriteLock
72 from webdav.common import Locked
73 from webdav.common import PreconditionFailed
74 from zope.contenttype import guess_content_type
75
76
77 class PlinnFolder(CMFCatalogAware, PortalFolder, DefaultDublinCoreImpl) :
78 """ Plinn Folder """
79
80 implements(IContentish)
81
82 security = ClassSecurityInfo()
83
84 manage_options = PortalFolder.manage_options
85
86 ## change security for inherited methods
87 security.declareProtected(AddPortalContent, 'manage_pasteObjects')
88
89 def __init__( self, id, title='' ) :
90 PortalFolder.__init__(self, id)
91 DefaultDublinCoreImpl.__init__(self, title = title)
92
93 security.declarePublic('allowedContentTypes')
94 def allowedContentTypes(self):
95 """
96 List type info objects for types which can be added in this folder.
97 Types can be filtered using the localContentTypes attribute.
98 """
99 allowedTypes = PortalFolder.allowedContentTypes(self)
100 if hasattr(self, 'localContentTypes'):
101 allowedTypes = [t for t in allowedTypes if t.title in self.localContentTypes]
102 return allowedTypes
103
104 security.declareProtected(View, 'objectIdCanBeDeleted')
105 def objectIdCanBeDeleted(self, id) :
106 """ Check permissions and ownership and return True
107 if current user can delete object id.
108 """
109 if _checkPermission(DeleteObjects, self) : # std zope perm
110 return True
111
112 elif _checkPermission(DeletePortalContents, self):
113 mtool = getToolByName(self, 'portal_membership')
114 authMember = mtool.getAuthenticatedMember()
115 ob = getattr(self, id)
116 if authMember.allowed(ob, object_roles=['Owner'] ) and \
117 _checkPermission(DeleteOwnedObjects, ob) : return True
118
119 else :
120 return False
121
122
123 security.declareProtected(DeletePortalContents, 'manage_delObjects')
124 def manage_delObjects(self, ids=[], REQUEST=None):
125 """Delete subordinate objects.
126 A member can delete his owned contents (if he has the 'Delete Portal Contents' permission)
127 without 'Delete objects' permission in this folder.
128 Return skipped object ids.
129 """
130 notOwned = []
131 if _checkPermission(DeleteObjects, self) : # std zope perm
132 PortalFolder.manage_delObjects(self, ids=ids, REQUEST=REQUEST)
133 else :
134 mtool = getToolByName(self, 'portal_membership')
135 authMember = mtool.getAuthenticatedMember()
136 owned = []
137 if type(ids) == StringType :
138 ids = [ids]
139 for id in ids :
140 ob = self._getOb(id)
141 if authMember.allowed(ob, object_roles=['Owner'] ) and \
142 _checkPermission(DeleteOwnedObjects, ob) : owned.append(id)
143 else : notOwned.append(id)
144 if owned :
145 PortalFolder.manage_delObjects(self, ids=owned, REQUEST=REQUEST)
146
147 if REQUEST is not None:
148 return self.manage_main(
149 self, REQUEST,
150 manage_tabs_message='Object(s) deleted.',
151 update_menu=1)
152 return notOwned
153
154
155 security.declareProtected(AddPortalContent, 'manage_renameObjects')
156 def manage_renameObjects(self, ids=[], new_ids=[], REQUEST=None) :
157 """ Rename subordinate objects
158 A member can rename his owned contents if he has the 'Modify Portal Content' permission.
159 Returns skippend object ids.
160 """
161 if len(ids) != len(new_ids):
162 raise BadRequest(_('Please rename each listed object.'))
163
164 if _checkPermission(ViewManagementScreens, self) : # std zope perm
165 return super(PlinnFolder, self).manage_renameObjects(ids, new_ids, REQUEST)
166
167 mtool = getToolByName(self, 'portal_membership')
168 authMember = mtool.getAuthenticatedMember()
169 skiped = []
170 for id, new_id in zip(ids, new_ids) :
171 if id == new_id : continue
172
173 ob = self._getOb(id)
174 if authMember.allowed(ob, object_roles=['Owner'] ) and \
175 _checkPermission(ModifyPortalContent, ob) :
176 self.manage_renameObject(id, new_id)
177 else :
178 skiped.append(id)
179
180 if REQUEST is not None :
181 return self.manage_main(self, REQUEST, update_menu=1)
182
183 return skiped
184
185
186 security.declareProtected(ListFolderContents, 'listFolderContents')
187 def listFolderContents( self, contentFilter=None ):
188 """ List viewable contentish and folderish sub-objects.
189 """
190 items = self.contentItems(filter=contentFilter)
191 l = []
192 for id, obj in items:
193 if _checkPermission(View, obj) :
194 l.append(obj)
195
196 return l
197
198
199 security.declareProtected(ListFolderContents, 'listNearestFolderContents')
200 def listNearestFolderContents(self, contentFilter=None, userid=None, sorted=False) :
201 """ Return folder contents and traverse
202 recursively unaccessfull sub folders to find
203 accessible contents.
204 """
205
206 filt = {}
207 if contentFilter :
208 filt = contentFilter.copy()
209 ctool = getToolByName(self, 'portal_catalog')
210 mtool = getToolByName(self, 'portal_membership')
211
212 if userid and _checkPermission(CheckMemberPermission, getToolByName(self, 'portal_url').getPortalObject()) :
213 checkFunc = lambda perm, ob : _checkMemberPermission(userid, View, ob)
214 filt['allowedRolesAndUsers'] = ctool._listAllowedRolesAndUsers( mtool.getMemberById(userid) )
215 else :
216 checkFunc = _checkPermission
217 filt['allowedRolesAndUsers'] = ctool._listAllowedRolesAndUsers( mtool.getAuthenticatedMember() )
218
219
220 # copy from CMFCore.PortalFolder.PortalFolder._filteredItems
221 pt = filt.get('portal_type', [])
222 if type(pt) is type(''):
223 pt = [pt]
224 types_tool = getToolByName(self, 'portal_types')
225 allowed_types = types_tool.listContentTypes()
226 if not pt:
227 pt = allowed_types
228 else:
229 pt = [t for t in pt if t in allowed_types]
230 if not pt:
231 # After filtering, no types remain, so nothing should be
232 # returned.
233 return []
234 filt['portal_type'] = pt
235 #---
236
237 query = ContentFilter(**filt)
238 nearestObjects = []
239
240 for o in self.objectValues() :
241 if query(o) :
242 if checkFunc(View, o):
243 nearestObjects.append(o)
244 elif getattr(o.aq_self,'isAnObjectManager', False):
245 nearestObjects.extend(_getDeepObjects(self, ctool, o, filter=filt))
246
247 if sorted and len(nearestObjects) > 0 :
248 key, reverse = self.getDefaultSorting()
249 if key != 'position' :
250 indexCallable = callable(getattr(nearestObjects[0], key))
251 if indexCallable :
252 sortfunc = lambda a, b : cmp(getattr(a, key)(), getattr(b, key)())
253 else :
254 sortfunc = lambda a, b : cmp(getattr(a, key), getattr(b, key))
255 nearestObjects.sort(cmp=sortfunc, reverse=reverse)
256
257 return nearestObjects
258
259 security.declareProtected(ListFolderContents, 'listCatalogedContents')
260 def listCatalogedContents(self, contentFilter={}):
261 """ query catalog and returns brains of contents.
262 Requires ExtendedPathIndex
263 """
264 ctool = getUtilityByInterfaceName('Products.CMFCore.interfaces.ICatalogTool')
265 contentFilter['path'] = {'query':'/'.join(self.getPhysicalPath()),
266 'depth':1}
267 return ctool(sort_on='position', **contentFilter)
268
269 security.declarePublic('synContentValues')
270 def synContentValues(self):
271 # value for syndication
272 return self.listNearestFolderContents()
273
274 security.declareProtected(View, 'SearchableText')
275 def SearchableText(self) :
276 """ for full text indexation
277 """
278 return '%s %s' % (self.title, self.description)
279
280 security.declareProtected(AddPortalFolders, 'manage_addPlinnFolder')
281 def manage_addPlinnFolder(self, id, title='', REQUEST=None):
282 """Add a new PortalFolder object with id *id*.
283 """
284 ob=PlinnFolder(id, title)
285 # from CMFCore.PortalFolder.PortalFolder :-)
286 self._setObject(id, ob)
287 if REQUEST is not None:
288 return self.folder_contents( # XXX: ick!
289 self, REQUEST, portal_status_message="Folder added")
290
291
292 security.declareProtected(AddPortalContent, 'put_upload')
293 def put_upload(self, REQUEST, RESPONSE):
294 """ Upload a content thru webdav put method.
295 The default behavior (NullRessource.PUT + PortalFolder.PUT_factory)
296 disallow files names with '_' at the begining.
297 """
298
299 self.dav__init(REQUEST, RESPONSE)
300
301 fileName = 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
347 ti = ob.getTypeInfo()
348 method_id = ti.queryMethodID('jsupload_snippet')
349 meth = method_id and getattr(ob, method_id) or (lambda : 'Not implemented')
350 RESPONSE.setStatus(httpRespCode)
351 RESPONSE.setHeader('Content-Type', 'text/xml;;charset=utf-8')
352 return '<fragment>%s></fragment>' % meth().strip()
353
354
355 # ## overload to maintain ownership if authenticated user has 'Manage portal' permission
356 # def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
357 # """Paste previously copied objects into the current object.
358 #
359 # If calling manage_pasteObjects from python code, pass the result of a
360 # previous call to manage_cutObjects or manage_copyObjects as the first
361 # argument.
362 #
363 # Also sends IObjectCopiedEvent and IObjectClonedEvent
364 # or IObjectWillBeMovedEvent and IObjectMovedEvent.
365 # """
366 # if cb_copy_data is not None:
367 # cp = cb_copy_data
368 # elif REQUEST is not None and REQUEST.has_key('__cp'):
369 # cp = REQUEST['__cp']
370 # else:
371 # cp = None
372 # if cp is None:
373 # raise CopyError, eNoData
374 #
375 # try:
376 # op, mdatas = _cb_decode(cp)
377 # except:
378 # raise CopyError, eInvalid
379 #
380 # oblist = []
381 # app = self.getPhysicalRoot()
382 # for mdata in mdatas:
383 # m = Moniker.loadMoniker(mdata)
384 # try:
385 # ob = m.bind(app)
386 # except ConflictError:
387 # raise
388 # except:
389 # raise CopyError, eNotFound
390 # self._verifyObjectPaste(ob, validate_src=op+1)
391 # oblist.append(ob)
392 #
393 # result = []
394 # if op == 0:
395 # # Copy operation
396 # mtool = getToolByName(self, 'portal_membership')
397 # utool = getToolByName(self, 'portal_url')
398 # portal = utool.getPortalObject()
399 # userIsPortalManager = mtool.checkPermission(ManagePortal, portal)
400 #
401 # for ob in oblist:
402 # orig_id = ob.getId()
403 # if not ob.cb_isCopyable():
404 # raise CopyError, eNotSupported % escape(orig_id)
405 #
406 # try:
407 # ob._notifyOfCopyTo(self, op=0)
408 # except ConflictError:
409 # raise
410 # except:
411 # raise CopyError, MessageDialog(
412 # title="Copy Error",
413 # message=sys.exc_info()[1],
414 # action='manage_main')
415 #
416 # id = self._get_id(orig_id)
417 # result.append({'id': orig_id, 'new_id': id})
418 #
419 # orig_ob = ob
420 # ob = ob._getCopy(self)
421 # ob._setId(id)
422 # notify(ObjectCopiedEvent(ob, orig_ob))
423 #
424 # if not userIsPortalManager :
425 # self._setObject(id, ob, suppress_events=True)
426 # else :
427 # self._setObject(id, ob, suppress_events=True, set_owner=0)
428 # ob = self._getOb(id)
429 # ob.wl_clearLocks()
430 #
431 # ob._postCopy(self, op=0)
432 #
433 # OFS.subscribers.compatibilityCall('manage_afterClone', ob, ob)
434 #
435 # notify(ObjectClonedEvent(ob))
436 #
437 # if REQUEST is not None:
438 # return self.manage_main(self, REQUEST, update_menu=1,
439 # cb_dataValid=1)
440 #
441 # elif op == 1:
442 # # Move operation
443 # for ob in oblist:
444 # orig_id = ob.getId()
445 # if not ob.cb_isMoveable():
446 # raise CopyError, eNotSupported % escape(orig_id)
447 #
448 # try:
449 # ob._notifyOfCopyTo(self, op=1)
450 # except ConflictError:
451 # raise
452 # except:
453 # raise CopyError, MessageDialog(
454 # title="Move Error",
455 # message=sys.exc_info()[1],
456 # action='manage_main')
457 #
458 # if not sanity_check(self, ob):
459 # raise CopyError, "This object cannot be pasted into itself"
460 #
461 # orig_container = aq_parent(aq_inner(ob))
462 # if aq_base(orig_container) is aq_base(self):
463 # id = orig_id
464 # else:
465 # id = self._get_id(orig_id)
466 # result.append({'id': orig_id, 'new_id': id})
467 #
468 # notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
469 # self, id))
470 #
471 # # try to make ownership explicit so that it gets carried
472 # # along to the new location if needed.
473 # ob.manage_changeOwnershipType(explicit=1)
474 #
475 # try:
476 # orig_container._delObject(orig_id, suppress_events=True)
477 # except TypeError:
478 # orig_container._delObject(orig_id)
479 # warnings.warn(
480 # "%s._delObject without suppress_events is discouraged."
481 # % orig_container.__class__.__name__,
482 # DeprecationWarning)
483 # ob = aq_base(ob)
484 # ob._setId(id)
485 #
486 # try:
487 # self._setObject(id, ob, set_owner=0, suppress_events=True)
488 # except TypeError:
489 # self._setObject(id, ob, set_owner=0)
490 # warnings.warn(
491 # "%s._setObject without suppress_events is discouraged."
492 # % self.__class__.__name__, DeprecationWarning)
493 # ob = self._getOb(id)
494 #
495 # notify(ObjectMovedEvent(ob, orig_container, orig_id, self, id))
496 # notifyContainerModified(orig_container)
497 # if aq_base(orig_container) is not aq_base(self):
498 # notifyContainerModified(self)
499 #
500 # ob._postCopy(self, op=1)
501 # # try to make ownership implicit if possible
502 # ob.manage_changeOwnershipType(explicit=0)
503 #
504 # if REQUEST is not None:
505 # REQUEST['RESPONSE'].setCookie('__cp', 'deleted',
506 # path='%s' % cookie_path(REQUEST),
507 # expires='Wed, 31-Dec-97 23:59:59 GMT')
508 # REQUEST['__cp'] = None
509 # return self.manage_main(self, REQUEST, update_menu=1,
510 # cb_dataValid=0)
511 #
512 # return result
513
514
515 InitializeClass(PlinnFolder)
516 PlinnFolderFactory = Factory(PlinnFolder)
517
518 def _getDeepObjects(self, ctool, o, filter={}):
519 res = ctool.unrestrictedSearchResults(path = '/'.join(o.getPhysicalPath()), **filter)
520
521 if not res :
522 return []
523 else :
524 deepObjects = []
525 res = list(res)
526 res.sort(lambda a, b: cmp(a.getPath(), b.getPath()))
527 previousPath = res[0].getPath()
528
529 deepObjects.append(res[0].getObject())
530 for b in res[1:] :
531 currentPath = b.getPath()
532 if currentPath.startswith(previousPath) and len(currentPath) > len(previousPath):
533 continue
534 else :
535 deepObjects.append(b.getObject())
536 previousPath = currentPath
537
538 return deepObjects
539
540
541 manage_addPlinnFolder = PlinnFolder.manage_addPlinnFolder.im_func