Back to index

plone3  3.1.7
base.py
Go to the documentation of this file.
00001 #  ATContentTypes http://plone.org/products/atcontenttypes/
00002 #  Archetypes reimplementation of the CMF core types
00003 #  Copyright (c) 2003-2006 AT Content Types development team
00004 #
00005 #  This program is free software; you can redistribute it and/or modify
00006 #  it under the terms of the GNU General Public License as published by
00007 #  the Free Software Foundation; either version 2 of the License, or
00008 #  (at your option) any later version.
00009 #
00010 #  This program is distributed in the hope that it will be useful,
00011 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
00012 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00013 #  GNU General Public License for more details.
00014 #
00015 #  You should have received a copy of the GNU General Public License
00016 #  along with this program; if not, write to the Free Software
00017 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
00018 #
00019 """
00020 
00021 """
00022 __author__  = 'Christian Heimes <tiran@cheimes.de>'
00023 __docformat__ = 'restructuredtext'
00024 
00025 
00026 import os
00027 import posixpath
00028 import logging
00029 import transaction
00030 
00031 from Products.ATContentTypes.config import HAS_LINGUA_PLONE
00032 if HAS_LINGUA_PLONE:
00033     from Products.LinguaPlone.public import BaseContent
00034     from Products.LinguaPlone.public import BaseFolder
00035     from Products.LinguaPlone.public import OrderedBaseFolder
00036     from Products.LinguaPlone.public import BaseBTreeFolder
00037     from Products.LinguaPlone.public import registerType
00038 else:
00039     from Products.Archetypes.atapi import BaseContent
00040     from Products.Archetypes.atapi import BaseFolder
00041     from Products.Archetypes.atapi import OrderedBaseFolder
00042     from Products.Archetypes.atapi import BaseBTreeFolder
00043     from Products.Archetypes.atapi import registerType
00044 
00045 from AccessControl import ClassSecurityInfo, Permissions
00046 from ComputedAttribute import ComputedAttribute
00047 from Globals import InitializeClass
00048 from Acquisition import aq_base
00049 from Acquisition import aq_inner
00050 from Acquisition import aq_parent
00051 from Globals import REPLACEABLE
00052 from webdav.Lockable import ResourceLockedError
00053 from webdav.NullResource import NullResource
00054 from zExceptions import MethodNotAllowed
00055 from zExceptions import NotFound
00056 from ZODB.POSException import ConflictError
00057 
00058 from Products.CMFCore.permissions import View
00059 from Products.CMFCore.permissions import ModifyPortalContent
00060 from Products.CMFCore.permissions import ManageProperties
00061 from Products.CMFCore.utils import getToolByName
00062 from Products.CMFDynamicViewFTI.browserdefault import BrowserDefaultMixin
00063 from Products.CMFPlone.PloneFolder import ReplaceableWrapper
00064 
00065 from Products.ATContentTypes import permission as ATCTPermissions
00066 from Products.ATContentTypes.config import MIME_ALIAS
00067 from Products.ATContentTypes.lib.constraintypes import ConstrainTypesMixin
00068 from Products.ATContentTypes.interfaces import IATContentType
00069 from Products.ATContentTypes.content.schemata import ATContentTypeSchema
00070 
00071 from plone.i18n.normalizer.interfaces import IUserPreferredFileNameNormalizer
00072 
00073 DEBUG = True
00074 LOG = logging.getLogger('ATCT')
00075 
00076 def registerATCT(class_, project):
00077     """Registers an ATContentTypes based type
00078 
00079     One reason to use it is to hide the lingua plone related magic.
00080     """
00081     assert IATContentType.isImplementedByInstancesOf(class_)
00082     registerType(class_, project)
00083 
00084 def cleanupFilename(filename, request=None):
00085     """Removes bad chars from file names to make them a good id
00086     """
00087     if not filename:
00088         return
00089     if request is not None:
00090         return IUserPreferredFileNameNormalizer(request).normalize(filename)
00091     return None
00092 
00093 def translateMimetypeAlias(alias):
00094     """Maps old CMF content types to real mime types
00095     """
00096     if alias.find('/') != -1:
00097         mime = alias
00098     else:
00099         mime = MIME_ALIAS.get(alias, None)
00100     assert(mime) # shouldn't be empty
00101     return mime
00102 
00103 
00104 class ATCTMixin(BrowserDefaultMixin):
00105     """Mixin class for AT Content Types"""
00106 
00107     schema         =  ATContentTypeSchema
00108 
00109     archetype_name = 'AT Content Type'
00110     _atct_newTypeFor = {'portal_type' : None, 'meta_type' : None}
00111     assocMimetypes = ()
00112     assocFileExt   = ()
00113     cmf_edit_kws   = ()
00114 
00115     # flag to show that the object is a temporary object
00116     isDocTemp = False
00117     _at_rename_after_creation = True # rename object according to the title?
00118 
00119     __implements__ = (IATContentType, BrowserDefaultMixin.__implements__)
00120 
00121     security       = ClassSecurityInfo()
00122 
00123     security.declareProtected(ModifyPortalContent,
00124                               'initializeArchetype')
00125     def initializeArchetype(self, **kwargs):
00126         """called by the generated add* factory in types tool
00127 
00128         Overwritten to call edit() instead of update() to have the cmf
00129         compatibility method.
00130         """
00131         try:
00132             self.initializeLayers()
00133             self.markCreationFlag()
00134             self.setDefaults()
00135             if kwargs:
00136                 self.edit(**kwargs)
00137             self._signature = self.Schema().signature()
00138             if self.isPrincipiaFolderish:
00139                 self.copyLayoutFromParent()
00140         except ConflictError:
00141             raise
00142         except Exception, msg:
00143             LOG.warn('Exception in initializeArchetype', exc_info=True)
00144             if DEBUG and str(msg) not in ('SESSION',):
00145                 # debug code
00146                 raise
00147 
00148     security.declarePrivate('copyLayoutFromParent')
00149     def copyLayoutFromParent(self):
00150         """Copies the layout from the parent object if it's of the same type."""
00151         parent = aq_parent(aq_inner(self))
00152         if parent is not None:
00153             # Only set the layout if we are the same type as out parent object
00154             if parent.meta_type == self.meta_type:
00155                 # If the parent is the same type as us it should implement
00156                 # BrowserDefaultMixin
00157                 parent_layout = parent.getLayout()
00158                 # Just in case we should make sure that the layout is
00159                 # available to the new object
00160                 if parent_layout in [l[0] for l in self.getAvailableLayouts()]:
00161                     self.setLayout(parent_layout)
00162 
00163     security.declareProtected(ModifyPortalContent, 'edit')
00164     def edit(self, *args, **kwargs):
00165         """Reimplementing edit() to have a compatibility method for the old
00166         cmf edit() method
00167         """
00168         if len(args) != 0:
00169             # use cmf edit method
00170             return self.cmf_edit(*args, **kwargs)
00171 
00172         # if kwargs is containing a key that is also in the list of cmf edit
00173         # keywords then we have to use the cmf_edit comp. method
00174         cmf_edit_kws = getattr(aq_inner(self).aq_explicit, 'cmf_edit_kws', ())
00175         for kwname in kwargs.keys():
00176             if kwname in cmf_edit_kws:
00177                 return self.cmf_edit(**kwargs)
00178         # standard AT edit - redirect to update()
00179         return self.update(**kwargs)
00180 
00181     security.declarePrivate('cmf_edit')
00182     def cmf_edit(self, *args, **kwargs):
00183         """Overwrite this method to make AT compatible with the crappy
00184         CMF edit()
00185         """
00186         raise NotImplementedError("cmf_edit method isn't implemented")
00187 
00188     def exclude_from_nav(self):
00189         """Accessor for excludeFromNav field
00190         """
00191         field = self.getField('excludeFromNav')
00192         if field is not None:
00193             return field.get(self)
00194         else:
00195             return False
00196 
00197     security.declareProtected(View, 'get_size')
00198     def get_size(self):
00199         """ZMI / Plone get size method
00200         """
00201         f = self.getPrimaryField()
00202         if f is None:
00203             return 0
00204         return f.get_size(self) or 0
00205 
00206 InitializeClass(ATCTMixin)
00207 
00208 class ATCTContent(ATCTMixin, BaseContent):
00209     """Base class for non folderish AT Content Types"""
00210 
00211     __implements__ = (BaseContent.__implements__,
00212                       ATCTMixin.__implements__)
00213 
00214     security       = ClassSecurityInfo()
00215 
00216     security.declarePrivate('manage_afterPUT')
00217     def manage_afterPUT(self, data, marshall_data, file, context, mimetype,
00218                         filename, REQUEST, RESPONSE):
00219         """After webdav/ftp PUT method
00220 
00221         Set title according to the id on webdav/ftp PUTs.
00222         """
00223         id = self.getId()
00224         title = self.Title()
00225         if not title:
00226             # Use the last-segment from the url as the id, as the
00227             # object might have been renamed somehow (eg: by id
00228             # mangling).
00229             request = REQUEST or getattr(self, 'REQUEST', None)
00230             if request is not None:
00231                 path_info = request.get('PATH_INFO')
00232                 if path_info:
00233                     id = posixpath.basename(path_info)
00234             self.setTitle(id)
00235 
00236 InitializeClass(ATCTContent)
00237 
00238 class ATCTFileContent(ATCTContent):
00239     """Base class for content types containing a file like ATFile or ATImage
00240 
00241     The file field *must* be the exclusive primary field
00242     """
00243 
00244     # the precondition attribute is required to make ATFile and
00245     # ATImage compatible with OFS.Image.*. The precondition feature is
00246     # (not yet) supported.
00247     precondition = ''
00248 
00249     manage_options = (
00250         ATCTContent.manage_options[0:1] +
00251         ATCTContent.manage_options[2:]
00252         )
00253 
00254     security = ClassSecurityInfo()
00255 
00256     security.declareProtected(View, 'download')
00257     def download(self, REQUEST=None, RESPONSE=None):
00258         """Download the file (use default index_html)
00259         """
00260         if REQUEST is None:
00261             REQUEST = self.REQUEST
00262         if RESPONSE is None:
00263             RESPONSE = REQUEST.RESPONSE
00264         field = self.getPrimaryField()
00265         return field.download(self, REQUEST, RESPONSE)
00266 
00267     security.declareProtected(View, 'index_html')
00268     def index_html(self, REQUEST=None, RESPONSE=None):
00269         """Make it directly viewable when entering the objects URL
00270         """
00271         if REQUEST is None:
00272             REQUEST = self.REQUEST
00273         if RESPONSE is None:
00274             RESPONSE = REQUEST.RESPONSE
00275         field = self.getPrimaryField()
00276         data  = field.getAccessor(self)(REQUEST=REQUEST, RESPONSE=RESPONSE)
00277         if data:
00278             return data.index_html(REQUEST, RESPONSE)
00279         # XXX what should be returned if no data is present?
00280 
00281     security.declareProtected(View, 'get_data')
00282     def get_data(self):
00283         """CMF compatibility method
00284         """
00285         data = aq_base(self.getPrimaryField().getAccessor(self)())
00286         return str(getattr(data, 'data', data))
00287 
00288     data = ComputedAttribute(get_data, 1)
00289 
00290     security.declareProtected(View, 'size')
00291     def size(self):
00292         """Get size (image_view.pt)
00293         """
00294         return self.get_size()
00295 
00296     security.declareProtected(View, 'get_content_type')
00297     def get_content_type(self):
00298         """CMF compatibility method
00299         """
00300         f = self.getPrimaryField().getAccessor(self)()
00301         return f and f.getContentType() or 'text/plain' #'application/octet-stream'
00302 
00303     content_type = ComputedAttribute(get_content_type, 1)
00304 
00305     security.declarePrivate('update_data')
00306     def update_data(self, data, content_type=None, size='ignored'):
00307         kwargs = {}
00308         if content_type is not None:
00309             kwargs['mimetype'] = content_type
00310         mutator = self.getPrimaryField().getMutator(self)
00311         mutator(data, **kwargs)
00312 
00313     security.declareProtected(ModifyPortalContent, 'manage_edit')
00314     def manage_edit(self, title, content_type, precondition='',
00315                     filedata=None, REQUEST=None):
00316         """
00317         Changes the title and content type attributes of the File or Image.
00318         """
00319         if self.wl_isLocked():
00320             raise ResourceLockedError, "File is locked via WebDAV"
00321 
00322         self.setTitle(title)
00323         if filedata is not None:
00324             self.update_data(filedata, content_type, len(filedata))
00325         if REQUEST:
00326             message="Saved changes."
00327             return self.manage_main(self,REQUEST,manage_tabs_message=message)
00328 
00329     def _cleanupFilename(self, filename, request=None):
00330         """Cleans the filename from unwanted or evil chars
00331         """
00332         if filename and not isinstance(filename, unicode):
00333             encoding = self.getCharset()
00334             filename = unicode(filename, encoding)
00335 
00336         filename = cleanupFilename(filename, request=request)
00337         return filename and filename.encode(encoding) or None
00338 
00339     def _setATCTFileContent(self, value, **kwargs):
00340         """Set id to uploaded id
00341         """
00342         field = self.getPrimaryField()
00343         # set first then get the filename
00344         field.set(self, value, **kwargs) # set is ok
00345         if self._isIDAutoGenerated(self.getId()):
00346             filename = field.getFilename(self, fromBaseUnit=False)
00347             request = self.REQUEST
00348             clean_filename = self._cleanupFilename(filename, request=request)
00349             request_id = request.form.get('id')
00350             if request_id and not self._isIDAutoGenerated(request_id):
00351                 # request contains an id
00352                 # skip renaming when then request id is not autogenerated which
00353                 # means the user has defined an id. It's autogenerated when the
00354                 # the user has disabled "short name editing".
00355                 return
00356             elif clean_filename == self.getId():
00357                 # destination id and old id are equal
00358                 return
00359             elif clean_filename:
00360                 # got a clean file name - rename it
00361                 # apply subtransaction. w/o a subtransaction renaming
00362                 # fails when the type is created using portal_factory
00363                 transaction.savepoint(optimistic=True)
00364                 self.setId(clean_filename)
00365 
00366     security.declareProtected(View, 'post_validate')
00367     def post_validate(self, REQUEST=None, errors=None):
00368         """Validates upload file and id
00369         """
00370         id     = REQUEST.form.get('id')
00371         field  = self.getPrimaryField()
00372         f_name = field.getName()
00373         upload = REQUEST.form.get('%s_file' % f_name, None)
00374         filename = getattr(upload, 'filename', None)
00375         if isinstance(filename, basestring):
00376             filename = os.path.basename(filename)
00377             filename = filename.split("\\")[-1]
00378         clean_filename = self._cleanupFilename(filename, request=REQUEST)
00379         used_id = (id and not self._isIDAutoGenerated(id)) and id or clean_filename
00380 
00381         if upload:
00382             # the file may have already been read by a
00383             # former method
00384             upload.seek(0)
00385 
00386         if not used_id:
00387             return
00388 
00389         if getattr(self, 'check_id', None) is not None:
00390             check_id = self.check_id(used_id,required=1)
00391         else:
00392             # If check_id is not available just look for conflicting ids
00393             parent = aq_parent(aq_inner(self))
00394             check_id = used_id in parent.objectIds() and \
00395                        'Id %s conflicts with an existing item'% used_id or False
00396         if check_id and used_id == id:
00397             errors['id'] = check_id
00398             REQUEST.form['id'] = used_id
00399         elif check_id:
00400             errors[f_name] = check_id
00401 
00402     security.declarePrivate('manage_afterPUT')
00403     def manage_afterPUT(self, data, marshall_data, file, context, mimetype,
00404                         filename, REQUEST, RESPONSE):
00405         """After webdav/ftp PUT method
00406 
00407         Set the title according to the uploaded filename if the title
00408         is empty or set it to the id if no filename is given.
00409         """
00410         id = self.getId()
00411         title = self.Title()
00412         if not title:
00413             if filename:
00414                 self.setTitle(filename)
00415             else:
00416                 # Use the last-segment from the url as the id, as the
00417                 # object might have been renamed somehow (eg: by id
00418                 # mangling).
00419                 request = REQUEST or getattr(self, 'REQUEST', None)
00420                 if request is not None:
00421                     path_info = request.get('PATH_INFO')
00422                     if path_info:
00423                         id = posixpath.basename(path_info)
00424                 self.setTitle(id)
00425 
00426 InitializeClass(ATCTFileContent)
00427 
00428 
00429 class ATCTFolder(ATCTMixin, BaseFolder):
00430     """Base class for folderish AT Content Types (but not for folders)
00431 
00432     DO NOT USE this base class for folders but only for folderish objects like
00433     AT Topic. It doesn't support constrain types!
00434     """
00435 
00436     __implements__ = (ATCTMixin.__implements__,
00437                       BaseFolder.__implements__)
00438 
00439     security       = ClassSecurityInfo()
00440 
00441     security.declareProtected(View, 'get_size')
00442     def get_size(self):
00443         """Returns 1 as folders have no size."""
00444         return 1
00445 
00446 InitializeClass(ATCTFolder)
00447 
00448 
00449 class ATCTFolderMixin(ConstrainTypesMixin, ATCTMixin):
00450     """ Constrained folderish type """
00451 
00452     __implements__ = (ATCTMixin.__implements__,
00453                       ConstrainTypesMixin.__implements__,)
00454 
00455     security       = ClassSecurityInfo()
00456 
00457     def __browser_default__(self, request):
00458         """ Set default so we can return whatever we want instead
00459         of index_html """
00460         return getToolByName(self, 'plone_utils').browserDefault(self)
00461 
00462     security.declareProtected(View, 'get_size')
00463     def get_size(self):
00464         """Returns 1 as folders have no size."""
00465         return 1
00466 
00467     security.declarePrivate('manage_afterMKCOL')
00468     def manage_afterMKCOL(self, id, result, REQUEST=None, RESPONSE=None):
00469         """After MKCOL handler
00470 
00471         Set title according to the id
00472         """
00473         # manage_afterMKCOL is called in the context of the parent
00474         # folder, *not* in the context of the new folder!
00475         new = getattr(self, id)
00476         title = new.Title()
00477         if not title.strip():
00478             # Use the last-segment from the url as the id, as the
00479             # object might have been renamed somehow (eg: by id
00480             # mangling).
00481             request = REQUEST or getattr(self, 'REQUEST', None)
00482             if request is not None:
00483                 path_info = request.get('PATH_INFO')
00484                 if path_info:
00485                     id = posixpath.basename(path_info)
00486             new.update(title=id)
00487 
00488     security.declareProtected(View, 'HEAD')
00489     def HEAD(self, REQUEST, RESPONSE):
00490         """Overwrite HEAD method for HTTP HEAD requests
00491 
00492         Returns 404 Not Found if the default view can't be acquired or 405
00493         Method not allowed if the default view has no HEAD method.
00494         """
00495         view_id = self.getDefaultPage() or self.getLayout()
00496         view_method = getattr(self, view_id, None)
00497         if view_method is None:
00498             # view method couldn't be acquired
00499             raise NotFound, "View method %s for requested resource is not " \
00500                              "available." % view_id
00501         if getattr(aq_base(view_method), 'HEAD', None) is not None:
00502             # view method has a HEAD method
00503             return view_method.__of__(self).HEAD(REQUEST, RESPONSE)
00504         else:
00505             raise MethodNotAllowed, 'Method not supported for this resource.'
00506 
00507 InitializeClass(ATCTFolderMixin)
00508 
00509 
00510 class ATCTOrderedFolder(ATCTFolderMixin, OrderedBaseFolder):
00511     """Base class for orderable folderish AT Content Types"""
00512 
00513     __implements__ = (ATCTFolderMixin.__implements__,
00514                       OrderedBaseFolder.__implements__)
00515 
00516     security       = ClassSecurityInfo()
00517 
00518     security.declareProtected(View, 'index_html')
00519     def index_html(self, REQUEST=None, RESPONSE=None):
00520         """Special case index_html"""
00521         request = REQUEST
00522         if request is None:
00523             request = getattr(self, 'REQUEST', None)
00524         if request and request.has_key('REQUEST_METHOD'):
00525             if request.maybe_webdav_client:
00526                 method = request['REQUEST_METHOD']
00527                 if method in ('PUT',):
00528                     # Very likely a WebDAV client trying to create something
00529                     return ReplaceableWrapper(NullResource(self, 'index_html'))
00530                 elif method in ('GET', 'HEAD', 'POST'):
00531                     # Do nothing, let it go and acquire.
00532                     pass
00533                 else:
00534                     raise AttributeError, 'index_html'
00535         # Acquire from parent
00536         _target = aq_parent(aq_inner(self)).aq_acquire('index_html')
00537         return ReplaceableWrapper(aq_base(_target).__of__(self))
00538 
00539     index_html = ComputedAttribute(index_html, 1)
00540 
00541 InitializeClass(ATCTOrderedFolder)
00542 
00543 
00544 class ATCTBTreeFolder(ATCTFolderMixin, BaseBTreeFolder):
00545     """Base class for folderish AT Content Types using a BTree"""
00546 
00547     __implements__ = ATCTFolderMixin.__implements__, \
00548                      BaseBTreeFolder.__implements__
00549 
00550     security       = ClassSecurityInfo()
00551 
00552     security.declareProtected(View, 'index_html')
00553     def index_html(self, REQUEST=None, RESPONSE=None):
00554         """
00555         BTree folders don't store objects as attributes, the
00556         implementation of index_html method in PloneFolder assumes
00557         this and by virtue of being invoked looked in the parent
00558         container. We override here to check the BTree data structs,
00559         and then perform the same lookup as BasePloneFolder if we
00560         don't find it.
00561         """
00562         _target = self.get('index_html')
00563         if _target is not None:
00564             return _target
00565         _target = aq_parent(aq_inner(self)).aq_acquire('index_html')
00566         return ReplaceableWrapper(aq_base(_target).__of__(self))
00567 
00568     index_html = ComputedAttribute(index_html, 1)
00569 
00570 InitializeClass(ATCTBTreeFolder)
00571 
00572 
00573 __all__ = ('ATCTContent', 'ATCTFolder', 'ATCTOrderedFolder',
00574            'ATCTBTreeFolder' )