############################################################################## # # Copyright (c) 2001, 2002 Zope Corporation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """$Id: ExternalEditor.py 69097 2006-07-11 19:51:48Z sidnei $ """ # Zope External Editor Product by Casey Duncan from string import join # For Zope 2.3 compatibility import types import re import urllib import Acquisition from Globals import InitializeClass from App.Common import rfc1123_date from AccessControl.SecurityManagement import getSecurityManager from AccessControl.SecurityInfo import ClassSecurityInfo from OFS import Image try: from webdav.Lockable import wl_isLocked except ImportError: # webdav module not available def wl_isLocked(ob): return 0 try: from ZPublisher.Iterators import IStreamIterator except ImportError: # pre-2.7.1 Zope without stream iterators IStreamIterator = None ExternalEditorPermission = 'Use external editor' _callbacks = [] class PDataStreamIterator: __implements__ = (IStreamIterator,) def __init__(self, data): self.data = data def __iter__(self): return iter(self) def next(self): if self.data is None: raise StopIteration data = self.data.data self.data = self.data.next return data def registerCallback(cb): """Register a callback to be called by the External Editor when it's about to be finished with collecting metadata for the to-be-edited file to allow actions to be taken, like for example inserting more metadata headers or enabling response compression or anything. """ _callbacks.append(cb) def applyCallbacks(ob, metadata, request, response): """Apply the registered callbacks in the order they were registered. The callbacks are free to perform any operation, including appending new metadata attributes and setting response headers. """ for cb in _callbacks: cb(ob, metadata, request, response) class ExternalEditor(Acquisition.Implicit): """Create a response that encapsulates the data needed by the ZopeEdit helper application """ security = ClassSecurityInfo() security.declareObjectProtected(ExternalEditorPermission) def __before_publishing_traverse__(self, self2, request): path = request['TraversalRequestNameStack'] if path: target = path[-1] if request.get('macosx') and target.endswith('.zem'): # Remove extension added by EditLink() for Mac finder # so we can traverse to the target in Zope target = target[:-4] request.set('target', target) path[:] = [] else: request.set('target', None) def index_html(self, REQUEST, RESPONSE, path=None): """Publish the object to the external editor helper app""" security = getSecurityManager() if path is None: parent = self.aq_parent try: ob = parent[REQUEST['target']] # Try getitem except KeyError: ob = getattr(parent, REQUEST['target']) # Try getattr except AttributeError: # Handle objects that are methods in ZClasses ob = parent.propertysheets.methods[REQUEST['target']] else: ob = self.restrictedTraverse( path ) r = [] r.append('url:%s' % ob.absolute_url()) r.append('meta_type:%s' % ob.meta_type) title = getattr(Acquisition.aq_base(ob), 'title', None) if title is not None: if callable(title): title = title() if isinstance(title, types.UnicodeType): title = unicode.encode(title, 'utf-8') r.append('title:%s' % title) if hasattr(Acquisition.aq_base(ob), 'content_type'): if callable(ob.content_type): r.append('content_type:%s' % ob.content_type()) else: r.append('content_type:%s' % ob.content_type) if REQUEST._auth: if REQUEST._auth[-1] == '\n': auth = REQUEST._auth[:-1] else: auth = REQUEST._auth r.append('auth:%s' % auth) r.append('cookie:%s' % REQUEST.environ.get('HTTP_COOKIE','')) if wl_isLocked(ob): # Object is locked, send down the lock token # owned by this user (if any) user_id = security.getUser().getId() for lock in ob.wl_lockValues(): if not lock.isValid(): continue # Skip invalid/expired locks creator = lock.getCreator() if creator and creator[1] == user_id: # Found a lock for this user, so send it r.append('lock-token:%s' % lock.getLockToken()) if REQUEST.get('borrow_lock'): r.append('borrow_lock:1') break # Apply any extra callbacks that might have been registered. applyCallbacks(ob, r, REQUEST, RESPONSE) # Finish metadata with an empty line. r.append('') metadata = join(r, '\n') metadata_len = len(metadata) # Using RESPONSE.setHeader('Pragma', 'no-cache') would be better, but # this chokes crappy most MSIE versions when downloads happen on SSL. # cf. http://support.microsoft.com/support/kb/articles/q316/4/31.asp RESPONSE.setHeader('Last-Modified', rfc1123_date()) RESPONSE.setHeader('Content-Type', 'application/x-zope-edit') # Check if we should send the file's data down the response. if REQUEST.get('skip_data'): # We've been requested to send only the metadata. The # client will presumably fetch the data itself. self._write_metadata(RESPONSE, metadata, metadata_len) return '' ob_data = getattr(Acquisition.aq_base(ob), 'data', None) if (ob_data is not None and isinstance(ob_data, Image.Pdata)): # We have a File instance with chunked data, lets stream it RESPONSE.setHeader('Content-Length', ob.get_size()) body = PDataStreamIterator(ob.data) elif hasattr(ob, 'manage_FTPget'): try: body = ob.manage_FTPget() except TypeError: # some need the R/R pair! body = ob.manage_FTPget(REQUEST, RESPONSE) elif hasattr(ob, 'EditableBody'): body = ob.EditableBody() elif hasattr(ob, 'document_src'): body = ob.document_src(REQUEST, RESPONSE) elif hasattr(ob, 'read'): body = ob.read() else: # can't read it! raise 'BadRequest', 'Object does not support external editing' if (IStreamIterator is not None and IStreamIterator.isImplementedBy(body)): # We need to manage our content-length because we're streaming. # The content-length should have been set in the response by # the method that returns the iterator, but we need to fix it up # here because we insert metadata before the body. clen = RESPONSE.headers.get('content-length', None) assert clen is not None self._write_metadata(RESPONSE, metadata, metadata_len + int(clen)) for data in body: RESPONSE.write(data) return '' # If we reached this point, body *must* be a string. return join((metadata, body), '\n') def _write_metadata(self, RESPONSE, metadata, length): RESPONSE.setHeader('Content-Length', length + 1) RESPONSE.write(metadata) RESPONSE.write('\n') InitializeClass(ExternalEditor) is_mac_user_agent = re.compile('.*Mac OS X.*|.*Mac_PowerPC.*').match def EditLink(self, object, borrow_lock=0, skip_data=0): """Insert the external editor link to an object if appropriate""" base = Acquisition.aq_base(object) user = getSecurityManager().getUser() editable = (hasattr(base, 'manage_FTPget') or hasattr(base, 'EditableBody') or hasattr(base, 'document_src') or hasattr(base, 'read')) if editable and user.has_permission(ExternalEditorPermission, object): query = {} if is_mac_user_agent(object.REQUEST['HTTP_USER_AGENT']): # Add extension to URL so that the Mac finder can # launch the ZopeEditManager helper app # this is a workaround for limited MIME type # support on MacOS X browsers ext = '.zem' query['macosx'] = 1 else: ext = '' if borrow_lock: query['borrow_lock'] = 1 if skip_data: query['skip_data'] = 1 url = "%s/externalEdit_/%s%s%s" % (object.aq_parent.absolute_url(), urllib.quote(object.getId()), ext, querystr(query)) return ('' 'External Editor' '' % (url, object.REQUEST.BASEPATH1) ) else: return '' def querystr(d): """Create a query string from a dict""" if d: return '?' + '&'.join( ['%s=%s' % (name, val) for name, val in d.items()]) else: return ''