Back to index

plone3  3.1.7
ExternalEditor.py
Go to the documentation of this file.
00001 ##############################################################################
00002 #
00003 # Copyright (c) 2001, 2002 Zope Corporation and Contributors.
00004 # All Rights Reserved.
00005 #
00006 # This software is subject to the provisions of the Zope Public License,
00007 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
00008 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00009 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00010 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00011 # FOR A PARTICULAR PURPOSE.
00012 #
00013 ##############################################################################
00014 """$Id: ExternalEditor.py 71655 2006-12-26 22:12:16Z sidnei $
00015 """
00016 
00017 # Zope External Editor Product by Casey Duncan
00018 
00019 from string import join # For Zope 2.3 compatibility
00020 import types
00021 import re
00022 import urllib
00023 import Acquisition
00024 from Globals import InitializeClass
00025 from App.Common import rfc1123_date
00026 from AccessControl.SecurityManagement import getSecurityManager
00027 from AccessControl.SecurityInfo import ClassSecurityInfo
00028 from OFS import Image
00029 try:
00030     from webdav.Lockable import wl_isLocked
00031 except ImportError:
00032     # webdav module not available
00033     def wl_isLocked(ob):
00034         return 0
00035 try:
00036     from ZPublisher.Iterators import IStreamIterator
00037 except ImportError:
00038     # pre-2.7.1 Zope without stream iterators
00039     IStreamIterator = None
00040 
00041 ExternalEditorPermission = 'Use external editor'
00042 
00043 _callbacks = []
00044 
00045 class PDataStreamIterator:
00046 
00047     __implements__ = (IStreamIterator,)
00048 
00049     def __init__(self, data):
00050         self.data = data
00051 
00052     def __iter__(self):
00053         return self
00054 
00055     def next(self):
00056         if self.data is None:
00057             raise StopIteration
00058         data = self.data.data
00059         self.data = self.data.next
00060         return data
00061 
00062 def registerCallback(cb):
00063     """Register a callback to be called by the External Editor when
00064     it's about to be finished with collecting metadata for the
00065     to-be-edited file to allow actions to be taken, like for example
00066     inserting more metadata headers or enabling response compression
00067     or anything.
00068     """
00069     _callbacks.append(cb)
00070 
00071 def applyCallbacks(ob, metadata, request, response):
00072     """Apply the registered callbacks in the order they were
00073     registered. The callbacks are free to perform any operation,
00074     including appending new metadata attributes and setting response
00075     headers.
00076     """
00077     for cb in _callbacks:
00078         cb(ob, metadata, request, response)
00079 
00080 class ExternalEditor(Acquisition.Implicit):
00081     """Create a response that encapsulates the data needed by the
00082        ZopeEdit helper application
00083     """
00084 
00085     security = ClassSecurityInfo()
00086     security.declareObjectProtected(ExternalEditorPermission)
00087 
00088     def __before_publishing_traverse__(self, self2, request):
00089         path = request['TraversalRequestNameStack']
00090         if path:
00091             target = path[-1]
00092             if request.get('macosx') and target.endswith('.zem'):
00093                 # Remove extension added by EditLink() for Mac finder
00094                 # so we can traverse to the target in Zope
00095                 target = target[:-4]
00096             request.set('target', target)
00097             path[:] = []
00098         else:
00099             request.set('target', None)
00100 
00101     def index_html(self, REQUEST, RESPONSE, path=None):
00102         """Publish the object to the external editor helper app"""
00103 
00104         security = getSecurityManager()
00105         if path is None:
00106             parent = self.aq_parent
00107             try:
00108                 ob = parent[REQUEST['target']] # Try getitem
00109             except KeyError:
00110                 ob = getattr(parent, REQUEST['target']) # Try getattr
00111             except AttributeError:
00112                 # Handle objects that are methods in ZClasses
00113                 ob = parent.propertysheets.methods[REQUEST['target']]
00114         else:
00115             ob = self.restrictedTraverse( path )
00116 
00117         r = []
00118         r.append('url:%s' % ob.absolute_url())
00119         r.append('meta_type:%s' % ob.meta_type)
00120 
00121         title = getattr(Acquisition.aq_base(ob), 'title', None)
00122         if title is not None:
00123             if callable(title):
00124                 title = title()
00125             if isinstance(title, types.UnicodeType):
00126                 title = unicode.encode(title, 'utf-8')
00127             r.append('title:%s' % title)
00128 
00129         if hasattr(Acquisition.aq_base(ob), 'content_type'):
00130             if callable(ob.content_type):
00131                 r.append('content_type:%s' % ob.content_type())
00132             else:
00133                 r.append('content_type:%s' % ob.content_type)
00134 
00135         if REQUEST._auth:
00136             if REQUEST._auth[-1] == '\n':
00137                 auth = REQUEST._auth[:-1]
00138             else:
00139                 auth = REQUEST._auth
00140 
00141             r.append('auth:%s' % auth)
00142 
00143         r.append('cookie:%s' % REQUEST.environ.get('HTTP_COOKIE',''))
00144 
00145         if wl_isLocked(ob):
00146             # Object is locked, send down the lock token
00147             # owned by this user (if any)
00148             user_id = security.getUser().getId()
00149             for lock in ob.wl_lockValues():
00150                 if not lock.isValid():
00151                     continue # Skip invalid/expired locks
00152                 creator = lock.getCreator()
00153                 if creator and creator[1] == user_id:
00154                     # Found a lock for this user, so send it
00155                     r.append('lock-token:%s' % lock.getLockToken())
00156                     if REQUEST.get('borrow_lock'):
00157                         r.append('borrow_lock:1')
00158                     break
00159 
00160         # Apply any extra callbacks that might have been registered.
00161         applyCallbacks(ob, r, REQUEST, RESPONSE)
00162 
00163         # Finish metadata with an empty line.
00164         r.append('')
00165         metadata = join(r, '\n')
00166         metadata_len = len(metadata)
00167 
00168         # Check if we should send the file's data down the response.
00169         if REQUEST.get('skip_data'):
00170             # We've been requested to send only the metadata. The
00171             # client will presumably fetch the data itself.
00172             self._write_metadata(RESPONSE, metadata, metadata_len)
00173             return ''
00174 
00175         ob_data = getattr(Acquisition.aq_base(ob), 'data', None)
00176         if (ob_data is not None and isinstance(ob_data, Image.Pdata)):
00177             # We have a File instance with chunked data, lets stream it.
00178             #
00179             # Note we are setting the content-length header here. This
00180             # is a simplification. Read comment below.
00181             #
00182             # We assume that ob.get_size() will return the exact size
00183             # of the PData chain. If that assumption is broken we
00184             # might have problems. This is mainly an optimization. If
00185             # we read the whole PData chain just to compute the
00186             # correct size that could cause the whole file to be read
00187             # into memory.
00188             RESPONSE.setHeader('Content-Length', ob.get_size())
00189             # It is safe to use this PDataStreamIterator here because
00190             # it is consumed right below. This is only used to
00191             # simplify the code below so it only has to deal with
00192             # stream iterators or plain strings.
00193             body = PDataStreamIterator(ob.data)
00194         elif hasattr(ob, 'manage_FTPget'):
00195             # Calling manage_FTPget *might* have side-effects. For
00196             # example, in Archetypes it does set the 'content-type'
00197             # response header, which would end up overriding our own
00198             # content-type header because we've set it 'too
00199             # early'. We've moved setting the content-type header to
00200             # the '_write_metadata' method since, and any manipulation
00201             # of response headers should happen there, if possible.
00202             try:
00203                 body = ob.manage_FTPget()
00204             except TypeError: # some need the R/R pair!
00205                 body = ob.manage_FTPget(REQUEST, RESPONSE)
00206         elif hasattr(ob, 'EditableBody'):
00207             body = ob.EditableBody()
00208         elif hasattr(ob, 'document_src'):
00209             body = ob.document_src(REQUEST, RESPONSE)
00210         elif hasattr(ob, 'read'):
00211             body = ob.read()
00212         else:
00213             # can't read it!
00214             raise 'BadRequest', 'Object does not support external editing'
00215 
00216         if (IStreamIterator is not None and
00217             IStreamIterator.isImplementedBy(body)):
00218             # We need to manage our content-length because we're streaming.
00219             # The content-length should have been set in the response by
00220             # the method that returns the iterator, but we need to fix it up
00221             # here because we insert metadata before the body.
00222             clen = RESPONSE.headers.get('content-length', None)
00223             assert clen is not None
00224             self._write_metadata(RESPONSE, metadata, metadata_len + int(clen))
00225             for data in body:
00226                 RESPONSE.write(data)
00227             return ''
00228 
00229         # If we reached this point, body *must* be a string. We *must*
00230         # set the headers ourselves since _write_metadata won't get
00231         # called.
00232         self._set_headers(RESPONSE)
00233         return join((metadata, body), '\n')
00234 
00235     def _set_headers(self, RESPONSE):
00236         # Using RESPONSE.setHeader('Pragma', 'no-cache') would be better, but
00237         # this chokes crappy most MSIE versions when downloads happen on SSL.
00238         # cf. http://support.microsoft.com/support/kb/articles/q316/4/31.asp
00239         RESPONSE.setHeader('Last-Modified', rfc1123_date())
00240         RESPONSE.setHeader('Content-Type', 'application/x-zope-edit')
00241 
00242     def _write_metadata(self, RESPONSE, metadata, length):
00243         # Set response content-type so that the browser gets hinted
00244         # about what application should handle this.
00245         self._set_headers(RESPONSE)
00246 
00247         # Set response length and write our metadata. The '+1' on the
00248         # content-length is the '\n' after the metadata.
00249         RESPONSE.setHeader('Content-Length', length + 1)
00250         RESPONSE.write(metadata)
00251         RESPONSE.write('\n')
00252 
00253 InitializeClass(ExternalEditor)
00254 
00255 is_mac_user_agent = re.compile('.*Mac OS X.*|.*Mac_PowerPC.*').match
00256 
00257 def EditLink(self, object, borrow_lock=0, skip_data=0):
00258     """Insert the external editor link to an object if appropriate"""
00259     base = Acquisition.aq_base(object)
00260     user = getSecurityManager().getUser()
00261     editable = (hasattr(base, 'manage_FTPget')
00262                 or hasattr(base, 'EditableBody')
00263                 or hasattr(base, 'document_src')
00264                 or hasattr(base, 'read'))
00265     if editable and user.has_permission(ExternalEditorPermission, object):
00266         query = {}
00267         if is_mac_user_agent(object.REQUEST['HTTP_USER_AGENT']):
00268             # Add extension to URL so that the Mac finder can
00269             # launch the ZopeEditManager helper app
00270             # this is a workaround for limited MIME type
00271             # support on MacOS X browsers
00272             ext = '.zem'
00273             query['macosx'] = 1
00274         else:
00275             ext = ''
00276         if borrow_lock:
00277             query['borrow_lock'] = 1
00278         if skip_data:
00279             query['skip_data'] = 1
00280         url = "%s/externalEdit_/%s%s%s" % (object.aq_parent.absolute_url(),
00281                                            urllib.quote(object.getId()),
00282                                            ext, querystr(query))
00283         return ('<a href="%s" '
00284                 'title="Edit using external editor">'
00285                 '<img src="%s/misc_/ExternalEditor/edit_icon" '
00286                 'align="middle" hspace="2" border="0" alt="External Editor" />'
00287                 '</a>' % (url, object.REQUEST.BASEPATH1)
00288                )
00289     else:
00290         return ''
00291 
00292 def querystr(d):
00293     """Create a query string from a dict"""
00294     if d:
00295         return '?' + '&'.join(
00296             ['%s=%s' % (name, val) for name, val in d.items()])
00297     else:
00298         return ''
00299