/
resolveuid_and_caption.py
378 lines (329 loc) · 13.4 KB
/
resolveuid_and_caption.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
from ZODB.POSException import ConflictError
from Acquisition import aq_base, aq_acquire, aq_parent
from zExceptions import NotFound
from zope.publisher.interfaces import NotFound as ztkNotFound
from DocumentTemplate.DT_Util import html_quote
from DocumentTemplate.DT_Var import newline_to_br
try:
from zope.component.hooks import getSite
except ImportError:
from zope.app.component.hooks import getSite
from zope.cachedescriptors.property import Lazy as lazy_property
from zope.component import getAllUtilitiesRegisteredFor
from zope.interface import implements, Interface, Attribute
from plone.outputfilters.browser.resolveuid import uuidToObject
import re
from urllib import unquote
from urlparse import urljoin
from urlparse import urlsplit
from sgmllib import SGMLParser, SGMLParseError
HAS_LINGUAPLONE = True
try:
from Products.LinguaPlone.utils import translated_references
except ImportError:
HAS_LINGUAPLONE = False
from plone.outputfilters.interfaces import IFilter
appendix_re = re.compile('^(.*)([\?#].*)$')
resolveuid_re = re.compile('^[./]*resolveuid/([^/]*)/?(.*)$')
# The SGMLParser works differently on Python 2.4 and later
# The attributes are passed escaped in the unknown_...-methods
# in 2.4 and raw in Python 2.6
# No need to escape in python 2.4
import sys
if sys.version_info[0] == 2 and sys.version_info[1] == 4:
escape = lambda s: s
else:
from cgi import escape
class IImageCaptioningEnabler(Interface):
available = Attribute(
"Boolean indicating whether image captioning should be performed.")
class IResolveUidsEnabler(Interface):
available = Attribute(
"Boolean indicating whether UID links should be resolved.")
singleton_tags = ["img", "area", "br", "hr", "input", "meta", "param", "col"]
def tag(img, **attributes):
if hasattr(aq_base(img), 'tag'):
return img.tag(**attributes)
class ResolveUIDAndCaptionFilter(SGMLParser):
""" Parser to convert UUID links and captioned images """
implements(IFilter)
def __init__(self, context=None, request=None):
SGMLParser.__init__(self)
self.current_status = None
self.context = context
self.request = request
self.pieces = []
self.in_link = False
# IFilter implementation
order = 800
@lazy_property
def captioned_image_template(self):
return self.context.restrictedTraverse('plone.outputfilters_captioned_image')
@lazy_property
def captioned_images(self):
for u in getAllUtilitiesRegisteredFor(IImageCaptioningEnabler):
if u.available:
return True
return False
@lazy_property
def resolve_uids(self):
for u in getAllUtilitiesRegisteredFor(IResolveUidsEnabler):
if u.available:
return True
return False
def is_enabled(self):
if self.context is None:
return False
else:
return True
def __call__(self, data):
self.feed(data)
self.close()
return self.getResult()
# SGMLParser implementation
def append_data(self, data, add_eol=0):
"""Append data unmodified to self.data, add_eol adds a newline
character"""
if add_eol:
data += '\n'
self.pieces.append(data)
def handle_charref(self, ref):
""" Handle characters, just add them again """
self.append_data("&#%s;" % ref)
def handle_entityref(self, ref):
""" Handle html entities, put them back as we get them """
self.append_data("&%s;" % ref)
def handle_data(self, text):
""" Add data unmodified """
self.append_data(text)
def handle_comment(self, text):
""" Handle comments unmodified """
self.append_data("<!--%s-->" % text)
def handle_pi(self, text):
""" Handle processing instructions unmodified"""
self.append_data("<?%s>" % text)
def handle_decl(self, text):
"""Handle declarations unmodified """
self.append_data("<!%s>" % text)
def lookup_uid(self, uid):
context = self.context
if HAS_LINGUAPLONE:
# If we have LinguaPlone installed, add support for language-aware
# references
uids = translated_references(context, context.Language(), uid)
if len(uids) > 0:
uid = uids[0]
return uuidToObject(uid)
def resolve_link(self, href):
obj = None
subpath = href
appendix = ''
# preserve querystring and/or appendix
match = appendix_re.match(href)
if match is not None:
subpath, appendix = match.groups()
if self.resolve_uids:
match = resolveuid_re.match(subpath)
if match is not None:
uid, _subpath = match.groups()
obj = self.lookup_uid(uid)
if obj is not None:
subpath = _subpath
return obj, subpath, appendix
def resolve_image(self, src):
description = ''
if urlsplit(src)[0]:
# We have a scheme
return None, None, src, description
base = self.context
subpath = src
appendix = ''
def traversal_stack(base, path):
if path.startswith('/'):
base = getSite()
path = path[1:]
obj = base
stack = [obj]
components = path.split('/')
while components:
child_id = unquote(components.pop(0))
try:
if hasattr(aq_base(obj), 'scale'):
if components:
child = obj.scale(child_id, components.pop())
else:
child = obj.field(child_id).get(obj.context)
else:
# Do not use restrictedTraverse here; the path to the
# image may lead over containers that lack the View
# permission for the current user!
# Also, if the image itself is not viewable, we rather
# show a broken image than hide it or raise
# unauthorized here (for the referring document).
child = obj.unrestrictedTraverse(child_id)
except ConflictError:
raise
except (AttributeError, KeyError, NotFound, ztkNotFound):
return
obj = child
stack.append(obj)
return stack
def traverse_path(base, path):
stack = traversal_stack(base, path)
if stack is None:
return
return stack[-1]
obj, subpath, appendix = self.resolve_link(src)
if obj is not None:
# resolved uid
fullimage = obj
image = traverse_path(fullimage, subpath)
elif '/@@' in subpath:
# split on view
pos = subpath.find('/@@')
fullimage = traverse_path(base, subpath[:pos])
if fullimage is None:
return None, None, src, description
image = traverse_path(fullimage, subpath[pos + 1:])
else:
stack = traversal_stack(base, subpath)
if stack is None:
return None, None, src, description
image = stack.pop()
# if it's a scale, find the full image by traversing one less
fullimage = image
stack.reverse()
for parent in stack:
if hasattr(aq_base(parent), 'tag'):
fullimage = parent
break
src = image.absolute_url() + appendix
description = aq_acquire(fullimage, 'Description')()
return image, fullimage, src, description
def handle_captioned_image(self, attributes, image, fullimage, caption):
"""Handle captioned image.
"""
klass = attributes['class']
del attributes['class']
del attributes['src']
view = fullimage.restrictedTraverse('@@images', None)
if view is not None:
original_width, original_height = view.getImageSize()
else:
original_width, original_height = fullimage.width, fullimage.height
if image is not fullimage:
# image is a scale object
tag = image.tag
width = image.width
else:
if hasattr(aq_base(image), 'tag'):
tag = image.tag
else:
tag = view.tag
width = original_width
options = {
'class': klass,
'originalwidth': attributes.get('width', None),
'originalalt': attributes.get('alt', None),
'url_path': fullimage.absolute_url_path(),
'caption': newline_to_br(html_quote(caption)),
'image': image,
'fullimage': fullimage,
'tag': tag(**attributes),
'isfullsize': image is fullimage or (
image.width == original_width and
image.height == original_height),
'width': attributes.get('width', width),
}
if self.in_link:
# Must preserve original link, don't overwrite
# with a link to the image
options['isfullsize'] = True
captioned_html = self.captioned_image_template(**options)
if isinstance(captioned_html, unicode):
captioned_html = captioned_html.encode('utf8')
self.append_data(captioned_html)
def unknown_starttag(self, tag, attrs):
"""Here we've got the actual conversion of links and images.
Convert UUID's to absolute URLs, and process captioned images to HTML.
"""
if tag in ['a', 'img', 'area']:
# Only do something if tag is a link, image, or image map area.
attributes = dict(attrs)
if tag == 'a':
self.in_link = True
if (tag == 'a' or tag == 'area') and 'href' in attributes:
href = attributes['href']
scheme = urlsplit(href)[0]
if not scheme and not href.startswith('/') and not href.startswith('mailto<'):
obj, subpath, appendix = self.resolve_link(href)
if obj is not None:
href = obj.absolute_url()
if subpath:
href += '/' + subpath
href += appendix
elif resolveuid_re.match(href) is None:
# absolutize relative URIs; this text isn't necessarily
# being rendered in the context where it was stored
relative_root = self.context
if not getattr(
self.context, 'isPrincipiaFolderish', False):
relative_root = aq_parent(self.context)
actual_url = relative_root.absolute_url()
href = urljoin(actual_url + '/', subpath) + appendix
attributes['href'] = href
attrs = attributes.iteritems()
elif tag == 'img':
src = attributes.get('src', '')
image, fullimage, src, description = self.resolve_image(src)
attributes["src"] = src
caption = description
# Check if the image needs to be captioned
if (self.captioned_images and image is not None and caption
and 'captioned' in attributes.get('class', '').split(' ')):
self.handle_captioned_image(attributes, image, fullimage,
caption)
return True
if fullimage is not None:
# Check to see if the alt / title tags need setting
title = aq_acquire(fullimage, 'Title')()
if 'alt' not in attributes:
attributes['alt'] = title
if 'title' not in attributes:
attributes['title'] = title
attrs = attributes.iteritems()
# Add the tag to the result
strattrs = "".join([' %s="%s"' % (key, escape(value)) for key, value in attrs])
if tag in singleton_tags:
self.append_data("<%s%s />" % (tag, strattrs))
else:
self.append_data("<%s%s>" % (tag, strattrs))
def unknown_endtag(self, tag):
"""Add the endtag unmodified"""
if tag == 'a':
self.in_link = False
self.append_data("</%s>" % tag)
def parse_declaration(self, i):
"""Fix handling of CDATA sections. Code borrowed from BeautifulSoup.
"""
j = None
if self.rawdata[i:i + 9] == '<![CDATA[':
k = self.rawdata.find(']]>', i)
if k == -1:
k = len(self.rawdata)
data = self.rawdata[i + 9:k]
j = k + 3
self.append_data("<![CDATA[%s]]>" % data)
else:
try:
j = SGMLParser.parse_declaration(self, i)
except SGMLParseError:
toHandle = self.rawdata[i:]
self.handle_data(toHandle)
j = i + len(toHandle)
return j
def getResult(self):
"""Return the parsed result and flush it"""
result = "".join(self.pieces)
self.pieces = None
return result