oood_commandtransform.py 8.07 KB
Newer Older
1
# -*- coding: utf-8 -*-
2 3 4
from Products.PortalTransforms.libtransforms.commandtransform import commandtransform
from Products.PortalTransforms.interfaces import idatastream
from Products.ERP5Type.Document import newTempOOoDocument
5
from Products.ERP5.Document.Document import ConversionError
6
from Products.CMFCore.utils import getToolByName
7
from Acquisition import aq_base
8
from zope.interface import implements
9 10 11
from OFS.Image import Image as OFSImage
from zLOG import LOG

12 13 14 15 16
from Products.ERP5OOo.OOoUtils import OOoBuilder
import re
from lxml import etree
from lxml.etree import ParseError, Element

17 18 19 20 21 22 23
from urllib import unquote
from urlparse import urlparse
try:
  # Python >= 2.6
  from urlparse import parse_qsl
except ImportError:
  from cgi import parse_qsl
24

25 26 27 28 29 30 31

# XXX Must be replaced by portal_data_adapters soon
from Products.ERP5OOo.Document.OOoDocument import OOoServerProxy
from Products.ERP5OOo.Document.OOoDocument import enc
from Products.ERP5OOo.Document.OOoDocument import dec


32
CLEAN_RELATIVE_PATH = re.compile('^../')
33

34 35
class OOoDocumentDataStream:
  """Handle OOoDocument in Portal Transforms"""
36
  implements(idatastream)
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71

  def setData(self, value):
    """set the main"""
    self.value = value

  def getData(self):
    return self.value

  def setSubObjects(self, objects):
    pass

  def getSubObjects(self):
    return {}

  def getMetadata(self):
    """return a dict-like object with any optional metadata from
    the transform
    You can modify the returned dictionnary to add/change metadata
    """
    return {}

  def isCacheable(self):
    """
     True by Default
    """
    return getattr(self, '_is_cacheable', True)

  def setCachable(self, value):
    self._is_cacheable = value

class OOOdCommandTransform(commandtransform):
  """Transformer using oood"""

  def __init__(self, context, name, data, mimetype):
    commandtransform.__init__(self, name)
72
    self.__name__ = name
73
    self.mimetype = mimetype
74
    self.context = context
75
    if self.mimetype == 'text/html':
76
      data = self.includeExternalCssList(data)
77
    self.data = data
78 79 80 81

  def name(self):
    return self.__name__

82
  def includeImageList(self, data):
83 84 85
    """Include Images in ODF archive

    - data: zipped archive content
86 87 88
    """
    builder = OOoBuilder(data)
    content = builder.extract('content.xml')
Nicolas Delaby's avatar
Nicolas Delaby committed
89 90 91 92
    xml_doc = etree.XML(content)
    image_tag_list = xml_doc.xpath('//*[name() = "draw:image"]')
    SVG_NAMESPACE = 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'
    XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink'
93
    ratio_px_cm = 2.54 / 100.
94 95
    # Flag to enable modification of OOoBuilder
    odt_content_modified = False
96
    for image_tag in image_tag_list:
97
      frame = image_tag.getparent()
98
      #Try to get image file from ZODB
Nicolas Delaby's avatar
Nicolas Delaby committed
99 100
      href_attribute_list = image_tag.xpath('.//@*[name() = "xlink:href"]')
      url = href_attribute_list[0]
101 102 103 104
      parse_result = urlparse(unquote(url))
      # urlparse return a 6-tuple: scheme, netloc, path, params, query, fragment
      path = parse_result[2]
      if path:
105 106 107 108
        # OOo corrupt relative Links inside HTML content during odt conversion
        # <img src="REF.TO.IMAGE" ... /> become <draw:image xlink:href="../REF.TO.IMAGE" ... />
        # So remove "../" added by OOo
        path = CLEAN_RELATIVE_PATH.sub('', path)
109 110 111
        # in some cases like Web Page content "/../" can be contained in image URL which will break 
        # restrictedTraverse calls, our best guess is to remove it
        path = path.replace('/../', '')
112 113 114
        # retrieve http parameters and use them to convert image
        query_parameter_string = parse_result[4]
        image_parameter_dict = dict(parse_qsl(query_parameter_string))
115 116
        try:
          image = self.context.restrictedTraverse(path)
117
        except (AttributeError, KeyError):
118 119 120
          #Image not found, this image is probably not hosted by ZODB. Do nothing
          image = None
        if image is not None:
121
          odt_content_modified = True
122
          content_type = image.getContentType()
