Back to index

plone3  3.1.7
Document.py
Go to the documentation of this file.
00001 ##############################################################################
00002 #
00003 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
00004 #
00005 # This software is subject to the provisions of the Zope Public License,
00006 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
00007 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00008 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00009 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00010 # FOR A PARTICULAR PURPOSE.
00011 #
00012 ##############################################################################
00013 """ Basic textual content object, supporting HTML, STX and plain text.
00014 
00015 $Id: Document.py 74063 2007-04-09 21:23:43Z tseaver $
00016 """
00017 
00018 import transaction
00019 from AccessControl import ClassSecurityInfo
00020 from AccessControl import getSecurityManager
00021 from Acquisition import aq_base
00022 from DocumentTemplate.DT_Util import html_quote
00023 from Globals import DTMLFile
00024 from Globals import InitializeClass
00025 from StructuredText.StructuredText import HTML
00026 from zope.component.factory import Factory
00027 from zope.interface import implements
00028 
00029 from Products.CMFCore.PortalContent import PortalContent
00030 from Products.CMFCore.utils import contributorsplitter
00031 from Products.CMFCore.utils import keywordsplitter
00032 from Products.GenericSetup.interfaces import IDAVAware
00033 
00034 from DublinCore import DefaultDublinCoreImpl
00035 from exceptions import EditingConflict
00036 from exceptions import ResourceLockedError
00037 from interfaces import IDocument
00038 from interfaces import IMutableDocument
00039 from interfaces.Document import IDocument as z2IDocument
00040 from interfaces.Document import IMutableDocument as z2IMutableDocument
00041 from permissions import ModifyPortalContent
00042 from permissions import View
00043 from utils import _dtmldir
00044 from utils import bodyfinder
00045 from utils import formatRFC822Headers
00046 from utils import html_headcheck
00047 from utils import Message as _
00048 from utils import parseHeadersBody
00049 from utils import SimpleHTMLParser
00050 
00051 
00052 def addDocument(self, id, title='', description='', text_format='', text=''):
00053     """Add a Document.
00054     """
00055     o = Document(id, title, description, text_format, text)
00056     self._setObject(id,o)
00057 
00058 
00059 class Document(PortalContent, DefaultDublinCoreImpl):
00060 
00061     """A Document - Handles both StructuredText and HTML.
00062     """
00063 
00064     implements(IMutableDocument, IDocument, IDAVAware)
00065     __implements__ = (z2IMutableDocument, z2IDocument,
00066                       PortalContent.__implements__,
00067                       DefaultDublinCoreImpl.__implements__)
00068 
00069     effective_date = expiration_date = None
00070     cooked_text = text = text_format = ''
00071     _size = 0
00072 
00073     _stx_level = 1                      # Structured text level
00074 
00075     _last_safety_belt_editor = ''
00076     _last_safety_belt = ''
00077     _safety_belt = ''
00078 
00079     security = ClassSecurityInfo()
00080 
00081     def __init__(self, id, title='', description='', text_format='', text=''):
00082         DefaultDublinCoreImpl.__init__(self)
00083         self.id = id
00084         self.title = title
00085         self.description = description
00086         self.setFormat(text_format)
00087         self._edit(text)
00088 
00089     security.declareProtected(ModifyPortalContent, 'manage_edit')
00090     manage_edit = DTMLFile('zmi_editDocument', _dtmldir)
00091 
00092     security.declareProtected(ModifyPortalContent, 'manage_editDocument')
00093     def manage_editDocument( self, text, text_format, file='', REQUEST=None ):
00094         """ A ZMI (Zope Management Interface) level editing method """
00095         Document.edit( self, text_format=text_format, text=text, file=file )
00096         if REQUEST is not None:
00097             REQUEST['RESPONSE'].redirect(
00098                 self.absolute_url()
00099                 + '/manage_edit'
00100                 + '?manage_tabs_message=Document+updated'
00101                 )
00102 
00103     def _edit(self, text):
00104         """ Edit the Document and cook the body.
00105         """
00106         self.text = text
00107         self._size = len(text)
00108 
00109         text_format = self.text_format
00110         if text_format == 'html':
00111             self.cooked_text = text
00112         elif text_format == 'plain':
00113             self.cooked_text = html_quote(text).replace('\n', '<br />')
00114         else:
00115             self.cooked_text = HTML(text, level=self._stx_level, header=0)
00116 
00117     #
00118     #   IMutableDocument method
00119     #
00120 
00121     security.declareProtected(ModifyPortalContent, 'edit')
00122     def edit(self, text_format, text, file='', safety_belt=''):
00123         """ Update the document.
00124 
00125         To add webDav support, we need to check if the content is locked, and if
00126         so return ResourceLockedError if not, call _edit.
00127 
00128         Note that this method expects to be called from a web form, and so
00129         disables header processing
00130         """
00131         self.failIfLocked()
00132         if not self._safety_belt_update(safety_belt=safety_belt):
00133             msg = _(u'Intervening changes from elsewhere detected. '
00134                     u'Please refetch the document and reapply your changes. '
00135                     u'(You may be able to recover your version using the '
00136                     u"browser 'back' button, but will have to apply them to "
00137                     u'a freshly fetched copy.)')
00138             raise EditingConflict(msg)
00139         if file and (type(file) is not type('')):
00140             contents=file.read()
00141             if contents:
00142                 text = contents
00143         if html_headcheck(text) and text_format.lower() != 'plain':
00144             text = bodyfinder(text)
00145         self.setFormat(text_format)
00146         self._edit(text)
00147         self.reindexObject()
00148 
00149     security.declareProtected(ModifyPortalContent, 'setMetadata')
00150     def setMetadata(self, headers):
00151         headers['Format'] = self.Format()
00152         new_subject = keywordsplitter(headers)
00153         headers['Subject'] = new_subject or self.Subject()
00154         new_contrib = contributorsplitter(headers)
00155         headers['Contributors'] = new_contrib or self.Contributors()
00156         for key, value in self.getMetadataHeaders():
00157             if not key in headers:
00158                 headers[key] = value
00159         self._editMetadata(title=headers['Title'],
00160                           subject=headers['Subject'],
00161                           description=headers['Description'],
00162                           contributors=headers['Contributors'],
00163                           effective_date=headers['Effective_date'],
00164                           expiration_date=headers['Expiration_date'],
00165                           format=headers['Format'],
00166                           language=headers['Language'],
00167                           rights=headers['Rights'],
00168                           )
00169 
00170     security.declarePrivate('guessFormat')
00171     def guessFormat(self, text):
00172         """ Simple stab at guessing the inner format of the text """
00173         if html_headcheck(text): return 'html'
00174         else: return 'structured-text'
00175 
00176     security.declarePrivate('handleText')
00177     def handleText(self, text, format=None, stx_level=None):
00178         """ Handles the raw text, returning headers, body, format """
00179         headers = {}
00180         if not format:
00181             format = self.guessFormat(text)
00182         if format == 'html':
00183             parser = SimpleHTMLParser()
00184             parser.feed(text)
00185             headers.update(parser.metatags)
00186             if parser.title:
00187                 headers['Title'] = parser.title
00188             body = bodyfinder(text)
00189         else:
00190             headers, body = parseHeadersBody(text, headers)
00191             if stx_level:
00192                 self._stx_level = stx_level
00193         return headers, body, format
00194 
00195     security.declarePublic( 'getMetadataHeaders' )
00196     def getMetadataHeaders(self):
00197         """Return RFC-822-style header spec."""
00198         hdrlist = DefaultDublinCoreImpl.getMetadataHeaders(self)
00199         hdrlist.append( ('SafetyBelt', self._safety_belt) )
00200         return hdrlist
00201 
00202     security.declarePublic( 'SafetyBelt' )
00203     def SafetyBelt(self):
00204         """Return the current safety belt setting.
00205         For web form hidden button."""
00206         return self._safety_belt
00207 
00208     security.declarePrivate('isValidSafetyBelt')
00209     def isValidSafetyBelt(self, safety_belt):
00210         """Check validity of safety belt.
00211         """
00212         if not safety_belt:
00213             # we have no safety belt value
00214             return True
00215         if self._safety_belt is None:
00216             # the current object has no safety belt (ie - freshly made)
00217             return True
00218         if safety_belt == self._safety_belt:
00219             # the safety belt does match the current one
00220             return True
00221         this_user = getSecurityManager().getUser().getId()
00222         if ((safety_belt == self._last_safety_belt)
00223                 and (this_user == self._last_safety_belt_editor)):
00224             # safety belt and user match last safety belt and user
00225             return True
00226         return False
00227 
00228     security.declarePrivate('updateSafetyBelt')
00229     def updateSafetyBelt(self, safety_belt):
00230         """Update safety belt tracking.
00231         """
00232         this_user = getSecurityManager().getUser().getId()
00233         self._last_safety_belt_editor = this_user
00234         self._last_safety_belt = safety_belt
00235         self._safety_belt = str(self._p_mtime)
00236 
00237     def _safety_belt_update(self, safety_belt=''):
00238         """Check validity of safety belt and update tracking if valid.
00239 
00240         Return 0 if safety belt is invalid, 1 otherwise.
00241 
00242         Note that the policy is deliberately lax if no safety belt value is
00243         present - "you're on your own if you don't use your safety belt".
00244 
00245         When present, either the safety belt token:
00246          - ... is the same as the current one given out, or
00247          - ... is the same as the last one given out, and the person doing the
00248            edit is the same as the last editor."""
00249 
00250         if not self.isValidSafetyBelt(safety_belt):
00251             return 0
00252         self.updateSafetyBelt(safety_belt)
00253         return 1
00254 
00255     ### Content accessor methods
00256 
00257     #
00258     #   IContentish method
00259     #
00260 
00261     security.declareProtected(View, 'SearchableText')
00262     def SearchableText(self):
00263         """ Used by the catalog for basic full text indexing """
00264         return "%s %s %s" % ( self.Title()
00265                             , self.Description()
00266                             , self.EditableBody()
00267                             )
00268 
00269     #
00270     #   IDocument methods
00271     #
00272 
00273     security.declareProtected(View, 'CookedBody')
00274     def CookedBody(self, stx_level=None, setlevel=0):
00275         """ Get the "cooked" (ready for presentation) form of the text.
00276 
00277         The prepared basic rendering of an object.  For Documents, this
00278         means pre-rendered structured text, or what was between the
00279         <BODY> tags of HTML.
00280 
00281         If the format is html, and 'stx_level' is not passed in or is the
00282         same as the object's current settings, return the cached cooked
00283         text.  Otherwise, recook.  If we recook and 'setlevel' is true,
00284         then set the recooked text and stx_level on the object.
00285         """
00286         if (self.text_format == 'html' or self.text_format == 'plain'
00287             or (stx_level is None)
00288             or (stx_level == self._stx_level)):
00289             return self.cooked_text
00290         else:
00291             cooked = HTML(self.text, level=stx_level, header=0)
00292             if setlevel:
00293                 self._stx_level = stx_level
00294                 self.cooked_text = cooked
00295             return cooked
00296 
00297     security.declareProtected(View, 'EditableBody')
00298     def EditableBody(self):
00299         """ Get the "raw" (as edited) form of the text.
00300 
00301         The editable body of text.  This is the raw structured text, or
00302         in the case of HTML, what was between the <BODY> tags.
00303         """
00304         return self.text
00305 
00306     #
00307     #   IDublinCore method
00308     #
00309 
00310     security.declareProtected(View, 'Format')
00311     def Format(self):
00312         """ Dublin Core Format element - resource format.
00313         """
00314         if self.text_format == 'html':
00315             return 'text/html'
00316         else:
00317             return 'text/plain'
00318 
00319     #
00320     #   IMutableDublinCore method
00321     #
00322 
00323     security.declareProtected(ModifyPortalContent, 'setFormat')
00324     def setFormat(self, format):
00325         """ Set text format and Dublin Core resource format.
00326         """
00327         value = str(format)
00328         old_value = self.text_format
00329 
00330         if value == 'text/html' or value == 'html':
00331             self.text_format = 'html'
00332         elif value == 'text/plain':
00333             if self.text_format not in ('structured-text', 'plain'):
00334                 self.text_format = 'structured-text'
00335         elif value == 'plain':
00336             self.text_format = 'plain'
00337         else:
00338             self.text_format = 'structured-text'
00339 
00340         # Did the format change? We might need to re-cook the content.
00341         if value != old_value:
00342             if html_headcheck(self.text) and value != 'plain':
00343                 self.text = bodyfinder(self.text)
00344 
00345             self._edit(self.text)
00346 
00347     ## FTP handlers
00348     security.declareProtected(ModifyPortalContent, 'PUT')
00349     def PUT(self, REQUEST, RESPONSE):
00350         """ Handle HTTP (and presumably FTP?) PUT requests """
00351         self.dav__init(REQUEST, RESPONSE)
00352         self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1)
00353 
00354         try:
00355             self.failIfLocked()
00356         except ResourceLockedError, msg:
00357             transaction.abort()
00358             RESPONSE.setStatus(423)
00359             return RESPONSE
00360 
00361         body = REQUEST.get('BODY', '')
00362         if REQUEST.get_header('Content-Type', '') == 'text/html':
00363             format = 'html'
00364         else:
00365             format = None
00366         headers, body, format = self.handleText(body, format)
00367 
00368         safety_belt = headers.get('SafetyBelt', '')
00369         if not self._safety_belt_update(safety_belt):
00370             # XXX Can we get an error msg through?  Should we be raising an
00371             #     exception, to be handled in the FTP mechanism?  Inquiring
00372             #     minds...
00373             transaction.abort()
00374             RESPONSE.setStatus(450)
00375             return RESPONSE
00376 
00377         self.setFormat(format)
00378         self.setMetadata(headers)
00379         self._edit(body)
00380         RESPONSE.setStatus(204)
00381         self.reindexObject()
00382         return RESPONSE
00383 
00384     _htmlsrc = (
00385         '<html>\n <head>\n'
00386         ' <title>%(title)s</title>\n'
00387        '%(metatags)s\n'
00388         ' </head>\n'
00389         ' <body>%(body)s</body>\n'
00390         '</html>\n'
00391         )
00392 
00393     security.declareProtected(View, 'manage_FTPget')
00394     def manage_FTPget(self):
00395         "Get the document body for FTP download (also used for the WebDAV SRC)"
00396         if self.Format() == 'text/html':
00397             ti = self.getTypeInfo()
00398             method_id = ti and ti.queryMethodID('gethtml', context=self)
00399             if method_id:
00400                 method = getattr(self, method_id)
00401                 if getattr(aq_base(method), 'isDocTemp', 0):
00402                     bodytext = method(self, self.REQUEST)
00403                 else:
00404                     bodytext = method()
00405             else:
00406                 # Use the old code as fallback. May be removed some day.
00407                 hdrlist = self.getMetadataHeaders()
00408                 hdrtext = ''
00409                 for name, content in hdrlist:
00410                     if name.lower() == 'title':
00411                         continue
00412                     else:
00413                         hdrtext = '%s\n <meta name="%s" content="%s" />' % (
00414                             hdrtext, name, content)
00415 
00416                 bodytext = self._htmlsrc % {
00417                     'title': self.Title(),
00418                     'metatags': hdrtext,
00419                     'body': self.EditableBody(),
00420                     }
00421         else:
00422             hdrlist = self.getMetadataHeaders()
00423             hdrtext = formatRFC822Headers( hdrlist )
00424             bodytext = '%s\r\n\r\n%s' % ( hdrtext, self.text )
00425 
00426         return bodytext
00427 
00428     security.declareProtected(View, 'get_size')
00429     def get_size( self ):
00430         """ Used for FTP and apparently the ZMI now too """
00431         return self._size
00432 
00433 InitializeClass(Document)
00434 
00435 DocumentFactory = Factory(Document)