oood_commandtransform.py 8.19 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
from Products.ERP5OOo.OOoUtils import OOoBuilder
import re
from lxml import etree
15
from lxml import html
16 17
from lxml.etree import ParseError, Element

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

26 27 28 29 30 31 32

# 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


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

35 36
class OOoDocumentDataStream:
  """Handle OOoDocument in Portal Transforms"""
37
  implements(idatastream)
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 72

  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)
73
    self.__name__ = name
74
    self.mimetype = mimetype
75
    self.context = context
76
    if self.mimetype == 'text/html':
77
      data = self.includeExternalCssList(data)
78
    self.data = data
79 80 81 82

  def name(self):
    return self.__name__

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

    - data: zipped archive content
87 88 89
    """
    builder = OOoBuilder(data)
    content = builder.extract('content.xml')
Nicolas Delaby's avatar
Nicolas Delaby committed
90 91 92 93
    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'
94
    ratio_px_cm = 2.54 / 100.
95 96
    # Flag to enable modification of OOoBuilder
    odt_content_modified = False
97
    for image_tag in image_tag_list:
98
      frame = image_tag.getparent()
99
      #Try to get image file from ZODB
Nicolas Delaby's avatar
Nicolas Delaby committed
100 101
      href_attribute_list = image_tag.xpath('.//@*[name() = "xlink:href"]')
      url = href_attribute_list[0]
102 103 104 105
      parse_result = urlparse(unquote(url))
      # urlparse return a 6-tuple: scheme, netloc, path, params, query, fragment
      path = parse_result[2]
      if path:
106 107 108 109
        # 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)
110 111 112
        # 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('/../', '')
113 114 115
        # retrieve http parameters and use them to convert image
        query_parameter_string = parse_result[4]
        image_parameter_dict = dict(parse_qsl(query_parameter_string))
116 117
        try:
          image = self.context.restrictedTraverse(path)
118
        except (AttributeError, KeyError):
119 120 121
          #Image not found, this image is probably not hosted by ZODB. Do nothing
          image = None
        if image is not None:
122
          odt_content_modified = True
123
          content_type = image.getContentType()
124 125 126 127
          mimetype_list = getToolByName(self.context.getPortalObject(),
                                     'mimetypes_registry').lookup(content_type)

          format = image_parameter_dict.pop('format', None)
128 129 130 131 132 133 134 135
          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
136
          if getattr(image, 'meta_type', None) == 'ERP5 Image':
137
            #ERP5 API
138
            # resize image according parameters
139
            mime, image_data = image.convert(format, **image_parameter_dict)
140 141 142 143 144 145
            image = OFSImage(image.getId(), image.getTitle(), image_data)

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

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

    - data: html content
163 164
    """
    try:
Nicolas Delaby's avatar
Nicolas Delaby committed
165 166
      xml_doc = etree.XML(data)
    except ParseError:
167 168 169
      #If not valid xhtml do nothing
      return data
    xpath = '//*[local-name() = "link"][@type = "text/css"]'
Nicolas Delaby's avatar
Nicolas Delaby committed
170
    css_link_tag_list = xml_doc.xpath(xpath)
171 172
    for css_link_tag in css_link_tag_list:
      #Try to get css from ZODB
Nicolas Delaby's avatar
Nicolas Delaby committed
173 174
      href_attribute_list = css_link_tag.xpath('.//@href')
      url = href_attribute_list[0]
175 176 177 178
      parse_result = urlparse(unquote(url))
      # urlparse return a 6-tuple: scheme, netloc, path, params, query, fragment
      path = parse_result[2]
      if path:
179
        try:
180 181
          css_object = self.context.restrictedTraverse(path)
        except (AttributeError, KeyError):
182
          #Image not found, this image is probably not hosted by ZODB. Do nothing
183 184 185 186 187 188 189 190
          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)
191
          parent_node = css_link_tag.getparent()
Nicolas Delaby's avatar
Nicolas Delaby committed
192 193 194 195 196
          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)
197 198
    xml_output = html.tostring(xml_doc, encoding='utf-8', method='xml',
                               include_meta_content_type=True)
199 200
    xml_output = xml_output.replace('<title/>', '<title></title>')
    return xml_output
201

202
  def convertTo(self, format):
203 204 205 206 207 208 209 210 211 212 213 214
    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'])
215
      if self.mimetype == 'text/html':
216
        data = self.includeImageList(data)
217 218
      return data
    else:
219
      raise ConversionError('Format not allowed %s' % format)