123 124 125 126
          mimetype_list = getToolByName(self.context.getPortalObject(),
                                     'mimetypes_registry').lookup(content_type)

          format = image_parameter_dict.pop('format', None)
127 128 129 130 131 132 133 134
          if not format:
            for mimetype_object in mimetype_list:
              if mimetype_object.extensions:
                format = mimetype_object.extensions[0]
                break
              elif mimetype_object.globs:
                format = mimetype_object.globs.strip('*.')
                break
135
          if getattr(image, 'meta_type', None) == 'ERP5 Image':
136
            #ERP5 API
137
            # resize image according parameters
138
            mime, image_data = image.convert(format, **image_parameter_dict)
139 140 141 142 143 144
            image = OFSImage(image.getId(), image.getTitle(), image_data)

          # image should be OFSImage
          data = image.data
          width = image.width
          height = image.height
145
          if height:
Nicolas Delaby's avatar
Nicolas Delaby committed
146
            frame.attrib.update({'{%s}height' % SVG_NAMESPACE: '%.3fcm' % (height * ratio_px_cm)})
147
          if width:
Nicolas Delaby's avatar
Nicolas Delaby committed
148
            frame.attrib.update({'{%s}width' % SVG_NAMESPACE: '%.3fcm' % (width * ratio_px_cm)})
149
          new_path = builder.addImage(data, format=format)
Nicolas Delaby's avatar
Nicolas Delaby committed
150
          image_tag.attrib.update({'{%s}href' % XLINK_NAMESPACE: new_path})
151 152 153 154
    if odt_content_modified:
      builder.replace('content.xml', etree.tostring(xml_doc, encoding='utf-8',
                                                    xml_declaration=True,
                                                    pretty_print=False))
155 156 157
    return builder.render()

  def includeExternalCssList(self, data):
158 159 160 161
    """Replace external Css link by style Element,
    to avoid ooo querying portal without crendentials through http.

    - data: html content
162 163
    """
    try:
Nicolas Delaby's avatar
Nicolas Delaby committed
164 165
      xml_doc = etree.XML(data)
    except ParseError:
166 167 168
      #If not valid xhtml do nothing
      return data
    xpath = '//*[local-name() = "link"][@type = "text/css"]'
Nicolas Delaby's avatar
Nicolas Delaby committed
169
    css_link_tag_list = xml_doc.xpath(xpath)
170 171
    for css_link_tag in css_link_tag_list:
      #Try to get css from ZODB
Nicolas Delaby's avatar
Nicolas Delaby committed
172 173
      href_attribute_list = css_link_tag.xpath('.//@href')
      url = href_attribute_list[0]
174 175 176 177
      parse_result = urlparse(unquote(url))
      # urlparse return a 6-tuple: scheme, netloc, path, params, query, fragment
      path = parse_result[2]
      if path:
178
        try:
179 180
          css_object = self.context.restrictedTraverse(path)
        except (AttributeError, KeyError):
181
          #Image not found, this image is probably not hosted by ZODB. Do nothing
182 183 184 185 186 187 188 189
          css_object = None
        if css_object is not None:
          if callable(aq_base(css_object)):
            #In case of DTMLDocument
            css_as_text = css_object(client=self.context.getPortalObject())
          else:
            #Other cases like files
            css_as_text = str(css_object)
190
          parent_node = css_link_tag.getparent()
Nicolas Delaby's avatar
Nicolas Delaby committed
191 192 193 194 195 196 197
          style_node = Element('style')
          style_node.text = css_as_text
          parent_node.append(style_node)
          style_node.attrib.update({'type': 'text/css'})
          parent_node.remove(css_link_tag)
    return etree.tostring(xml_doc, encoding='utf-8',
                          xml_declaration=False, pretty_print=False, )
198

199
  def convertTo(self, format):
200 201 202 203 204 205 206 207 208 209 210 211
    server_proxy = OOoServerProxy(self.context)
    response_code, response_dict, message = \
                           server_proxy.getAllowedTargetItemList(self.mimetype)
    allowed_extension_list = response_dict['response_data']
    if format in dict(allowed_extension_list):
      response_code, response_dict, message = server_proxy.run_generate(
                                                                '',
                                                                enc(self.data),
                                                                None,
                                                                format,
                                                                self.mimetype)
      data = dec(response_dict['data'])
212
      if self.mimetype == 'text/html':
213
        data = self.includeImageList(data)
214 215
      return data
    else:
216
      raise ConversionError('Format not allowed %s' % format)