# Copyright (c) 2002-2005 Infrae. All rights reserved. # See also LICENSE.txt # $Revision: 1.35 $ # Zope from StringIO import StringIO import re import sys import traceback import urlparse from AccessControl import ClassSecurityInfo from Products.PageTemplates.PageTemplateFile import PageTemplateFile from Globals import InitializeClass from Persistence import Persistent from zExceptions import InternalError from webdav.common import Conflict from Products.ParsedXML.ExtraDOM import writeStream from Products.ParsedXML.ParsedXML import createDOMDocument from Products.ParsedXML.PrettyPrinter import _translateCdata from Products.ParsedXML.ParsedXML import ParsedXML # Silva from Products.Silva import SilvaPermissions from Products.Silva.VersionedContent import CatalogedVersionedContent from Products.Silva.helpers import add_and_edit, translateCdata from Products.Silva.Version import CatalogedVersion from Products.Silva import mangle from Products.Silva.ImporterRegistry import importer_registry, xml_import_helper, get_xml_id, get_xml_title from Products.Silva.Metadata import export_metadata from Products.Silva.i18n import translate as _ from Products.Silva.ContentObjectFactoryRegistry import contentObjectFactoryRegistry # For XML-Conversions for editors from transform.Transformer import EditorTransformer from transform.base import Context from Products.Silva.interfaces import IVersionedContent, IContainerPolicy from Products.Silva.interfaces import IVersion from Products.Silva.Versioning import VersioningError from Products.SilvaDocument.i18n import translate as _ from Products.SilvaDocument import externalsource from xml.sax import make_parser from xml.sax.handler import ContentHandler from Products.SilvaMetadata.Exceptions import BindingError icon="www/silvadoc.gif" addable_priority = -1 class Document(CatalogedVersionedContent): __doc__ = _( """A Document is the basic unit of information in Silva. A document can – much like word processor documents – contain text, lists, tables, headers, subheads, images, etc. Documents can have two accessible versions, one online for the public, another in process (editable or approved/published). Older versions can be rolled forward and made editable. """) security = ClassSecurityInfo() meta_type = "Silva Document" __implements__ = IVersionedContent # some scary DAV stuff... __dav_collection__ = False isAnObjectManager = False # A hackish way, to get a Silva tab in between the standard ZMI tabs inherited_manage_options = CatalogedVersionedContent.manage_options manage_options=( (inherited_manage_options[0],)+ ({'label':'Silva /edit...', 'action':'edit'},)+ inherited_manage_options[1:] ) # ACCESSORS security.declareProtected(SilvaPermissions.View, 'is_cacheable') def is_cacheable(self): """Return true if this document is cacheable. That means the document contains no dynamic elements like code, datasource or toc. """ non_cacheable_elements = ['toc', 'code',] viewable = self.get_viewable() if viewable is None: return 0 # It should suffice to test the children of the root element only, # since currently the only non-cacheable elements are root elements for node in viewable.content.documentElement.childNodes: node_name = node.nodeName if node_name in non_cacheable_elements: return 0 # FIXME: how can we make this more generic as it is very # specific now..? if node_name == 'source': is_cacheable = externalsource.isSourceCacheable( self.aq_inner, node) if not is_cacheable: return 0 return 1 security.declareProtected(SilvaPermissions.ReadSilvaContent, 'to_xml') def to_xml(self, context): """Render object to XML. """ f = context.f if context.last_version == 1: version_id = self.get_next_version() if version_id is None: version_id = self.get_public_version() else: version_id = self.get_public_version() if version_id is None: return f.write('' % self.id) version = getattr(self, version_id) version.to_xml(context) f.write('') security.declareProtected(SilvaPermissions.ChangeSilvaContent, 'editor_storage') def editor_storage(self, string=None, editor='kupu', encoding=None): """provide xml/xhtml/html (GET requests) and (heuristic) back-transforming to xml/xhtml/html (POST requests) """ transformer = EditorTransformer(editor=editor) # we need to know what browser we are dealing with in order to know # what html to produce, unfortunately Mozilla uses different tags in # some cases (b instead of strong, i instead of em) browser = 'Mozilla' if self.REQUEST['HTTP_USER_AGENT'].find('MSIE') > -1: browser = 'IE' if string is None: ctx = Context(f=StringIO(), last_version=1, url=self.absolute_url(), browser=browser, model=self) self.to_xml(ctx) htmlnode = transformer.to_target(sourceobj=ctx.f.getvalue(), context=ctx) if encoding is not None: ret = htmlnode.asBytes(encoding=encoding) ret = ret.replace('\xa0', ' ') else: ret = unicode(htmlnode.asBytes('utf8'),'utf8') ret = ret.replace(u'\xa0', u' ') #print 'returning:', repr(ret) return ret else: #print 'incoming', repr(string) version = self.get_editable() if version is None: raise _("No version to store to!") # get any metadata elements saved to the metadata # XXX currently uses a completely different machinery to parse # the XML, perhaps we want to move that to the transformations # at some point (but would require too much hacking for now) errors = self._set_metadata_from_html(string, version) if errors: raise BindingError, errors ctx = Context(url=self.absolute_url(), browser=browser, model=self, request=self.REQUEST) silvanode = transformer.to_source(targetobj=string, context=ctx)[0] title = silvanode.find('title')[0].extract_text() docnode = silvanode.find('doc')[0] content = docnode.asBytes(encoding="UTF8") version.content.manage_edit(content) # needs utf8-encoded string version.set_title(title) # needs unicode version.sec_update_last_author_info() # Clear widget cache for this version. version.clearEditorCache() #print 'storing:', repr(content) def _set_metadata_from_html(self, html, version): """Rip the metadata out of the HTML (meta tags), set it on version""" # XXX obviously it would make sense if the transformations could # tackle this instead of doing it seperately, however, they are messy # enough as they are now (and not capable of such a thing yet) # XXX SAX took about 2 seconds (!) to get the meta values, # so I decided to try re instead... this made sense, parsing now takes # less than one hundreth of a second (usually even less than one # thousandth!) metamapping = {} import re reg = re.compile(r'\]+\/\>') while 1: match = reg.search(html) if not match: break tag = match.group(0) html = html.replace(tag, '') reg_props = re.compile(r'([a-zA-Z_]+)="([^"]+)"') found = {} while 1: match = reg_props.search(tag) if not match: break tag = tag.replace(match.group(0), '') if match.group(1) in ['name', 'content', 'scheme']: found[match.group(1)] = self._deentitize(match.group(2)) if (found.has_key('name') and found.has_key('content') and found.has_key('scheme')): if not metamapping.has_key(found['scheme']): metamapping[found['scheme']] = {} metamapping[found['scheme']][found['name']] = found['content'] errors = {} binding = self.service_metadata.getMetadata(self.get_editable()) for namespace, values in metamapping.items(): # %!#$%# metadata system expects UTF-8 :( for key, value in values.items(): del values[key] if value is None: continue values[key.encode('UTF-8')] = value.encode('UTF-8') set = self.service_metadata.getMetadataSetFor(namespace) ret = binding.setValues(set.id, values, reindex=1) if ret: errors.update(ret) return errors def _deentitize(self, xml): return xml.replace('<', '<').replace('>', '>').\ replace('"', '"').replace('&', '&') def revert_to_previous(self): """ Create a new version of public version, throw away the current one. Overrides Versioning.revert_to_previous() to be able to clear the widget cache too. """ Document.inheritedAttribute('revert_to_previous')(self) version = self.get_editable() version.clearEditorCache() def manage_beforeDelete(self, item, container): Document.inheritedAttribute('manage_beforeDelete')(self, item, container) # Does the widget cache needs be cleared for all versions - I think so... for version in self._get_indexable_versions(): version_object = getattr(self, str(version), None) if version_object: self.service_editor.clearCache(version_object.content) def transform_and_store(self, content_type, content): ret = content try: if content_type in ['text/html', 'application/xhtml+xml']: html = content self.editor_storage(html, 'kupu') # invalidate Document cache version = self.get_editable() version.clearEditorCache() # XXX This can be removed, right? ret = self.editor_storage(editor='kupu', encoding="UTF-8") else: # plain Silva document XML self.get_editable().content.manage_edit(content) except: # a tad ugly, but for debug purposes it would be nice to see # what's actually gone wrong exc, e, tb = sys.exc_info() # XXX should store to zLog instead print '%s: %s' % (exc, e) print '\n%s' % '\n'.join(traceback.format_tb(tb)) print # reraise the exception so the enduser at least sees something's # gone wrong raise return ret # WebDAV API security.declareProtected(SilvaPermissions.ChangeSilvaContent, 'PUT') def PUT(self, REQUEST=None, RESPONSE=None): """PUT support""" # XXX we may want to make this more modular/pluggable at some point # to allow more content-types (+ transformations) if REQUEST is None: REQUEST = self.REQUEST if RESPONSE is None: RESPONSE = REQUEST.RESPONSE content = REQUEST['BODYFILE'].read() content_type = self._get_content_type_from_request(REQUEST, content) ret = self.transform_and_store(content_type, content) def _get_content_type_from_request(self, request, content): """tries to figure out the content type of a PUT body from a request the request may not have a content-type header available, if so we should use the contents itself to determine the content type """ ct = request.get_header('content-type') if ct is not None: return ct content = re.sub('', '', content).strip() if content.startswith('') # display edit screen as main management screen security.declareProtected('View management screens', 'manage_main') manage_main = PageTemplateFile('www/documentVersionEdit', globals()) def to_xml(self, context): f = context.f f.write('%s' % translateCdata(self.get_title())) self.get_xml_content(f) export_metadata(self, context) def get_xml_content(self, f): self.content.documentElement.writeStream(f) def manage_afterClone(self, item): # if we're a copy, clear cache # XXX should be part of workflow system self.clearEditorCache() security.declareProtected(SilvaPermissions.AccessContentsInformation, 'fulltext') def fulltext(self): """Return the content of this object without any xml""" if self.version_status() == 'unapproved': return '' return ' '.join((self.get_title(), self._flattenxml(self.content_xml()))) security.declareProtected(SilvaPermissions.AccessContentsInformation, 'content_xml') def content_xml(self): """Returns the documentElement of the content's XML """ s = StringIO() self.content.documentElement.writeStream(s) value = s.getvalue() s.close() return value def _flattenxml(self, xmlinput): """Cuts out all the XML-tags, helper for fulltext (for content-objects) """ # XXX this need to be fixed by using ZCTextIndex or the like return xmlinput def clearEditorCache(self): """ Clears editor cache for this version """ editor_service = self.service_editor document_element = self.content.documentElement editor_service.clearCache(document_element) InitializeClass(DocumentVersion) manage_addDocumentForm = PageTemplateFile("www/documentAdd", globals(), __name__='manage_addDocumentForm') def manage_addDocument(self, id, title, REQUEST=None): """Add a Document.""" if not mangle.Id(self, id).isValid(): return object = Document(id) self._setObject(id, object) object = getattr(self, id) object.manage_addProduct['SilvaDocument'].manage_addDocumentVersion( '0', title) object.create_version('0', None, None) add_and_edit(self, id, REQUEST) return '' manage_addDocumentVersionForm = PageTemplateFile( "www/documentVersionAdd", globals(), __name__='manage_addDocumentVersionForm') def manage_addDocumentVersion(self, id, title, REQUEST=None): """Add a Document version to the Silva-instance.""" version = DocumentVersion(id, title) self._setObject(id, version) version = self._getOb(id) version.set_title(title) add_and_edit(self, id, REQUEST) return '' def xml_import_handler(object, node): id = get_xml_id(node) title = get_xml_title(node) id = str(mangle.Id(object, id).unique()) object.manage_addProduct['SilvaDocument'].manage_addDocument(id, title) newdoc = getattr(object, id) newdoc.sec_update_last_author_info() for child in node.childNodes: if child.nodeName == u'doc': version = getattr(newdoc, '0') childxml = writeStream(child).getvalue().encode('utf8') version.content.manage_edit(childxml) # expects utf8 elif hasattr(newdoc, 'set_%s' % child.nodeName.encode('utf8')) \ and child.nodeValue: getattr(newdoc, 'set_%s' % child.nodeName.encode('utf8'))( child.nodeValue.encode('utf8')) return newdoc class SilvaDocumentPolicy(Persistent): __implements__ = IContainerPolicy def createDefaultDocument(self, container, title): container.manage_addProduct['SilvaDocument'].manage_addDocument( 'index', title) container.index.sec_update_last_author_info() def document_factory(self, id, title, body): """Add a Document.""" if not mangle.Id(self, id).isValid(): return obj = Document(id).__of__(self) version = DocumentVersion('0', title).__of__(self) obj._setObject('0', version) obj.create_version('0', None, None) version = obj.get_editable() version.content.manage_edit(body) return obj def _should_create_document(id, ct, body): rightct = (ct in ['application/x-silva-document-xml']) rightext = id.endswith('.slv') return rightct or rightext contentObjectFactoryRegistry.registerFactory( document_factory, _should_create_document)