root/CPS3/products/CPSCore/tags/CPS-3.4.2/ProxyBase.py

Revision 47241, 54.6 kB (checked in by ogrisel, 3 years ago)

s/zLOG/logging on CPSCore.ProxyBase?

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1 # (C) Copyright 2002, 2003 Nuxeo SARL <http://nuxeo.com>
2 # Author: Florent Guillaume <fg@nuxeo.com>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License version 2 as published
6 # by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
16 # 02111-1307, USA.
17 #
18 # $Id$
19
20 import os
21 from logging import getLogger
22 from types import DictType
23 from ExtensionClass import Base
24 from cPickle import Pickler, Unpickler
25 from cStringIO import StringIO
26 import tempfile
27 from zipfile import ZipFile
28 from struct import pack, unpack
29 from ComputedAttribute import ComputedAttribute
30 from Globals import InitializeClass, DTMLFile
31 from AccessControl import ClassSecurityInfo
32 from AccessControl import Unauthorized
33 import Acquisition
34 from Acquisition import aq_base, aq_parent, aq_inner
35 from OFS.SimpleItem import Item
36 from OFS.Image import File
37 from OFS.Traversable import Traversable
38 from webdav.WriteLockInterface import WriteLockInterface
39
40 import zope.interface
41 from Products.CPSCore.interfaces import ICPSProxy
42
43 from Products.CMFCore.utils import getToolByName
44 from Products.CMFCore.permissions import View
45 from Products.CMFCore.permissions import ModifyPortalContent
46 from Products.CMFCore.permissions import ViewManagementScreens
47 from Products.CPSCore.permissions import ViewArchivedRevisions
48 from Products.CPSCore.permissions import ChangeSubobjectsOrder
49 from Products.CMFCore.CMFCatalogAware import CMFCatalogAware
50
51 from Products.CPSUtil.timeoutcache import getCache
52 from Products.CPSUtil.integration import isUserAgentMsie
53 from Products.CPSCore.utils import KEYWORD_DOWNLOAD_FILE, \
54      KEYWORD_ARCHIVED_REVISION, KEYWORD_SWITCH_LANGUAGE, \
55      KEYWORD_VIEW_LANGUAGE, KEYWORD_VIEW_ZIP, SESSION_LANGUAGE_KEY, \
56      REQUEST_LANGUAGE_KEY
57 from Products.CPSCore.EventServiceTool import getEventService
58 from Products.CPSCore.CPSBase import CPSBaseFolder
59 from Products.CPSCore.CPSBase import CPSBaseDocument
60 from Products.CPSCore.CPSBase import CPSBaseBTreeFolder
61
62 from Products.CPSCore.IndexationManager import get_indexation_manager
63
64
65 PROBLEMATIC_FILES_SUFFIXES = ('.exe', '.sxw', '.sxc')
66 CACHE_ZIP_VIEW_KEY = 'CPS_ZIP_VIEW'
67 CACHE_ZIP_VIEW_TIMEOUT = 7200           # time to cache in second
68
69 logger = getLogger('CPSCore.ProxyBase')
70
71 class ProxyBase(Base):
72     """Mixin class for proxy types.
73
74     A proxy stores:
75
76     - docid, the document family it points to.
77
78     - language_revs, a mapping of language -> revision. The revision is
79       an integer representing a single revision of a document.
80
81     - tag, an optional integer tag. A tag is an abstract reference to
82       the mapping language -> revision. It is used to provide branch
83       lineage between proxies. """
84
85     zope.interface.implements(ICPSProxy)
86
87     security = ClassSecurityInfo()
88     use_mcat = 0
89
90     def __init__(self, docid=None, default_language=None, language_revs=None,
91                  from_language_revs=None, tag=None):
92         self._docid = docid
93         self._default_language = default_language
94         self._language_revs = language_revs or {}
95         self._from_language_revs = from_language_revs or {}
96         self._tag = None
97
98     #
99     # API
100     #
101
102     # XXX was def getRepoId(self):
103     security.declareProtected(View, 'getDocid')
104     def getDocid(self):
105         """Get the docid for this proxy."""
106         return self._docid
107
108     security.declarePrivate('setDocid')
109     def setDocid(self, docid):
110         """Set the docid of a proxy.
111
112         (Used when proxies are unshared.)
113         """
114         self._docid = docid
115
116     security.declareProtected(View, 'getDefaultLanguage')
117     def getDefaultLanguage(self):
118         """Get the default language for this proxy."""
119         return self._default_language
120
121     security.declarePrivate('setDefaultLanguage')
122     def setDefaultLanguage(self, default_language):
123         """Set the default language for this proxy."""
124         # Called by CPSWorkflow after creating an empty proxy.
125         self._default_language = default_language
126
127     # XXX was def getVersionInfos(self):
128     security.declareProtected(View, 'getLanguageRevisions')
129     def getLanguageRevisions(self):
130         """Get the mapping of language -> revision."""
131         return self._language_revs.copy()
132
133     security.declarePrivate('_getLanguageRevisions')
134     def _getLanguageRevisions(self):
135         """Get the mapping of language -> revision, without copy.
136
137         (Called by ProxyTool.)
138         """
139         return self._language_revs
140
141     security.declarePrivate('setLanguageRevision')
142     def setLanguageRevision(self, lang, rev):
143         """Set the revision for a language.
144
145         Does not notify the event service.
146
147         (Called by ProxyTool.)
148         """
149         self._p_changed = 1
150         self._language_revs[lang] = rev
151
152     security.declareProtected(View, 'getFromLanguageRevisions')
153     def getFromLanguageRevisions(self):
154         """Get the originating language mapping for this proxy.
155
156         This is used by checkout and checkin mechanism to find what the
157         origin of a given proxy is.
158         """
159         return self._from_language_revs.copy()
160
161     security.declarePrivate('_getFromLanguageRevisions')
162     def _getFromLanguageRevisions(self):
163         """Get the originating language mapping for this proxy (no copy)."""
164         return self._from_language_revs
165
166     security.declarePrivate('setFromLanguageRevisions')
167     def setFromLanguageRevisions(self, from_language_revs):
168         """Set the originating language mapping for this proxy."""
169         self._from_language_revs = from_language_revs
170
171     security.declareProtected(View, 'getTag')
172     def getTag(self):
173         """Get the tag for this proxy, or None."""
174         return self._tag
175
176     security.declarePrivate('setTag')
177     def setTag(self, tag):
178         """Set the tag for this proxy."""
179         self._tag = tag
180
181     security.declareProtected(View, 'getLanguage')
182     def getLanguage(self, lang=None):
183         """Get the selected language for a proxy."""
184         pxtool = getToolByName(self, 'portal_proxies')
185         return pxtool.getBestRevision(self, lang=lang)[0]
186
187     security.declareProtected(View, 'getRevision')
188     def getRevision(self, lang=None):
189         """Get the best revision for a proxy."""
190         pxtool = getToolByName(self, 'portal_proxies')
191         return pxtool.getBestRevision(self, lang=lang)[1]
192
193     security.declareProtected(View, 'getContent')
194     def getContent(self, lang=None, rev=None):
195         """Get the content object referred to by this proxy.
196
197         The returned object may depend on the current language.
198
199         If rev is passed, this specific revision is returned.
200         """
201         return self._getContent(lang=lang, rev=rev)
202
203     security.declareProtected(ModifyPortalContent, 'getEditableContent')
204     def getEditableContent(self, lang=None):
205         """Get the editable content object referred to by this proxy.
206
207         The returned object may depend on the current language.
208         """
209         return self._getContent(lang=lang, editable=1)
210
211     security.declarePrivate('_getContent')
212     def _getContent(self, lang=None, rev=None, editable=0):
213         """Get the content object, maybe editable.
214
215         Returns an object acquisition-wrapped under the proxy itself,
216         so that security is correctly inferred.
217         """
218         pxtool = getToolByName(self, 'portal_proxies')
219         ob = pxtool.getContent(self, lang=lang, rev=rev, editable=editable)
220         # Rewrapping plays havoc with absolute_url, but that's not a
221         # problem as nobody should ever get the absolute_url of an
222         # object in the repository. In the previous implementation
223         # anyway you got URLs containing portal_repository/ which is bad
224         # too.
225         if ob is None:
226             return None
227         return aq_base(ob).__of__(self)
228
229     security.declarePrivate('proxyChanged')
230     def proxyChanged(self):
231         """Do necessary notifications after a proxy was changed."""
232         self.reindexObject()
233         pxtool = getToolByName(self, 'portal_proxies')
234         utool = getToolByName(self, 'portal_url')
235         rpath = utool.getRelativeUrl(self)
236         pxtool._modifyProxy(self, rpath) # XXX or directly event ?
237         evtool = getEventService(self)
238         evtool.notify('sys_modify_object', self, {})
239
240     def __getitem__(self, name):
241         """Transparent traversal of the proxy to the real subobjects.
242
243         Used for skins that don't take proxies enough into account.
244         Returns an object wrapped in the acquisition context of the proxy.
245
246         Parses URL revision switch of the form:
247           mydoc/archivedRevision/n/...
248
249         Parses URL translation switch of the form:
250           mydoc/switchLanguage/<lang>/...
251
252         Parses URLs for download of the form:
253           mydoc/downloadFile/attrname/mydocname.pdf
254         """
255         if name == KEYWORD_ARCHIVED_REVISION:
256             if self.isProxyArchived():
257                 raise KeyError(name)
258             switcher = RevisionSwitcher(self)
259             return switcher.__of__(self)
260         elif name == KEYWORD_SWITCH_LANGUAGE:
261             if self.isProxyArchived():
262                 raise KeyError(name)
263             switcher = LanguageSwitcher(self)
264             return switcher.__of__(self)
265         elif name == KEYWORD_VIEW_LANGUAGE:
266             if self.isProxyArchived():
267                 raise KeyError(name)
268             viewer = LanguageViewer(self)
269             return viewer.__of__(self)
270         ob = self._getContent()
271         if ob is None:
272             raise KeyError(name)
273         if name == KEYWORD_DOWNLOAD_FILE:
274             downloader = FileDownloader(ob, self)
275             return downloader.__of__(self)
276         elif name == KEYWORD_VIEW_ZIP:
277             zipview = ViewZip(ob, self)
278             return zipview.__of__(self)
279         try:
280             res = getattr(ob, name)
281         except AttributeError:
282             try:
283                 res = ob[name]
284             except (KeyError, IndexError, TypeError, AttributeError):
285                 raise KeyError, name
286         return res
287
288     #
289     # Freezing
290     #
291
292     security.declareProtected(ModifyPortalContent, 'freezeProxy')
293     def freezeProxy(self, REQUEST=None):
294         """Freeze the proxy.
295
296         Freezing means that any attempt at modification will create a new
297         revision. This allows for lazy copying.
298
299         (Called by CPSWorkflow.)
300         """
301         if REQUEST is not None:
302             raise Unauthorized('Not allowed through the web')
303         pxtool = getToolByName(self, 'portal_proxies')
304         self._freezeProxy(self, pxtool)
305
306     security.declarePrivate('_freezeProxy')
307     def _freezeProxy(self, ob, pxtool):
308         """Freeze the proxy."""
309         # XXX use an event?
310         pxtool.freezeProxy(self)
311
312     security.declarePublic('isProxyArchived')
313     def isProxyArchived(self):
314         """Is the proxy archived. False."""
315         return 0
316
317     #
318     # Staging
319     #
320
321     security.declarePrivate('serializeProxy')
322     def serializeProxy(self):
323         """Serialize the proxy, without subobjects.
324
325         Assumes no persistent attributes.
326         Assumes a normal ObjectManager with _objects.
327         Does NOT export subobjects from objectValues().
328         """
329         # skip subobjects
330         skipids = self.objectIds()
331         skipids.append('_objects')
332         # other skipped ids
333         skipids.extend(['_owner',
334                         'workflow_history', # is persistent...
335                         ])
336         # ok ids
337         okids = ['id',
338                  'portal_type',
339                  '_properties',
340                  '__ac_local_roles__',
341                  '__ac_local_group_roles__',
342                  # proxy definition
343                  '_docid',
344                  '_default_language',
345                  '_language_revs',
346                  '_from_language_revs',
347                  '_tag',
348                  # dublin core
349                  'title',
350                  'description',
351                  'subject',
352                  'creation_date',
353                  'modification_date',
354                  'effective_date',
355                  'expiration_date',
356                  'rights',
357                  'format',
358                  'language',
359                  'contributors',
360                  ]
361         # writable properties are ok too
362         for prop in self._properties:
363             if 'w' in prop.get('mode', 'w'):
364                 okids.append(prop['id'])
365         # extract
366         stuff = {}
367         for k, v in self.__dict__.items():
368             if k in skipids:
369                 continue
370             if k in okids:
371                 stuff[k] = v
372                 continue
373             # security
374             if k.startswith('_') and k.endswith('_Permission'):
375                 stuff[k] = v
376                 continue
377             logger.debug("Warning: serialize of %s found unknown %s=%s",
378                          self.getId(), k, v)
379             stuff[k] = v # Serialize it anyway
380         # now serialize stuff
381         f = StringIO()
382         p = Pickler(f, 1)
383         p.dump(stuff)
384         ser = f.getvalue()
385         class_name = self.__class__.__name__
386         return (pack('>I', len(class_name))
387                 +class_name
388                 +ser)
389
390     #
391     # Security
392     #
393
394     def _reindexObject(self, idxs=[]):
395         """Called to reindex when the object has changed."""
396         logger.debug("reindex idxs=%s for %s", idxs,
397                      '/'.join(self.getPhysicalPath()))
398         if 'allowedRolesAndUsers' in idxs:
399             # Both must be updated
400             idxs.append('localUsersWithRoles')
401         return CMFCatalogAware.reindexObject(self, idxs=idxs)
402
403     # overloaded
404     def reindexObject(self, idxs=[]):
405         """Schedule object for reindexation
406         """
407         get_indexation_manager().push(self, idxs=idxs)
408
409     # overloaded
410     def indexObject(self):
411         """Schedule object for indexing.
412         """
413         get_indexation_manager().push(self, idxs=[])
414
415     def _reindexObjectSecurity(self, skip_self=False):
416         """Called to security-related indexes."""
417         logger.debug("reindex security for %s",
418                      '/'.join(self.getPhysicalPath()))
419         # Notify that this proxy's security has changed.
420         # Listeners will have to recurse if necessary.
421         # (The notification for the object repo is done by the repo.)
422         evtool = getEventService(self)
423         evtool.notify('sys_modify_security', self, {})
424         return CMFCatalogAware.reindexObjectSecurity(self, skip_self)
425
426     # overloaded
427     def reindexObjectSecurity(self):
428         get_indexation_manager().push(self, with_security=True)
429
430     #
431     # Helpers
432     #
433
434     security.declarePublic('Title')
435     def Title(self):
436         """The object's title."""
437         title = ''
438         ob = self.getContent()
439         title = ob.Title()
440         if self.use_mcat and title:
441             translation_service = getToolByName(self, 'translation_service',
442                                                 None)
443             if translation_service is not None:
444                 title = translation_service(msgid=title, default=title)
445         return title
446
447     security.declarePublic('title_or_id')
448     def title_or_id(self):
449         """The object's title or id."""
450         return self.Title() or self.getId()
451
452     security.declarePublic('SearchableText')
453     def SearchableText(self):
454         """No searchable text."""
455         return ''
456
457     def Type(self):
458         """Dublin core Type."""
459         # Used by main_template.
460         ob = self.getContent()
461         if ob is not None:
462             return ob.Type()
463         else:
464             return ''
465
466     security.declarePublic('getProxyLanguages')
467     def getProxyLanguages(self):
468         """Return all available languages."""
469         return self._getLanguageRevisions().keys()
470
471     #
472     # Helper for I18n catalog
473     #
474     security.declarePrivate('_getAllMCatalogTranslation')
475     def _getAllMCatalogTranslation(self, msgid):
476         """Return a dict of message catalog translation."""
477         translation_service = getToolByName(self, 'translation_service', None)
478         if translation_service is None:
479             return {'en': msgid}
480         ret = {}
481         for locale in translation_service.getSupportedLanguages():
482             ret[locale] = translation_service(msgid=msgid,
483                                               target_language=locale,
484                                               default=msgid)
485         return ret
486
487     security.declareProtected(View, 'getL10nTitles')
488     def getL10nTitles(self):
489         """Return a dict of title in all available languages.
490
491         This is used for catalog metadata, see the indexableWrapperObject
492         to understand how it is used as metadata."""
493         ret = {}
494         if self.use_mcat:
495             title = self.getContent().Title()
496             ret = self._getAllMCatalogTranslation(title)
497         else:
498             for locale in self.getProxyLanguages():
499                 ob = self.getContent(lang=locale)
500                 ret[locale] = ob.Title()
501         return ret
502
503     security.declareProtected(View, 'getL10nDescriptions')
504     def getL10nDescriptions(self):
505         """Return a dict of description in all available languages.
506
507         This is used for catalog metadata."""
508         ret = {}
509         if self.use_mcat:
510             desc = self.getContent().Description()
511             ret = self._getAllMCatalogTranslation(desc)
512         for locale in self.getProxyLanguages():
513             ob = self.getContent(lang=locale)
514             ret[locale] = ob.Description()
515         return ret
516
517     security.declareProtected(View, 'isDefaultLanguage')
518     def isDefaultLanguage(self):
519         """Return 1 if proxy is the default language proxy.
520
521         This is used as catalog index."""
522         if self.getLanguage() == self.getDefaultLanguage():
523             return 1
524         return 0
525
526     #
527     # Helper for proxy folderish documents
528     #
529     security.declareProtected(View, 'isOutsideProxyFolderishDocument')
530     def isOutsideProxyFolderishDocument(self):
531         """Returns true if outside any proxy folderish document."""
532         container = aq_parent(aq_inner(self))
533         if hasattr(container, 'thisProxyFolderishDocument'):
534             return 0
535         else:
536             return 1
537
538     security.declareProtected(View, 'isCPSFolderish')
539     def isCPSFolderish(self):
540         """Return True if the document is a structural folderish."""
541         if not self.isPrincipiaFolderish:
542             # structural document must be folderish
543             return False
544         ttool = getToolByName(self, 'portal_types')
545         if getattr(ttool[self.portal_type],
546                    'cps_display_as_document_in_listing', False):
547             # this folderish document is not structural
548             return False
549         return True
550
551     #
552     # Revision management
553     #
554
555     security.declareProtected(ModifyPortalContent, 'revertToRevisions')
556     def revertToRevisions(self, language_revs, freeze=1):
557         """Revert this proxy to older revisions.
558
559         If freeze=1 (default), freeze the current revisions.
560         """
561         pxtool = getToolByName(self, 'portal_proxies')
562         pxtool.revertProxyToRevisions(self, language_revs, freeze)
563
564     security.declareProtected(ModifyPortalContent, 'delArchivedRevisions')
565     def delArchivedRevisions(self, revs):
566         """Delete some archived revisions of this proxy.
567         """
568         pxtool = getToolByName(self, 'portal_proxies')
569         pxtool.delProxyArchivedRevisions(self, revs)
570
571     security.declareProtected(View, 'getArchivedInfos')
572     def getArchivedInfos(self):
573         """Get info about the archived revisions for this proxy.
574
575         Returns a list of dicts with info:
576           rev, lang, modified, rpaths, is_frozen
577
578         (Called by user code to display a history.)
579         """
580         docid = self.getDocid()
581         pxtool = getToolByName(self, 'portal_proxies')
582         return pxtool.getArchivedInfosForDocid(docid)
583
584     #
585     # Translation helpers.
586     #
587
588     security.declareProtected(ModifyPortalContent, 'addLanguageToProxy')
589     def addLanguageToProxy(self, lang, from_lang=None, REQUEST=None, *args,
590                            **kw):
591         """Add a new language."""
592         if REQUEST is not None:
593             raise Unauthorized("Not accessible TTW")
594         pxtool = getToolByName(self, 'portal_proxies')
595         rev = pxtool.createRevision(self, lang, from_lang, *args, **kw)
596         return rev
597
598     security.declareProtected(ModifyPortalContent, 'delLanguageFromProxy')
599     def delLanguageFromProxy(self, lang, REQUEST=None):
600         """Delete a language.
601
602         Cannot delete the default language or the last remaining language.
603         """
604         if REQUEST is not None:
605             raise Unauthorized("Not accessible TTW")
606         if lang == self.getDefaultLanguage():
607             raise ValueError("Cannot delete default language '%s'" % lang)
608         language_revs = self._getLanguageRevisions()
609         if not language_revs.has_key(lang):
610             raise ValueError("Cannot delete invalid language '%s'" % lang)
611         if len(language_revs) == 1:
612             raise ValueError("Cannot delete last language '%s'" % lang)
613         del language_revs[lang]
614         self._language_revs = language_revs # XXX no setLanguageRevisions
615         self.proxyChanged()
616
617     #
618     # ZMI
619     #
620
621     proxybase_manage_options = (
622         {'label': 'Proxy',
623          'action': 'manage_proxyInfo',
624          },
625         )
626
627     security.declareProtected(ViewManagementScreens, 'manage_proxyInfo')
628     manage_proxyInfo = DTMLFile('zmi/proxy_info', globals())
629
630     _properties = (
631         {'id':'PropDocid', 'type':'string', 'mode':''},
632         {'id':'PropDefaultLanguage', 'type':'string', 'mode':''},
633         {'id':'PropLanguageRevisions', 'type':'string', 'mode':''},
634         {'id':'PropFromLanguageRevisions', 'type':'string', 'mode':''},
635         {'id':'PropTag', 'type':'string', 'mode':''},
636         )
637     PropDocid = ComputedAttribute(getDocid, 1)
638     PropDefaultLanguage = ComputedAttribute(getDefaultLanguage, 1)
639     PropLanguageRevisions = ComputedAttribute(getLanguageRevisions, 1)
640     PropFromLanguageRevisions = ComputedAttribute(getFromLanguageRevisions, 1)
641     PropTag = ComputedAttribute(getTag, 1)
642
643 InitializeClass(ProxyBase)
644
645
646 class FileDownloader(Acquisition.Explicit):
647     """Intermediate object allowing for file download.
648
649     Returned by a proxy during traversal of .../downloadFile/.
650
651     Parses URLs of the form .../downloadFile/attrname/mydocname.pdf
652     """
653
654     __implements__ = (WriteLockInterface,)
655
656     security = ClassSecurityInfo()
657     security.declareObjectPublic()
658
659     logger = getLogger('CPSCore.ProxyBase.FileDownloader')
660
661     def __init__(self, ob, proxy):
662         """
663         Init the FileDownloader with the document and proxy to which it pertains.
664
665         ob is the document that owns the file and proxy is the proxy of this
666         same document
667         """
668         self.ob = ob
669         self.proxy = proxy
670         self.state = 0
671         self.attrname = None
672         self.file = None
673         self.filename = None
674
675     def __repr__(self):
676         s = '<FileDownloader for %s' % `self.ob`
677         if self.state > 0:
678             s += '/'+self.attrname
679         if self.state > 1:
680             s += '/'+self.filename
681         s += '>'
682         return s
683
684     def __bobo_traverse__(self, request, name):
685         state = self.state
686         ob = self.ob
687         if state == 0:
688             # First call, swallow attribute
689             if not hasattr(aq_base(ob), name):
690                 logger.debug("Not a base attribute: '%s'", name)
691                 raise KeyError(name)
692             file = getattr(ob, name)
693             if file is not None and not isinstance(file, File):
694                 logger.debug("Attribute '%s' is not a File but %s",
695                              name, `file`)
696                 raise KeyError(name)
697             self.attrname = name
698             self.file = file
699             self.state = 1
700             return self
701         elif state == 1:
702             # Got attribute, swallow filename
703             self.filename = name
704             self.state = 2
705             self.meta_type = getattr(self.file, 'meta_type', '')
706             return self
707         elif name in ('index_html', 'absolute_url', 'content_type',
708                       'HEAD', 'PUT', 'LOCK', 'UNLOCK',):
709             return getattr(self, name)
710         else:
711             raise KeyError(name)
712
713     security.declareProtected(View, 'absolute_url')
714     def absolute_url(self):
715         url = self.proxy.absolute_url() + '/' + KEYWORD_DOWNLOAD_FILE
716         if self.state > 0:
717             url += '/' + self.attrname
718         if self.state > 1:
719             url += '/' + self.filename
720         return url
721
722     security.declareProtected(View, 'content_type')
723     def content_type(self):
724         if self.state != 2:
725             return None
726         return self.file.content_type
727
728     security.declareProtected(View, 'index_html')
729     def index_html(self, REQUEST, RESPONSE):
730         """Publish the file or image."""
731         if self.state != 2:
732             return None
733         file = self.file
734         if file is not None:
735             file_basename, file_suffix = os.path.splitext(self.filename)
736             # This code is here to allow MSIE, and potentially other browsers,
737             # to retrieve some files as ZIP archives, eg. without using its
738             # plugins since those plugins may fail in some circumstances.
739             if (isUserAgentMsie(REQUEST) and
740                 file_suffix in PROBLEMATIC_FILES_SUFFIXES):
741                 RESPONSE.setHeader('Content-Type', 'application/zip')
742                 RESPONSE.setHeader('Content-disposition',
743                                    'attachment; filename=%s%s'
744                                    % (file_basename, '.zip'))
745                 fd, archive_filepath = tempfile.mkstemp(suffix='.zip')
746                 archive_file = ZipFile(archive_filepath, 'w')
747                 archive_file.writestr(self.filename, str(file))
748                 os.close(fd)
749                 archive_file = open(archive_filepath, 'r')
750                 out = archive_file.read()
751                 archive_file.close()
752                 os.unlink(archive_filepath)
753                 return out
754
755             return file.index_html(REQUEST, RESPONSE)
756         else:
757             RESPONSE.setHeader('Content-Type', 'text/plain')
758             RESPONSE.setHeader('Content-Length', '0')
759             return ''
760
761     # Attribut checked by ExternalEditor to know if it can "WebDAV" on this
762     # object.
763     def EditableBody(self):
764         if self.state != 2:
765             return None
766         file = self.file
767         if file is not None:
768             return str(self.file.data)
769
770     security.declareProtected(View, 'HEAD')
771     def HEAD(self, REQUEST, RESPONSE):
772         """Retrieve the HEAD information for HTTP."""
773         if self.state != 2:
774             return None
775         file = self.file
776         if file is not None:
777             return file.HEAD(REQUEST, RESPONSE)
778         else:
779             RESPONSE.setHeader('Content-Type', 'text/plain')
780             RESPONSE.setHeader('Content-Length', '0')
781             return ''
782
783     security.declareProtected(ModifyPortalContent, 'PUT')
784     def PUT(self, REQUEST, RESPONSE):
785         """Handle HTTP (and presumably FTP?) PUT requests (WebDAV)."""
786         self.logger.debug("PUT()")
787         if self.state != 2:
788             self.logger.debug("BadRequest: Cannot PUT with state != 2")
789             raise 'BadRequest', "Cannot PUT with state != 2"
790         document = self.proxy.getEditableContent()
791         file = getattr(document, self.attrname)
792         response = file.PUT(REQUEST, RESPONSE)
793         # If the considered document is a CPSDocument we must use the edit()
794         # method since this method does important things such as setting dirty
795         # flags on modified fields.
796         # XXX: Note that using edit() modifies the file attribute twice.
797         # We shouldn't use the file.PUT() method but it is helpful to get the
798         # needed response object.
799         if getattr(aq_base(document), '_has_generic_edit_method', 0):
800             document.edit({self.attrname: file})
801         return response
802
803     security.declareProtected(ModifyPortalContent, 'LOCK')
804     def LOCK(self, REQUEST, RESPONSE):
805         """Handle HTTP (and presumably FTP?) LOCK requests (WebDAV)."""
806         self.logger.debug("LOCK()")
807         if self.state != 2:
808             self.logger.debug("BadRequest: Cannot LOCK with state != 2")
809             raise 'BadRequest', "Cannot LOCK with state != 2"
810         document = self.proxy.getEditableContent()
811         file = getattr(document, self.attrname)
812         return file.LOCK(REQUEST, RESPONSE)
813
814     security.declareProtected(ModifyPortalContent, 'UNLOCK')
815     def UNLOCK(self, REQUEST, RESPONSE):
816         """Handle HTTP (and presumably FTP?) UNLOCK requests (WebDAV)."""
817         self.logger("UNLOCK()")
818         if self.state != 2:
819             self.logger.debug("BadRequest: Cannot UNLOCK with state != 2")
820             raise 'BadRequest', "Cannot UNLOCK with state != 2"
821         document = self.proxy.getEditableContent()
822         file = getattr(document, self.attrname)
823         return file.UNLOCK(REQUEST, RESPONSE)
824
825     def wl_lockValues(self, killinvalids=0):
826         """Handle HTTP (and presumably FTP?) wl_lockValues requests (WebDAV)."""
827         self.logger("wl_lockValues()")
828         if self.state != 2:
829             self.logger.debug("BadRequest: Cannot wl_lockValues with state != 2")
830             raise 'BadRequest', "Cannot wl_lockValues with state != 2"
831         document = self.proxy.getEditableContent()
832         file = getattr(document, self.attrname)
833         return file.wl_lockValues(killinvalids)
834
835     def wl_isLocked(self):
836         """Handle HTTP (and presumably FTP?) wl_isLocked requests (WebDAV)."""
837         self.logger.debug("wl_isLocked()")
838         if self.state != 2:
839             self.logger.debug("BadRequest: Cannot wl_isLocked with state != 2")
840             raise 'BadRequest', "Cannot wl_isLocked with state != 2"
841         document = self.proxy.getEditableContent()
842         file = getattr(document, self.attrname)
843         return file.wl_isLocked()
844
845 InitializeClass(FileDownloader)
846
847
848 class LanguageSwitcher(Acquisition.Explicit):
849     """Language Switcher.
850
851     Use a session flag to keep a proxy into a selected locale."""
852
853     security = ClassSecurityInfo()
854     security.declareObjectPublic()
855
856     # Never viewable, so skipped by breadcrumbs.
857     _View_Permission = ()
858
859     def __init__(self, proxy):
860         self.proxy = proxy
861         self.id = KEYWORD_SWITCH_LANGUAGE
862
863     def __repr__(self):
864         return '<LanguageSwitcher for %s>' % repr(self.proxy)
865
866     def __bobo_traverse__(self, REQUEST, lang):
867         proxy = self.proxy
868         utool = getToolByName(self, 'portal_url')
869         rpath = utool.getRelativeUrl(proxy)
870         # store information by the time of the request to change the
871         # language used for viewing the current document,
872         # bypassing translation_service
873         if REQUEST:
874             if not REQUEST.has_key('SESSION'):
875                 # unrestricted traverse pass a fake REQUEST without SESSION
876                 REQUEST = getattr(self.proxy, 'REQUEST')
877             if REQUEST.has_key('SESSION'):
878                 langswitch = REQUEST.SESSION.get(SESSION_LANGUAGE_KEY, {})
879                 langswitch[rpath] = lang
880                 REQUEST.SESSION[SESSION_LANGUAGE_KEY] = langswitch
881         #  Return the proxy in the context of the container
882         container = aq_parent(aq_inner(proxy))
883         return proxy.__of__(container)
884
885 InitializeClass(LanguageSwitcher)
886
887
888 class LanguageViewer(Acquisition.Explicit):
889     """Language Viewer.
890
891     Use a REQUEST variable to keep a temporary selected locale."""
892
893     security = ClassSecurityInfo()
894     security.declareObjectPublic()
895
896     # Never viewable, so skipped by breadcrumbs.
897     _View_Permission = ()
898
899     def __init__(self, proxy):
900         self.proxy = proxy
901         self.id = KEYWORD_SWITCH_LANGUAGE
902
903     def __repr__(self):
904         return '<LanguageViewer for %s>' % repr(self.proxy)
905
906     def __bobo_traverse__(self, REQUEST, lang):
907         proxy = self.proxy
908         utool = getToolByName(self, 'portal_url')
909         rpath = utool.getRelativeUrl(proxy)
910         # store information by the time of the request to change the
911         # language used for viewing the current document, bypassing
912         # translation_service
913         if REQUEST is not None:
914             langswitch = REQUEST.get(REQUEST_LANGUAGE_KEY, {})
915             langswitch[rpath] = lang
916             if isinstance(REQUEST, DictType):
917                 # unrestrictedtraverse use a fake REQUEST
918                 REQUEST = getattr(self.proxy, 'REQUEST')
919             REQUEST.set(REQUEST_LANGUAGE_KEY, langswitch)
920         # Return the proxy in the context of the container
921         container = aq_parent(aq_inner(proxy))
922         return proxy.__of__(container)
923
924     # Needed by brain.getObject in Zope >= 2.7.6
925     getPhysicalRoot = Acquisition.Acquired
926     unrestrictedTraverse = Traversable.unrestrictedTraverse.im_func
927     restrictedTraverse = Traversable.restrictedTraverse.im_func
928
929 InitializeClass(LanguageViewer)
930
931
932
933 class RevisionSwitcher(Acquisition.Explicit):
934     """Intermediate object allowing for revision choice.
935
936     Returned by a proxy during traversal of .../archivedRevision/.
937
938     Parses URLs of the form .../archivedRevision/n/...
939     """
940
941     security = ClassSecurityInfo()
942     security.declareObjectPublic()
943
944     # Never viewable, so skipped by breadcrumbs.
945     _View_Permission = ()
946
947     logger = getLogger("CPSCore.ProxyBase.RevisionSwitcher")
948
949     def __init__(self, proxy):
950         self.proxy = proxy
951         self.id = KEYWORD_ARCHIVED_REVISION
952
953     def __repr__(self):
954         return '<RevisionSwitcher for %s>' % `self.proxy`
955
956     def __bobo_traverse__(self, request, rev):
957         try:
958             rev = int(rev)
959         except ValueError:
960             self.logger.debug("Invalid revision %r", rev)
961             raise KeyError(rev)
962
963         proxy = self.proxy
964         docid = proxy._docid
965         pxtool = getToolByName(proxy, 'portal_proxies')
966         ob = pxtool.getContentByRevision(docid, rev)
967         if ob is None:
968             self.logger.debug("Unknown revision %s", rev)
969             raise KeyError(rev)
970
971         # Find language
972         base_ob = aq_base(ob)
973         if hasattr(base_ob, 'Language'):
974             lang = ob.Language()
975         elif hasattr(base_ob, 'language'):
976             lang = ob.language
977         else:
978             lang = proxy._default_language
979
980         revproxy = VirtualProxy(ob, docid, rev, lang)
981         revproxy._setId(KEYWORD_ARCHIVED_REVISION+'/'+str(rev))
982
983         return revproxy.__of__(proxy)
984
985 InitializeClass(RevisionSwitcher)
986
987
988 class VirtualProxy(ProxyBase, CPSBaseDocument):
989     """Virtual proxy used for revision access.
990
991     Provides access to a fixed revision.
992     """
993
994     security = ClassSecurityInfo()
995     security.declareObjectProtected(ViewArchivedRevisions)
996
997     # Never modifiable.
998     _Modify_portal_content_Permission = ()
999
1000     def __init__(self, ob, docid, rev, lang):
1001         self._ob = ob
1002
1003         self._docid = docid
1004         self._default_language = lang
1005         self._language_revs = {lang: rev}
1006         self._from_language_revs = {}
1007         self._tag = None
1008
1009         self.portal_type = ob.portal_type
1010         # No workflow state.
1011
1012     security.declarePrivate('_getContent')
1013     def _getContent(self, lang=None, rev=None, editable=0):
1014         """Get the content object, maybe editable."""
1015         if editable:
1016             raise ValueError("Cannot get editable version of a virtual proxy")
1017         if lang is not None:
1018             raise ValueError("Cannot specify lang for a virtual proxy")
1019         if rev is not None:
1020             raise ValueError("Cannot specify rev for a virtual proxy")
1021         # Wrap in the virtual proxy, which is wrapped in the real proxy,
1022         # to get proper security context.
1023         return aq_base(self._ob).__of__(self)
1024
1025     security.declareProtected(View, 'getLanguage')
1026     def getLanguage(self, lang=None):
1027         """Get the language for the virtual proxy."""
1028         return self._default_language
1029
1030     security.declareProtected(View, 'getRevision')
1031     def getRevision(self, lang=None):
1032         """Get the revision for the virtual proxy."""
1033         return self._language_revs.values()[0]
1034
1035     security.declarePublic('isProxyArchived')
1036     def isProxyArchived(self):
1037         """Is the proxy archived. True."""
1038         return 1
1039
1040 InitializeClass(VirtualProxy)
1041
1042 # initialize zip_cache to store unzipped documents
1043 zip_cache = getCache(CACHE_ZIP_VIEW_KEY)
1044 zip_cache.setTimeout(CACHE_ZIP_VIEW_TIMEOUT)
1045
1046 class ViewZip(Acquisition.Explicit):
1047     """Intermediate object allowing to view zipped content
1048
1049     Returned by a proxy during traversal of .../viewZip/.
1050
1051     Parses URLs of the form .../viewZip/attrname/mydocname.zip/path/in/archive
1052     the content is cached using a TimeoutCache.
1053     """
1054     security = ClassSecurityInfo()
1055     security.declareObjectPublic()
1056
1057     logger = getLogger('CPSCore.ProxyBase.ViewZip')
1058
1059     def __init__(self, ob, proxy):
1060         """
1061         Init the ViewZip with the document and proxy to which it pertains.
1062
1063         ob is the document that owns the file and proxy is the proxy of this
1064         same document
1065         """
1066         self.ob = ob
1067         self.proxy = proxy
1068         self.state = 0
1069         self.attrname = None
1070         self.file = None
1071         self.filepath = []
1072
1073     def __repr__(self):
1074         s = '<ViewZip for %s' % `self.ob`
1075         if self.state > 0:
1076             s += '/'+self.attrname
1077         if self.state > 1:
1078             s += '/'+ '/'.join(self.filepath)
1079         s += '>'
1080         return s
1081
1082     def __bobo_traverse__(self, request, name):
1083         state = self.state
1084         ob = self.ob
1085         if state == 0:
1086             # First call, swallow attribute which should be a zipfile
1087             if not hasattr(aq_base(ob), name):
1088                 self.logger.debug("Not a base attribute: '%s'", name)
1089                 raise KeyError(name)
1090             file = getattr(ob, name)
1091             if file is not None and not isinstance(file, File):
1092                 self.logger.debug("Attribute '%s' is not a File but %r",
1093                                   name, file)
1094                 raise KeyError(name)
1095             self.attrname = name
1096             self.file = file
1097             self.state = 1
1098             return self
1099         else:
1100             if name in ('index_html', 'absolute_url', 'content_type'):
1101                 return getattr(self, name)
1102             # extract file path in the zip
1103             self.filepath.append(name)
1104             self.state += 1
1105             return self
1106
1107     security.declareProtected(View, 'absolute_url')
1108     def absolute_url(self):
1109         url = self.proxy.absolute_url() + '/' + KEYWORD_VIEW_ZIP
1110         if self.state > 0:
1111             url += '/' + self.attrname
1112         if self.state > 1:
1113             url += '/' + '/'.join(self.filepath)
1114         return url
1115
1116     security.declareProtected(View, 'content_type')
1117     def content_type(self):
1118         if self.state != 2:
1119             return None
1120         return self.file.content_type
1121
1122     security.declareProtected(View, 'index_html')
1123     def index_html(self, REQUEST, RESPONSE):
1124         """Publish the file or image."""
1125         filename = self.filepath[0]
1126         filepath = '/'.join(self.filepath[1:])
1127         key = self.absolute_url()
1128         last_modified = int(self.proxy.getContent().modified())
1129         content = zip_cache.get(key, min_date=last_modified)
1130         if content is None:
1131             self.logger.debug('extract %s from %s', filepath, filename)
1132             # XXX this is very ineficiant as str(file.data) load all the
1133             # content in memory, ofs file should implement a file-like object
1134             # with all stdio methods seek, tell, read, close...
1135             # another way will be to use a DiskFile
1136             zipfile = ZipFile(StringIO(str(self.file.data)), 'r')
1137             try:
1138                 content = zipfile.read(filepath)
1139             except KeyError:
1140                 self.logger.debug('not found %s: %s', filename, filepath)
1141                 content = 0
1142             # cache for next access
1143             zip_cache[key] = content
1144         # set mime type
1145         registry = getToolByName(self, 'mimetypes_registry', None)
1146         if registry is not None:
1147             mimetype = registry.lookupExtension(
1148                 os.path.basename(filepath.lower()))
1149             if mimetype:
1150                 RESPONSE.setHeader('Content-Type', mimetype.normalized())
1151         # render content keeping original html base
1152         RESPONSE.setBase(None)
1153         return content
1154
1155 InitializeClass(ViewZip)
1156
1157 #
1158 # Serialization
1159 #
1160
1161 def unserializeProxy(ser, ob=None):
1162     """Unserialize a proxy from a string.
1163
1164     If ob is not None, modify that object in place.
1165     Returns the object.
1166     Does not send any notification.
1167     """
1168     l = unpack('>I', ser[:4])[0]
1169     class_name = ser[4:4+l]
1170     ser = ser[4+l:]
1171     f = StringIO(ser)
1172     p = Unpickler(f)
1173     stuff = p.load()
1174     if ob is None:
1175         if class_name == 'ProxyFolder':
1176             ob = ProxyFolder('')
1177         elif class_name == 'ProxyDocument':
1178             ob = ProxyDocument('')
1179         elif class_name == 'ProxyFolderishDocument':
1180             ob = ProxyFolderishDocument('')
1181         else:
1182             logger.error('unserialize got class_name=%s', class_name)
1183             return None
1184     ob.__dict__.update(stuff)
1185     return ob
1186
1187 #
1188 # Make Item have standard proxy methods, so that calling
1189 # for instance ob.getContent() on a non-proxy object will work.
1190 #
1191
1192 class NotAProxy:
1193     security = ClassSecurityInfo()
1194
1195     security.declareProtected(View, 'getDocid')
1196     def getDocid(self):
1197         """No Docid."""
1198         return ''
1199
1200     security.declareProtected(View, 'getDefaultLanguage')
1201     def getDefaultLanguage(self):
1202         """Get the default language."""
1203         return self.getLanguage()
1204
1205     security.declareProtected(View, 'getLanguageRevisions')
1206     def getLanguageRevisions(self):
1207         """No mapping."""
1208         return {self.getLanguage():self.getRevision()}
1209
1210     security.declareProtected(View, 'getFromLanguageRevisions')
1211     def getFromLanguageRevisions(self):
1212         """No mapping."""
1213         return {}
1214
1215     security.declareProtected(View, 'getLanguage')
1216     def getLanguage(self, lang=None):
1217         """Get the selected language."""
1218         lang = 'en'
1219         translation_service = getToolByName(self, 'translation_service', None)
1220         if translation_service is not None:
1221             lang = translation_service.getDefaultLanguage()
1222         return lang
1223
1224     security.declareProtected(View, 'getProxyLanguages')
1225     def getProxyLanguages(self):
1226         """return all available languages."""
1227         return [self.getLanguage()]
1228
1229     security.declareProtected(View, 'getRevision')
1230     def getRevision(self, lang=None):
1231         """Get the best revision."""
1232         return 0
1233
1234     security.declareProtected(View, 'getContent')
1235     def getContent(self, lang=None):
1236         """Return the object itself."""
1237         return self
1238
1239     security.declareProtected(ModifyPortalContent, 'getEditableContent')
1240     def getEditableContent(self, lang=None):
1241         """Return the object itself."""
1242         return self
1243
1244     security.declarePublic('isProxyArchived')
1245     def isProxyArchived(self):
1246         """Is the proxy archived."""
1247         return 0
1248
1249     def isCPSFolderish(self):
1250         """Return True if the document is a structural folderish."""
1251         return self.isPrincipiaFolderish
1252
1253
1254 InitializeClass(NotAProxy)
1255
1256 # Add all methods to Item class.
1257 for attr, val in NotAProxy.__dict__.items():
1258     if not attr.startswith('_'):
1259         setattr(Item, attr, val)
1260
1261 #
1262 # Concrete types for proxies.
1263 #
1264
1265 factory_type_information = (
1266     {'id': 'CPS Proxy Folder',
1267      'description': 'A proxy to a folder.',
1268      'title': '',
1269      'content_icon': 'folder_icon.png',
1270      'product': 'CPSCore',
1271      'meta_type': 'CPS Proxy Folder',
1272      'factory': 'addProxyFolder',
1273      'immediate_view': '',
1274      'filter_content_types': 1,
1275      'allowed_content_types': (),
1276      'actions': (),
1277      },
1278     {'id': 'CPS Proxy Document',
1279      'description': 'A proxy to a document.',
1280      'title': '',
1281      'content_icon': 'document_icon.png',
1282      'product': 'CPSCore',
1283      'meta_type': 'CPS Proxy Document',
1284      'factory': 'addProxyDocument',
1285      'immediate_view': '',
1286      'filter_content_types': 1,
1287      'allowed_content_types': (),
1288      'actions': (),
1289      },
1290     {'id': 'CPS Proxy Folderish Document',
1291      'description': 'A proxy to a folderish document.',
1292      'title': '',
1293      'content_icon': 'folder_icon.png',
1294      'product': 'CPSCore',
1295      'meta_type': 'CPS Proxy Folderish Document',
1296      'factory': 'addProxyFolderishDocument',
1297      'immediate_view': '',
1298      'filter_content_types': 1,
1299      'allowed_content_types': (),
1300      'actions': (),
1301      },
1302     {'id': 'CPS Proxy BTree Folder',
1303      'description': 'A proxy to a folder.',
1304      'title': '',
1305      'content_icon': 'folder_icon.png',
1306      'product': 'CPSCore',
1307      'meta_type': 'CPS Proxy BTree Folder',
1308      'factory': 'addProxyBTreeFolder',
1309      'immediate_view': '',
1310      'filter_content_types': 1,
1311      'allowed_content_types': (),
1312      'actions': (),
1313      },
1314     {'id': 'CPS Proxy BTree Folderish Document',
1315      'description': 'A proxy to a folderish document.',
1316      'title': '',
1317      'content_icon': 'folder_icon.png',
1318      'product': 'CPSCore',
1319      'meta_type': 'CPS Proxy BTree Folderish Document',
1320      'factory': 'addProxyBTreeFolderishDocument',
1321      'immediate_view': '',
1322      'filter_content_types': 1,
1323      'allowed_content_types': (),
1324      'actions': (),
1325      },
1326     )
1327
1328 class ProxyFolder(ProxyBase, CPSBaseFolder):
1329     """A proxy folder is a folder whose data is indirected to a document
1330     in a repository."""
1331
1332     meta_type = 'CPS Proxy Folder'
1333
1334     security = ClassSecurityInfo()
1335
1336     def __init__(self, id, **kw):
1337         CPSBaseFolder.__init__(self, id)
1338         ProxyBase.__init__(self, **kw)
1339
1340     def getCPSCustomCSS(self):
1341         """
1342         Return the cps custom CSS from this folder
1343         or from one of its parents if they have one.
1344         """
1345
1346         portal = self.portal_url.getPortalObject()
1347         current = self.getContent()
1348         current_proxy = self
1349
1350         while current.id != portal.id and \
1351                   getattr(current, 'cps_custom_css', "") == "":
1352             current_proxy = current_proxy.aq_inner.aq_parent
1353             current = current_proxy.getContent()
1354
1355         if current.id == portal.id:
1356             return ""
1357         else:
1358             return current.cps_custom_css
1359
1360     security.declareProtected(View, 'thisProxyFolder')
1361     def thisProxyFolder(self):
1362         """Get the closest proxy folder from a context.
1363        
1364         Used by acquisition.
1365         """
1366         return self
1367        
1368     manage_options = (CPSBaseFolder.manage_options[:1] +
1369                       ProxyBase.proxybase_manage_options +
1370                       CPSBaseFolder.manage_options[1:]
1371                       )
1372
1373     # Changing security declarations of inherited methods from OFS.OrderSupport
1374     # to gain finer granularity
1375     security.declareProtected(ChangeSubobjectsOrder,
1376             'moveObjectsByDelta',
1377             'moveObjectsUp',
1378             'moveObjectsDown',
1379             'moveObjectsToTop',
1380             'moveObjectsToBottom',
1381             'orderObjects',
1382             'moveObjectToPosition',
1383             )
1384
1385     # Trying to remain consistent with Zope default behaviour
1386     security.setPermissionDefault(ChangeSubobjectsOrder, ('Manager', 'Owner'))
1387
1388
1389 InitializeClass(ProxyFolder)
1390
1391
1392 class ProxyDocument(ProxyBase, CPSBaseDocument):
1393     """A proxy document is a document whose data is indirected to a document
1394     in a repository."""
1395
1396     meta_type = 'CPS Proxy Document'
1397     # portal_type will be set to the target's portal_type after creation
1398
1399     def __init__(self, id, **kw):
1400         CPSBaseDocument.__init__(self, id)
1401         ProxyBase.__init__(self, **kw)
1402
1403     manage_options = (ProxyBase.proxybase_manage_options +
1404                       CPSBaseDocument.manage_options
1405                       )
1406
1407 InitializeClass(ProxyDocument)
1408
1409
1410 class ProxyFolderishDocument(ProxyFolder):
1411     """A proxy folderish document is a folderish document,
1412     whose data is indirected to a document in a repository."""
1413
1414     meta_type = 'CPS Proxy Folderish Document'
1415     # portal_type will be set to the target's portal_type after creation
1416
1417     _isFolderishDocument = 1
1418
1419     security = ClassSecurityInfo()
1420
1421     #
1422     # Utility methods
1423     #
1424
1425     security.declareProtected(View, 'thisProxyFolderishDocument')
1426     def thisProxyFolderishDocument(self):
1427         """Return this proxy folderish document.
1428
1429         Used by acquisition.
1430         """
1431         return self
1432
1433     security.declareProtected(View, 'topProxyFolderishDocument')
1434     def topProxyFolderishDocument(self):
1435         """Return the top enclosing proxy folderish document.
1436
1437         Used by acquisition.
1438         """
1439         container = aq_parent(aq_inner(self))
1440         try:
1441             return container.topProxyFolderishDocument()
1442         except AttributeError:
1443             return self
1444
1445     security.declareProtected(View, 'thisProxyFolder')
1446     def thisProxyFolder(self):
1447         """Get the closest proxy folder from a context.
1448        
1449         Used by acquisition.
1450         """
1451         return self.aq_parent.thisProxyFolder()
1452
1453     #
1454     # Freezing
1455     #
1456
1457     security.declarePrivate('freezeProxy')
1458     def freezeProxy(self):
1459         """Freeze the proxy and all subproxies.
1460
1461         (Called by CPSWorkflow.)
1462         """
1463         # XXX use an event?
1464         pxtool = getToolByName(self, 'portal_proxies')
1465         self._freezeProxyRecursive(self, pxtool)
1466
1467     security.declarePrivate('_freezeProxyRecursive')
1468     def _freezeProxyRecursive(self, ob, pxtool):
1469         """Freeze this proxy and recurse."""
1470         if not isinstance(ob, ProxyBase):
1471             return
1472         self._freezeProxy(ob, pxtool)
1473         for subob in ob.objectValues():
1474             self._freezeProxyRecursive(subob, pxtool)
1475
1476 InitializeClass(ProxyFolderishDocument)
1477
1478
1479 class ProxyBTreeFolder(ProxyBase, CPSBaseBTreeFolder):
1480     """A proxy btree folder is a folder whose data is indirected to a document
1481     in a repository."""
1482
1483     meta_type = 'CPS Proxy BTree Folder'
1484
1485     security = ClassSecurityInfo()
1486
1487     def __init__(self, id, **kw):
1488         CPSBaseBTreeFolder.__init__(self, id)
1489         ProxyBase.__init__(self, **kw)
1490
1491     def __nonzero__(self):
1492         """Return True because proxy exists
1493
1494         BTree behavior checks __len__ method, that returns True only if BTree
1495         is not empty ; here we only want to check that the proxy btree exists.
1496         """
1497         return True
1498
1499     def getCPSCustomCSS(self):
1500         """
1501         Return the cps custom CSS from this folder
1502         or from one of its parents if they have one.
1503         """
1504
1505         portal = self.portal_url.getPortalObject()
1506         current = self.getContent()
1507         current_proxy = self
1508
1509         while current.id != portal.id and \
1510                   getattr(current, 'cps_custom_css', "") == "":
1511             current_proxy = current_proxy.aq_inner.aq_parent
1512             current = current_proxy.getContent()
1513
1514         if current.id == portal.id:
1515             return ""
1516         else:
1517             return current.cps_custom_css
1518
1519     security.declareProtected(View, 'thisProxyFolder')
1520     def thisProxyFolder(self):
1521         """Get the closest proxy folder from a context.
1522        
1523         Used by acquisition.
1524         """
1525         return self
1526
1527     manage_options = (CPSBaseFolder.manage_options[:1] +
1528                       ProxyBase.proxybase_manage_options +
1529                       CPSBaseFolder.manage_options[1:]
1530                       )
1531
1532 InitializeClass(ProxyBTreeFolder)
1533
1534
1535 class ProxyBTreeFolderishDocument(ProxyBTreeFolder):
1536     """A proxy btree folderish document is a folderish document,
1537     whose data is indirected to a document in a repository."""
1538
1539     meta_type = 'CPS Proxy BTree Folderish Document'
1540     # portal_type will be set to the target's portal_type after creation
1541
1542     _isFolderishDocument = 1
1543
1544     security = ClassSecurityInfo()
1545
1546     #
1547     # Utility methods
1548     #
1549
1550     security.declareProtected(View, 'thisProxyFolderishDocument')
1551     def thisProxyFolderishDocument(self):
1552         """Return this proxy folderish document.
1553
1554         Used by acquisition.
1555         """
1556         return self
1557
1558     security.declareProtected(View, 'topProxyFolderishDocument')
1559     def topProxyFolderishDocument(self):
1560         """Return the top enclosing proxy folderish document.
1561
1562         Used by acquisition.
1563         """
1564         container = aq_parent(aq_inner(self))
1565         try:
1566             return container.topProxyFolderishDocument()
1567         except AttributeError:
1568             return self
1569
1570     security.declareProtected(View, 'thisProxyFolder')
1571     def thisProxyFolder(self):
1572         """Get the closest proxy folder from a context.
1573        
1574         Used by acquisition.
1575         """
1576         return self.aq_parent.thisProxyFolder()
1577
1578     #
1579     # Freezing
1580     #
1581
1582     security.declarePrivate('freezeProxy')
1583     def freezeProxy(self):
1584         """Freeze the proxy and all subproxies.
1585
1586         (Called by CPSWorkflow.)
1587         """
1588         # XXX use an event?
1589         pxtool = getToolByName(self, 'portal_proxies')
1590         self._freezeProxyRecursive(self, pxtool)
1591
1592     security.declarePrivate('_freezeProxyRecursive')
1593     def _freezeProxyRecursive(self, ob, pxtool):
1594         """Freeze this proxy and recurse."""
1595         if not isinstance(ob, ProxyBase):
1596             return
1597         self._freezeProxy(ob, pxtool)
1598         for subob in ob.objectValues():
1599             self._freezeProxyRecursive(subob, pxtool)
1600
1601 InitializeClass(ProxyBTreeFolderishDocument)
1602
1603
1604 def addProxyFolder(container, id, REQUEST=None, **kw):
1605     """Add a proxy folder."""
1606     # container is a dispatcher when called from ZMI
1607     ob = ProxyFolder(id, **kw)
1608     id = ob.getId()
1609     container._setObject(id, ob)
1610     if REQUEST is not None:
1611         REQUEST.RESPONSE.redirect(container.absolute_url() + '/manage_main')
1612
1613 def addProxyDocument(container, id, REQUEST=None, **kw):
1614     """Add a proxy document."""
1615     # container is a dispatcher when called from ZMI
1616     ob = ProxyDocument(id, **kw)
1617     id = ob.getId()
1618     container._setObject(id, ob)
1619     if REQUEST is not None:
1620         REQUEST.RESPONSE.redirect(container.absolute_url() + '/manage_main')
1621
1622 def addProxyFolderishDocument(container, id, REQUEST=None, **kw):
1623     """Add a proxy folderish document."""
1624     # container is a dispatcher when called from ZMI
1625     ob = ProxyFolderishDocument(id, **kw)
1626     id = ob.getId()
1627     container._setObject(id, ob)
1628     if REQUEST is not None:
1629         REQUEST.RESPONSE.redirect(container.absolute_url() + '/manage_main')
1630
1631 def addProxyBTreeFolder(container, id, REQUEST=None, **kw):
1632     """Add a proxy btree folder."""
1633     # container is a dispatcher when called from ZMI
1634     ob = ProxyBTreeFolder(id, **kw)
1635     id = ob.getId()
1636     container._setObject(id, ob)
1637     if REQUEST is not None:
1638         REQUEST.RESPONSE.redirect(container.absolute_url() + '/manage_main')
1639
1640 def addProxyBTreeFolderishDocument(container, id, REQUEST=None, **kw):
1641     """Add a proxy btree folderish document."""
1642     # container is a dispatcher when called from ZMI
1643     ob = ProxyBTreeFolderishDocument(id, **kw)
1644     id = ob.getId()
1645     container._setObject(id, ob)
1646     if REQUEST is not None:
1647         REQUEST.RESPONSE.redirect(container.absolute_url() + '/manage_main')
Note: See TracBrowser for help on using the browser.