comparison MoinMoin/items/__init__.py @ 0:5568cf133caf moin20-repo-reboot

create moin/2.0 repo, drop all history (see notes below) Up to now, we used the moin/2.0-dev repository (which was cloned from another, older moin repo quite some time ago). Over the years, these repositories got rather fat (>200MB) and were a pain to clone over slow, high-latency or unreliable connections. After having finished most of the dirty work in moin2, having killed all the 3rd party code we had bundled with (is now installed by quickinstall / pip / setuptools), it is now a good time to get rid of the history (the history made up most of the repository's size). If you need to look at the history, look there: http://hg.moinmo.in/moin/2.0-dev The new moin/2.0 repository has the files as of this changesets: http://hg.moinmo.in/moin/2.0-dev/rev/075132a755dc The changeset hashes that link the repositories will be tagged (in both repositories) as "moin20-repo-reboot".
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sun, 20 Feb 2011 20:53:45 +0100
parents
children 41e2918dcafd
comparison
equal deleted inserted replaced
-1:000000000000 0:5568cf133caf
1 # -*- coding: iso-8859-1 -*-
2 """
3 MoinMoin - misc. mimetype items
4
5 While MoinMoin.storage cares for backend storage of items,
6 this module cares for more high-level, frontend items,
7 e.g. showing, editing, etc. of wiki items.
8
9 @copyright: 2009 MoinMoin:ThomasWaldmann,
10 2009 MoinMoin:ReimarBauer,
11 2009 MoinMoin:ChristopherDenter,
12 2009 MoinMoin:BastianBlank,
13 2010 MoinMoin:ValentinJaniaut,
14 2010 MoinMoin:DiogenesAugusto
15 @license: GNU GPL, see COPYING for details.
16 """
17
18 import os, re, time, datetime, shutil, base64
19 import tarfile
20 import zipfile
21 import tempfile
22 from StringIO import StringIO
23 from MoinMoin.security.textcha import TextCha, TextChaizedForm, TextChaValid
24 from MoinMoin.util.forms import make_generator
25
26 try:
27 import PIL
28 from PIL import Image as PILImage
29 from PIL.ImageChops import difference as PILdiff
30 except ImportError:
31 PIL = None
32
33 from MoinMoin import log
34 logging = log.getLogger(__name__)
35
36 try:
37 import json
38 except ImportError:
39 import simplejson as json
40
41 from flask import current_app as app
42 from flask import flaskg
43
44 from flask import request, url_for, Response, abort, escape
45 from werkzeug import is_resource_modified
46 from jinja2 import Markup
47
48 from MoinMoin.i18n import _, L_, N_
49 from MoinMoin.themes import render_template
50 from MoinMoin import wikiutil, config, user
51 from MoinMoin.util.send_file import send_file
52 from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError, AccessDeniedError, \
53 StorageError
54 from MoinMoin.storage import HASH_ALGORITHM
55
56 COLS = 80
57 ROWS_DATA = 20
58 ROWS_META = 10
59
60 UUID = "uuid"
61 NAME = "name"
62 NAME_OLD = "name_old"
63
64 # if an item is reverted, we store the revision number we used for reverting there:
65 REVERTED_TO = "reverted_to"
66
67 # some metadata key constants:
68 ACL = "acl"
69
70 # This says: I am a system item
71 IS_SYSITEM = "is_syspage"
72 # This says: original sysitem as contained in release: <release>
73 SYSITEM_VERSION = "syspage_version"
74
75 # keys for storing group and dict information
76 # group of user names, e.g. for ACLs:
77 USERGROUP = "usergroup"
78 # needs more precise name / use case:
79 SOMEDICT = "somedict"
80
81 MIMETYPE = "mimetype"
82 SIZE = "size"
83 LANGUAGE = "language"
84 ITEMLINKS = "itemlinks"
85 ITEMTRANSCLUSIONS = "itemtransclusions"
86 TAGS = "tags"
87
88 ACTION = "action"
89 ADDRESS = "address"
90 HOSTNAME = "hostname"
91 USERID = "userid"
92 EXTRA = "extra"
93 COMMENT = "comment"
94
95
96 class DummyRev(dict):
97 """ if we have no stored Revision, we use this dummy """
98 def __init__(self, item, mimetype):
99 self[MIMETYPE] = mimetype
100 self.item = item
101 self.timestamp = 0
102 self.revno = None
103 def read(self, size=-1):
104 return ''
105 def seek(self, offset, whence=0):
106 pass
107 def tell(self):
108 return 0
109
110
111 class DummyItem(object):
112 """ if we have no stored Item, we use this dummy """
113 def __init__(self, name):
114 self.name = name
115 def list_revisions(self):
116 return [] # same as an empty Item
117
118
119 class Item(object):
120 """ Highlevel (not storage) Item """
121 @classmethod
122 def create(cls, name=u'', mimetype=None, rev_no=None, item=None):
123 if rev_no is None:
124 rev_no = -1
125 if mimetype is None:
126 mimetype = 'application/x-nonexistent'
127
128 try:
129 if item is None:
130 item = flaskg.storage.get_item(name)
131 else:
132 name = item.name
133 except NoSuchItemError:
134 logging.debug("No such item: %r" % name)
135 item = DummyItem(name)
136 rev = DummyRev(item, mimetype)
137 logging.debug("Item %r, created dummy revision with mimetype %r" % (name, mimetype))
138 else:
139 logging.debug("Got item: %r" % name)
140 try:
141 rev = item.get_revision(rev_no)
142 except NoSuchRevisionError:
143 try:
144 rev = item.get_revision(-1) # fall back to current revision
145 # XXX add some message about invalid revision
146 except NoSuchRevisionError:
147 logging.debug("Item %r has no revisions." % name)
148 rev = DummyRev(item, mimetype)
149 logging.debug("Item %r, created dummy revision with mimetype %r" % (name, mimetype))
150 logging.debug("Got item %r, revision: %r" % (name, rev_no))
151 mimetype = rev.get(MIMETYPE) or mimetype # XXX: Why do we need ... or ... ?
152 logging.debug("Item %r, got mimetype %r from revision meta" % (name, mimetype))
153 logging.debug("Item %r, rev meta dict: %r" % (name, dict(rev)))
154
155 def _find_item_class(mimetype, BaseClass, best_match_len=-1):
156 #logging.debug("_find_item_class(%r,%r,%r)" % (mimetype, BaseClass, best_match_len))
157 Class = None
158 for ItemClass in BaseClass.__subclasses__():
159 for supported_mimetype in ItemClass.supported_mimetypes:
160 if mimetype.startswith(supported_mimetype):
161 match_len = len(supported_mimetype)
162 if match_len > best_match_len:
163 best_match_len = match_len
164 Class = ItemClass
165 #logging.debug("_find_item_class: new best match: %r by %r)" % (supported_mimetype, ItemClass))
166 best_match_len, better_Class = _find_item_class(mimetype, ItemClass, best_match_len)
167 if better_Class:
168 Class = better_Class
169 return best_match_len, Class
170
171 ItemClass = _find_item_class(mimetype, cls)[1]
172 logging.debug("ItemClass %r handles %r" % (ItemClass, mimetype))
173 return ItemClass(name=name, rev=rev, mimetype=mimetype)
174
175 def __init__(self, name, rev=None, mimetype=None):
176 self.name = name
177 self.rev = rev
178 self.mimetype = mimetype
179
180 def get_meta(self):
181 return self.rev or {}
182 meta = property(fget=get_meta)
183
184 def _render_meta(self):
185 # override this in child classes
186 return ''
187
188 def feed_input_conv(self):
189 return self.name
190
191 def internal_representation(self, converters=['smiley', 'link']):
192 """
193 Return the internal representation of a document using a DOM Tree
194 """
195 flaskg.clock.start('conv_in_dom')
196 hash_name = HASH_ALGORITHM
197 hash_hexdigest = self.rev.get(hash_name)
198 if hash_hexdigest:
199 cid = wikiutil.cache_key(usage="internal_representation",
200 hash_name=hash_name,
201 hash_hexdigest=hash_hexdigest)
202 doc = app.cache.get(cid)
203 else:
204 # likely a non-existing item
205 doc = cid = None
206 if doc is None:
207 # We will see if we can perform the conversion:
208 # FROM_mimetype --> DOM
209 # if so we perform the transformation, otherwise we don't
210 from MoinMoin.converter import default_registry as reg
211 from MoinMoin.util.iri import Iri
212 from MoinMoin.util.mime import Type, type_moin_document
213 from MoinMoin.util.tree import moin_page, xlink
214 input_conv = reg.get(Type(self.mimetype), type_moin_document)
215 if not input_conv:
216 raise TypeError("We cannot handle the conversion from %s to the DOM tree" % self.mimetype)
217 link_conv = reg.get(type_moin_document, type_moin_document,
218 links='extern', url_root=Iri(request.url_root))
219 smiley_conv = reg.get(type_moin_document, type_moin_document,
220 icon='smiley')
221
222 # We can process the conversion
223 links = Iri(scheme='wiki', authority='', path='/' + self.name)
224 input = self.feed_input_conv()
225 doc = input_conv(input)
226 # XXX is the following assuming that the top element of the doc tree
227 # is a moin_page.page element? if yes, this is the wrong place to do that
228 # as not every doc will have that element (e.g. for images, we just get
229 # moin_page.object, for a tar item, we get a moin_page.table):
230 doc.set(moin_page.page_href, unicode(links))
231 for conv in converters:
232 if conv == 'smiley':
233 doc = smiley_conv(doc)
234 elif conv == 'link':
235 doc = link_conv(doc)
236 if cid:
237 app.cache.set(cid, doc)
238 flaskg.clock.stop('conv_in_dom')
239 return doc
240
241 def _expand_document(self, doc):
242 from MoinMoin.converter import default_registry as reg
243 from MoinMoin.util.mime import type_moin_document
244 include_conv = reg.get(type_moin_document, type_moin_document, includes='expandall')
245 macro_conv = reg.get(type_moin_document, type_moin_document, macros='expandall')
246 flaskg.clock.start('conv_include')
247 doc = include_conv(doc)
248 flaskg.clock.stop('conv_include')
249 flaskg.clock.start('conv_macro')
250 doc = macro_conv(doc)
251 flaskg.clock.stop('conv_macro')
252 return doc
253
254 def _render_data(self):
255 from MoinMoin.converter import default_registry as reg
256 from MoinMoin.util.mime import Type, type_moin_document
257 from MoinMoin.util.tree import html
258 include_conv = reg.get(type_moin_document, type_moin_document, includes='expandall')
259 macro_conv = reg.get(type_moin_document, type_moin_document, macros='expandall')
260 # TODO: Real output format
261 html_conv = reg.get(type_moin_document, Type('application/x-xhtml-moin-page'))
262 doc = self.internal_representation()
263 doc = self._expand_document(doc)
264 flaskg.clock.start('conv_dom_html')
265 doc = html_conv(doc)
266 flaskg.clock.stop('conv_dom_html')
267
268 from array import array
269 out = array('u')
270 flaskg.clock.start('conv_serialize')
271 doc.write(out.fromunicode, namespaces={html.namespace: ''}, method='xml')
272 out = out.tounicode()
273 flaskg.clock.stop('conv_serialize')
274 return out
275
276 def _render_data_xml(self, converters):
277 from MoinMoin.util.tree import moin_page, xlink, html
278 doc = self.internal_representation(converters)
279
280 from array import array
281 out = array('u')
282 doc.write(out.fromunicode,
283 namespaces={moin_page.namespace: '',
284 xlink.namespace: 'xlink',
285 html.namespace: 'html',
286 },
287 method='xml')
288 return out.tounicode()
289
290 def _do_modify_show_templates(self):
291 # call this if the item is still empty
292 rev_nos = []
293 item_templates = self.get_templates(self.mimetype)
294 return render_template('modify_show_template_selection.html',
295 item_name=self.name,
296 rev=self.rev,
297 mimetype=self.mimetype,
298 templates=item_templates,
299 first_rev_no=rev_nos and rev_nos[0],
300 last_rev_no=rev_nos and rev_nos[-1],
301 meta_rendered='',
302 data_rendered='',
303 )
304
305 def meta_filter(self, meta):
306 """ kill metadata entries that we set automatically when saving """
307 kill_keys = [# shall not get copied from old rev to new rev
308 SYSITEM_VERSION,
309 NAME_OLD,
310 # are automatically implanted when saving
311 NAME,
312 HASH_ALGORITHM,
313 COMMENT,
314 ACTION,
315 ADDRESS, HOSTNAME, USERID,
316 ]
317 for key in kill_keys:
318 meta.pop(key, None)
319 return meta
320
321 def meta_text_to_dict(self, text):
322 """ convert meta data from a text fragment to a dict """
323 meta = json.loads(text)
324 return self.meta_filter(meta)
325
326 def meta_dict_to_text(self, meta, use_filter=True):
327 """ convert meta data from a dict to a text fragment """
328 meta = dict(meta)
329 if use_filter:
330 meta = self.meta_filter(meta)
331 return json.dumps(meta, sort_keys=True, indent=2, ensure_ascii=False)
332
333 def get_data(self):
334 return '' # TODO create a better method for binary stuff
335 data = property(fget=get_data)
336
337 def _write_stream(self, content, new_rev, bufsize=8192):
338 if hasattr(content, "read"):
339 while True:
340 buf = content.read(bufsize)
341 if not buf:
342 break
343 new_rev.write(buf)
344 elif isinstance(content, str):
345 new_rev.write(content)
346 else:
347 raise StorageError("unsupported content object: %r" % content)
348
349 def copy(self, name, comment=u''):
350 """
351 copy this item to item <name>
352 """
353 old_item = self.rev.item
354 flaskg.storage.copy_item(old_item, name=name)
355 current_rev = old_item.get_revision(-1)
356 # we just create a new revision with almost same meta/data to show up on RC
357 self._save(current_rev, current_rev, name=name, action=u'COPY', comment=comment)
358
359 def _rename(self, name, comment, action):
360 self.rev.item.rename(name)
361 self._save(self.meta, self.data, name=name, action=action, comment=comment)
362
363 def rename(self, name, comment=u''):
364 """
365 rename this item to item <name>
366 """
367 return self._rename(name, comment, action=u'RENAME')
368
369 def delete(self, comment=u''):
370 """
371 delete this item by moving it to the trashbin
372 """
373 trash_prefix = u'Trash/' # XXX move to config
374 now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime())
375 # make trash name unique by including timestamp:
376 trashname = u'%s%s (%s UTC)' % (trash_prefix, self.name, now)
377 return self._rename(trashname, comment, action=u'TRASH')
378
379 def revert(self):
380 # called from revert UI/POST
381 comment = request.form.get('comment')
382 self._save(self.meta, self.data, action=u'REVERT', comment=comment)
383
384 def destroy(self, comment=u'', destroy_item=False):
385 # called from destroy UI/POST
386 if destroy_item:
387 # destroy complete item with all revisions, metadata, etc.
388 self.rev.item.destroy()
389 else:
390 # just destroy this revision
391 self.rev.destroy()
392
393 def modify(self):
394 # called from modify UI/POST
395 data_file = request.files.get('data_file')
396 mimetype = request.values.get('mimetype', 'text/plain')
397 if data_file and data_file.filename:
398 # user selected a file to upload
399 data = data_file.stream
400 mimetype = wikiutil.MimeType(filename=data_file.filename).mime_type()
401 else:
402 # take text from textarea
403 data = request.form.get('data_text', '')
404 if data:
405 data = self.data_form_to_internal(data)
406 data = self.data_internal_to_storage(data)
407 mimetype = 'text/plain'
408 else:
409 data = '' # could've been u'' also!
410 mimetype = None
411 meta_text = request.form.get('meta_text', '')
412 meta = self.meta_text_to_dict(meta_text)
413 comment = request.form.get('comment')
414 self._save(meta, data, mimetype=mimetype, comment=comment)
415
416 def _save(self, meta, data, name=None, action=u'SAVE', mimetype=None, comment=u''):
417 if name is None:
418 name = self.name
419 backend = flaskg.storage
420 try:
421 storage_item = backend.get_item(name)
422 except NoSuchItemError:
423 storage_item = backend.create_item(name)
424 try:
425 currentrev = storage_item.get_revision(-1)
426 rev_no = currentrev.revno
427 if mimetype is None:
428 # if we didn't get mimetype info, thus reusing the one from current rev:
429 mimetype = currentrev.get(MIMETYPE)
430 except NoSuchRevisionError:
431 rev_no = -1
432 newrev = storage_item.create_revision(rev_no + 1)
433 for k, v in meta.iteritems():
434 # TODO Put metadata into newrev here for now. There should be a safer way
435 # of input for this.
436 newrev[k] = v
437
438 # we store the previous (if different) and current item name into revision metadata
439 # this is useful for rename history and backends that use item uids internally
440 oldname = meta.get(NAME)
441 if oldname and oldname != name:
442 newrev[NAME_OLD] = oldname
443 newrev[NAME] = name
444
445 self._write_stream(data, newrev)
446 timestamp = time.time()
447 # XXX if meta is from old revision, and user did not give a non-empty
448 # XXX comment, re-using the old rev's comment is wrong behaviour:
449 comment = unicode(comment or meta.get(COMMENT, ''))
450 if comment:
451 newrev[COMMENT] = comment
452 # allow override by form- / qs-given mimetype:
453 mimetype = request.values.get('mimetype', mimetype)
454 # allow override by give metadata:
455 assert mimetype is not None
456 newrev[MIMETYPE] = unicode(meta.get(MIMETYPE, mimetype))
457 newrev[ACTION] = unicode(action)
458 self.before_revision_commit(newrev, data)
459 storage_item.commit()
460 # XXX Event ?
461
462 def before_revision_commit(self, newrev, data):
463 """
464 hook that can be used to add more meta data to a revision before
465 it is committed.
466
467 @param newrev: new (still uncommitted) revision - modify as wanted
468 @param data: either str or open file (we can avoid having to read/seek
469 rev's data with this)
470 """
471 remote_addr = request.remote_addr
472 if remote_addr:
473 newrev[ADDRESS] = unicode(remote_addr)
474 newrev[HOSTNAME] = unicode(wikiutil.get_hostname(remote_addr))
475 if flaskg.user.valid:
476 newrev[USERID] = unicode(flaskg.user.id)
477
478 def search_items(self, term=None):
479 """ search items matching the term or,
480 if term is None, return all items
481 """
482 if term:
483 backend_items = flaskg.storage.search_items(term)
484 else:
485 # special case: we just want all items
486 backend_items = flaskg.storage.iteritems()
487 for item in backend_items:
488 yield Item.create(item=item)
489
490 list_items = search_items # just for cosmetics
491
492 def count_items(self, term=None):
493 """
494 Return item count for matching items. See search_items() for details.
495 """
496 count = 0
497 # we intentionally use a loop to avoid creating a list with all item objects:
498 for item in self.list_items(term):
499 count += 1
500 return count
501
502 def get_index(self):
503 """ create an index of sub items of this item """
504 import re
505 from MoinMoin.search.term import NameRE
506
507 if self.name:
508 prefix = self.name + u'/'
509 else:
510 # trick: an item of empty name can be considered as "virtual root item",
511 # that has all wiki items as sub items
512 prefix = u''
513 sub_item_re = u"^%s.*" % re.escape(prefix)
514 regex = re.compile(sub_item_re, re.UNICODE)
515
516 item_iterator = self.search_items(NameRE(regex))
517
518 # We only want the sub-item part of the item names, not the whole item objects.
519 prefix_len = len(prefix)
520 items = [(item.name, item.name[prefix_len:], item.meta.get(MIMETYPE))
521 for item in item_iterator]
522 return sorted(items)
523
524 def flat_index(self):
525 index = self.get_index()
526 index = [(fullname, relname, mimetype)
527 for fullname, relname, mimetype in index
528 if u'/' not in relname]
529 return index
530
531 index_template = 'index.html'
532
533
534 class NonExistent(Item):
535 supported_mimetypes = ['application/x-nonexistent']
536 mimetype_groups = [
537 ('markup text items', [
538 ('text/x.moin.wiki', 'Wiki (MoinMoin)'),
539 ('text/x.moin.creole', 'Wiki (Creole)'),
540 ('text/x-mediawiki', 'Wiki (MediaWiki)'),
541 ('text/x-rst', 'ReST'),
542 ('application/docbook+xml', 'DocBook'),
543 ('text/html', 'HTML'),
544 ]),
545 ('other text items', [
546 ('text/plain', 'plain text'),
547 ('text/x-diff', 'diff/patch'),
548 ('text/x-python', 'python code'),
549 ('text/csv', 'csv'),
550 ('text/x-irclog', 'IRC log'),
551 ]),
552 ('image items', [
553 ('image/jpeg', 'JPEG'),
554 ('image/png', 'PNG'),
555 ('image/svg+xml', 'SVG'),
556 ]),
557 ('audio items', [
558 ('audio/wave', 'WAV'),
559 ('audio/ogg', 'OGG'),
560 ('audio/mpeg', 'MP3'),
561 ('audio/webm', 'WebM'),
562 ]),
563 ('video items', [
564 ('video/ogg', 'OGG'),
565 ('video/webm', 'WebM'),
566 ('video/mp4', 'MP4'),
567 ]),
568 ('drawing items', [
569 ('application/x-twikidraw', 'TDRAW'),
570 ('application/x-anywikidraw', 'ADRAW'),
571 ('application/x-svgdraw', 'SVGDRAW'),
572 ]),
573
574 ('other items', [
575 ('application/pdf', 'PDF'),
576 ('application/zip', 'ZIP'),
577 ('application/x-tar', 'TAR'),
578 ('application/x-gtar', 'TGZ'),
579 ('application/octet-stream', 'binary file'),
580 ]),
581 ]
582
583 def do_get(self):
584 abort(404)
585
586 def _convert(self):
587 abort(404)
588
589 def do_modify(self, template_name):
590 # XXX think about and add item template support
591 return render_template('modify_show_type_selection.html',
592 item_name=self.name,
593 mimetype_groups=self.mimetype_groups,
594 )
595
596
597 class Binary(Item):
598 """ An arbitrary binary item, fallback class for every item mimetype. """
599 supported_mimetypes = [''] # fallback, because every mimetype starts with ''
600
601 modify_help = """\
602 There is no help, you're doomed!
603 """
604
605 template = "modify_binary.html"
606
607 # XXX reads item rev data into memory!
608 def get_data(self):
609 if self.rev is not None:
610 return self.rev.read()
611 else:
612 return ''
613 data = property(fget=get_data)
614
615 def _render_meta(self):
616 return "<pre>%s</pre>" % self.meta_dict_to_text(self.meta, use_filter=False)
617
618 def get_templates(self, mimetype=None):
619 """ create a list of templates (for some specific mimetype) """
620 from MoinMoin.search.term import AND, LastRevisionMetaDataMatch
621 term = LastRevisionMetaDataMatch(TAGS, ['template']) # XXX there might be other tags
622 if mimetype:
623 term = AND(term, LastRevisionMetaDataMatch(MIMETYPE, mimetype))
624 item_iterator = self.search_items(term)
625 items = [item.name for item in item_iterator]
626 return sorted(items)
627
628 def do_modify(self, template_name):
629 # XXX think about and add item template support
630 #if template_name is None and isinstance(self.rev, DummyRev):
631 # return self._do_modify_show_templates()
632 form = TextChaizedForm.from_defaults()
633 TextCha(form).amend_form()
634 return render_template(self.template,
635 item_name=self.name,
636 rows_meta=ROWS_META, cols=COLS,
637 revno=0,
638 meta_text=self.meta_dict_to_text(self.meta),
639 help=self.modify_help,
640 form=form,
641 gen=make_generator(),
642 )
643
644 copy_template = 'copy.html'
645 delete_template = 'delete.html'
646 destroy_template = 'destroy.html'
647 diff_template = 'diff.html'
648 rename_template = 'rename.html'
649 revert_template = 'revert.html'
650
651 def _render_data_diff(self, oldrev, newrev):
652 hash_name = HASH_ALGORITHM
653 if oldrev[hash_name] == newrev[hash_name]:
654 return _("The items have the same data hash code (that means they very likely have the same data).")
655 else:
656 return _("The items have different data.")
657
658 _render_data_diff_text = _render_data_diff
659 _render_data_diff_raw = _render_data_diff
660
661 def _convert(self):
662 return _("Impossible to convert the data to the mimetype: %(mimetype)s",
663 mimetype=request.values.get('mimetype'))
664
665 def do_get(self):
666 hash = self.rev.get(HASH_ALGORITHM)
667 if is_resource_modified(request.environ, hash): # use hash as etag
668 return self._do_get_modified(hash)
669 else:
670 return Response(status=304)
671
672 def _do_get_modified(self, hash):
673 member = request.values.get('member')
674 return self._do_get(hash, member)
675
676 def _do_get(self, hash, member=None):
677 filename = None
678 if member: # content = file contained within a archive item revision
679 path, filename = os.path.split(member)
680 mt = wikiutil.MimeType(filename=filename)
681 content_disposition = mt.content_disposition(app.cfg)
682 content_type = mt.content_type()
683 content_length = None
684 file_to_send = self.get_member(member)
685 else: # content = item revision
686 rev = self.rev
687 try:
688 mimestr = rev[MIMETYPE]
689 except KeyError:
690 mimestr = mimetypes.guess_type(rev.item.name)[0]
691 mt = wikiutil.MimeType(mimestr=mimestr)
692 content_disposition = mt.content_disposition(app.cfg)
693 content_type = mt.content_type()
694 content_length = rev.size
695 file_to_send = rev
696
697 # TODO: handle content_disposition is not None
698 # Important: empty filename keeps flask from trying to autodetect filename,
699 # as this would not work for us, because our file's are not necessarily fs files.
700 return send_file(file=file_to_send,
701 mimetype=content_type,
702 as_attachment=False, attachment_filename=filename,
703 cache_timeout=10, # wiki data can change rapidly
704 add_etags=True, etag=hash, conditional=True)
705
706
707 class RenderableBinary(Binary):
708 """ This is a base class for some binary stuff that renders with a object tag. """
709 supported_mimetypes = []
710
711
712 class Application(Binary):
713 supported_mimetypes = []
714
715
716 class TarMixin(object):
717 """
718 TarMixin offers additional functionality for tar-like items to list and
719 access member files and to create new revisions by multiple posts.
720 """
721 def list_members(self):
722 """
723 list tar file contents (member file names)
724 """
725 self.rev.seek(0)
726 tf = tarfile.open(fileobj=self.rev, mode='r')
727 return tf.getnames()
728
729 def get_member(self, name):
730 """
731 return a file-like object with the member file data
732
733 @param name: name of the data in the container file
734 """
735 self.rev.seek(0)
736 tf = tarfile.open(fileobj=self.rev, mode='r')
737 return tf.extractfile(name)
738
739 def put_member(self, name, content, content_length, expected_members):
740 """
741 puts a new member file into a temporary tar container.
742 If all expected members have been put, it saves the tar container
743 to a new item revision.
744
745 @param name: name of the data in the container file
746 @param content: the data to store into the tar file (str or file-like)
747 @param content_length: byte-length of content (for str, None can be given)
748 @param expected_members: set of expected member file names
749 """
750 if not name in expected_members:
751 raise StorageError("tried to add unexpected member %r to container item %r" % (name, self.name))
752 if isinstance(name, unicode):
753 name = name.encode('utf-8')
754 temp_fname = os.path.join(tempfile.gettempdir(), 'TarContainer_' +
755 wikiutil.cache_key(usage='TarContainer', name=self.name))
756 tf = tarfile.TarFile(temp_fname, mode='a')
757 ti = tarfile.TarInfo(name)
758 if isinstance(content, str):
759 if content_length is None:
760 content_length = len(content)
761 content = StringIO(content) # we need a file obj
762 elif not hasattr(content, 'read'):
763 logging.error("unsupported content object: %r" % content)
764 raise StorageError("unsupported content object: %r" % content)
765 assert content_length >= 0 # we don't want -1 interpreted as 4G-1
766 ti.size = content_length
767 tf.addfile(ti, content)
768 tf_members = set(tf.getnames())
769 tf.close()
770 if tf_members - expected_members:
771 msg = "found unexpected members in container item %r" % self.name
772 logging.error(msg)
773 os.remove(temp_fname)
774 raise StorageError(msg)
775 if tf_members == expected_members:
776 # everything we expected has been added to the tar file, save the container as revision
777 meta = {"mimetype": self.mimetype}
778 data = open(temp_fname, 'rb')
779 self._save(meta, data, name=self.name, action=u'SAVE', mimetype=self.mimetype, comment='')
780 data.close()
781 os.remove(temp_fname)
782
783
784 class ApplicationXTar(TarMixin, Application):
785 supported_mimetypes = ['application/x-tar', 'application/x-gtar']
786
787 def feed_input_conv(self):
788 return self.rev
789
790
791 class ZipMixin(object):
792 """
793 ZipMixin offers additional functionality for zip-like items to list and
794 access member files.
795 """
796 def list_members(self):
797 """
798 list zip file contents (member file names)
799 """
800 self.rev.seek(0)
801 zf = zipfile.ZipFile(self.rev, mode='r')
802 return zf.namelist()
803
804 def get_member(self, name):
805 """
806 return a file-like object with the member file data
807
808 @param name: name of the data in the zip file
809 """
810 self.rev.seek(0)
811 zf = zipfile.ZipFile(self.rev, mode='r')
812 return zf.open(name, mode='r')
813
814 def put_member(self, name, content, content_length, expected_members):
815 raise NotImplementedError
816
817
818 class ApplicationZip(ZipMixin, Application):
819 supported_mimetypes = ['application/zip']
820
821 def feed_input_conv(self):
822 return self.rev
823
824
825 class PDF(Application):
826 supported_mimetypes = ['application/pdf', ]
827
828
829 class Video(Binary):
830 supported_mimetypes = ['video/', ]
831
832
833 class Audio(Binary):
834 supported_mimetypes = ['audio/', ]
835
836
837 class Image(Binary):
838 """ Any Image mimetype """
839 supported_mimetypes = ['image/', ]
840
841
842 class RenderableImage(RenderableBinary):
843 """ Any Image mimetype """
844 supported_mimetypes = []
845
846
847 class SvgImage(RenderableImage):
848 """ SVG images use <object> tag mechanism from RenderableBinary base class """
849 supported_mimetypes = ['image/svg+xml']
850
851
852 class RenderableBitmapImage(RenderableImage):
853 """ PNG/JPEG/GIF images use <img> tag (better browser support than <object>) """
854 supported_mimetypes = [] # if mimetype is also transformable, please list
855 # in TransformableImage ONLY!
856
857
858 class TransformableBitmapImage(RenderableBitmapImage):
859 """ We can transform (resize, rotate, mirror) some image types """
860 supported_mimetypes = ['image/png', 'image/jpeg', 'image/gif', ]
861
862 def _transform(self, content_type, size=None, transpose_op=None):
863 """ resize to new size (optional), transpose according to exif infos,
864 result data should be content_type.
865 """
866 try:
867 from PIL import Image as PILImage
868 except ImportError:
869 # no PIL, we can't do anything, we just output the revision data as is
870 return content_type, self.rev.read()
871
872 if content_type == 'image/jpeg':
873 output_type = 'JPEG'
874 elif content_type == 'image/png':
875 output_type = 'PNG'
876 elif content_type == 'image/gif':
877 output_type = 'GIF'
878 else:
879 raise ValueError("content_type %r not supported" % content_type)
880
881 # revision obj has read() seek() tell(), thus this works:
882 image = PILImage.open(self.rev)
883 image.load()
884
885 try:
886 # if we have EXIF data, we can transpose (e.g. rotate left),
887 # so the rendered image is correctly oriented:
888 transpose_op = transpose_op or 1 # or self.exif['Orientation']
889 except KeyError:
890 transpose_op = 1 # no change
891
892 if size is not None:
893 image = image.copy() # create copy first as thumbnail works in-place
894 image.thumbnail(size, PILImage.ANTIALIAS)
895
896 transpose_func = {
897 1: lambda image: image,
898 2: lambda image: image.transpose(PILImage.FLIP_LEFT_RIGHT),
899 3: lambda image: image.transpose(PILImage.ROTATE_180),
900 4: lambda image: image.transpose(PILImage.FLIP_TOP_BOTTOM),
901 5: lambda image: image.transpose(PILImage.ROTATE_90).transpose(PILImage.FLIP_TOP_BOTTOM),
902 6: lambda image: image.transpose(PILImage.ROTATE_270),
903 7: lambda image: image.transpose(PILImage.ROTATE_90).transpose(PILImage.FLIP_LEFT_RIGHT),
904 8: lambda image: image.transpose(PILImage.ROTATE_90),
905 }
906 image = transpose_func[transpose_op](image)
907
908 outfile = StringIO()
909 image.save(outfile, output_type)
910 data = outfile.getvalue()
911 outfile.close()
912 return content_type, data
913
914 def _do_get_modified(self, hash):
915 try:
916 width = int(request.values.get('w'))
917 except (TypeError, ValueError):
918 width = None
919 try:
920 height = int(request.values.get('h'))
921 except (TypeError, ValueError):
922 height = None
923 try:
924 transpose = int(request.values.get('t'))
925 assert 1 <= transpose <= 8
926 except (TypeError, ValueError, AssertionError):
927 transpose = 1
928 if width or height or transpose != 1:
929 # resize requested, XXX check ACL behaviour! XXX
930 hash_name = HASH_ALGORITHM
931 hash_hexdigest = self.rev[hash_name]
932 cid = wikiutil.cache_key(usage="ImageTransform",
933 hash_name=hash_name,
934 hash_hexdigest=hash_hexdigest,
935 width=width, height=height, transpose=transpose)
936 c = app.cache.get(cid)
937 if c is None:
938 content_type = self.rev[MIMETYPE]
939 size = (width or 99999, height or 99999)
940 content_type, data = self._transform(content_type, size=size, transpose_op=transpose)
941 headers = wikiutil.file_headers(content_type=content_type, content_length=len(data))
942 app.cache.set(cid, (headers, data))
943 else:
944 # XXX TODO check ACL behaviour
945 headers, data = c
946 return Response(data, headers=headers)
947 else:
948 return self._do_get(hash)
949
950 def _render_data_diff(self, oldrev, newrev):
951 if PIL is None:
952 # no PIL, we can't do anything, we just call the base class method
953 return super(TransformableBitmapImage, self)._render_data_diff(oldrev, newrev)
954 url = url_for('frontend.diffraw', item_name=self.name, rev1=oldrev.revno, rev2=newrev.revno)
955 return Markup('<img src="%s" />' % escape(url))
956
957 def _render_data_diff_raw(self, oldrev, newrev):
958 hash_name = HASH_ALGORITHM
959 cid = wikiutil.cache_key(usage="ImageDiff",
960 hash_name=hash_name,
961 hash_old=oldrev[hash_name],
962 hash_new=newrev[hash_name])
963 c = app.cache.get(cid)
964 if c is None:
965 if PIL is None:
966 abort(404)
967
968 content_type = newrev[MIMETYPE]
969 if content_type == 'image/jpeg':
970 output_type = 'JPEG'
971 elif content_type == 'image/png':
972 output_type = 'PNG'
973 elif content_type == 'image/gif':
974 output_type = 'GIF'
975 else:
976 raise ValueError("content_type %r not supported" % content_type)
977
978 oldimage = PILImage.open(oldrev)
979 newimage = PILImage.open(newrev)
980 oldimage.load()
981 newimage.load()
982 diffimage = PILdiff(newimage, oldimage)
983 outfile = StringIO()
984 diffimage.save(outfile, output_type)
985 data = outfile.getvalue()
986 outfile.close()
987 headers = wikiutil.file_headers(content_type=content_type, content_length=len(data))
988 app.cache.set(cid, (headers, data))
989 else:
990 # XXX TODO check ACL behaviour
991 headers, data = c
992 return Response(data, headers=headers)
993
994 def _render_data_diff_text(self, oldrev, newrev):
995 return super(TransformableBitmapImage, self)._render_data_diff_text(oldrev, newrev)
996
997
998 class Text(Binary):
999 """ Any kind of text """
1000 supported_mimetypes = ['text/']
1001
1002 template = "modify_text.html"
1003
1004 # text/plain mandates crlf - but in memory, we want lf only
1005 def data_internal_to_form(self, text):
1006 """ convert data from memory format to form format """
1007 return text.replace(u'\n', u'\r\n')
1008
1009 def data_form_to_internal(self, data):
1010 """ convert data from form format to memory format """
1011 return data.replace(u'\r\n', u'\n')
1012
1013 def data_internal_to_storage(self, text):
1014 """ convert data from memory format to storage format """
1015 return text.replace(u'\n', u'\r\n').encode(config.charset)
1016
1017 def data_storage_to_internal(self, data):
1018 """ convert data from storage format to memory format """
1019 return data.decode(config.charset).replace(u'\r\n', u'\n')
1020
1021 def feed_input_conv(self):
1022 return self.data_storage_to_internal(self.data).split(u'\n')
1023
1024 def _render_data_diff(self, oldrev, newrev):
1025 from MoinMoin.util.diff_html import diff
1026 old_text = self.data_storage_to_internal(oldrev.read())
1027 new_text = self.data_storage_to_internal(newrev.read())
1028 storage_item = flaskg.storage.get_item(self.name)
1029 revs = storage_item.list_revisions()
1030 diffs = [(d[0], Markup(d[1]), d[2], Markup(d[3])) for d in diff(old_text, new_text)]
1031 return Markup(render_template('diff_text.html',
1032 item_name=self.name,
1033 oldrev=oldrev,
1034 newrev=newrev,
1035 min_revno=revs[0],
1036 max_revno=revs[-1],
1037 diffs=diffs,
1038 ))
1039
1040 def _render_data_diff_text(self, oldrev, newrev):
1041 from MoinMoin.util import diff_text
1042 oldlines = self.data_storage_to_internal(oldrev.read()).split('\n')
1043 newlines = self.data_storage_to_internal(newrev.read()).split('\n')
1044 difflines = diff_text.diff(oldlines, newlines)
1045 return '\n'.join(difflines)
1046
1047 def do_modify(self, template_name):
1048 form = TextChaizedForm.from_defaults()
1049 TextCha(form).amend_form()
1050 if template_name is None and isinstance(self.rev, DummyRev):
1051 return self._do_modify_show_templates()
1052 if template_name:
1053 item = Item.create(template_name)
1054 data_text = self.data_storage_to_internal(item.data)
1055 else:
1056 data_text = self.data_storage_to_internal(self.data)
1057 meta_text = self.meta_dict_to_text(self.meta)
1058 return render_template(self.template,
1059 item_name=self.name,
1060 rows_data=ROWS_DATA, rows_meta=ROWS_META, cols=COLS,
1061 revno=0,
1062 data_text=data_text,
1063 meta_text=meta_text,
1064 lang='en', direction='ltr',
1065 help=self.modify_help,
1066 form=form,
1067 gen=make_generator(),
1068 )
1069
1070
1071 class MarkupItem(Text):
1072 """
1073 some kind of item with markup
1074 (internal links and transcluded items)
1075 """
1076 def before_revision_commit(self, newrev, data):
1077 """
1078 add ITEMLINKS and ITEMTRANSCLUSIONS metadata
1079 """
1080 super(MarkupItem, self).before_revision_commit(newrev, data)
1081
1082 from MoinMoin.converter import default_registry as reg
1083 from MoinMoin.util.iri import Iri
1084 from MoinMoin.util.mime import Type, type_moin_document
1085 from MoinMoin.util.tree import moin_page
1086
1087 input_conv = reg.get(Type(self.mimetype), type_moin_document)
1088 item_conv = reg.get(type_moin_document, type_moin_document,
1089 items='refs', url_root=Iri(request.url_root))
1090
1091 i = Iri(scheme='wiki', authority='', path='/' + self.name)
1092
1093 doc = input_conv(self.data_storage_to_internal(data).split(u'\n'))
1094 doc.set(moin_page.page_href, unicode(i))
1095 doc = item_conv(doc)
1096
1097 newrev[ITEMLINKS] = item_conv.get_links()
1098 newrev[ITEMTRANSCLUSIONS] = item_conv.get_transclusions()
1099
1100 class MoinWiki(MarkupItem):
1101 """ MoinMoin wiki markup """
1102 supported_mimetypes = ['text/x.moin.wiki']
1103
1104
1105 class CreoleWiki(MarkupItem):
1106 """ Creole wiki markup """
1107 supported_mimetypes = ['text/x.moin.creole']
1108
1109
1110 class MediaWiki(MarkupItem):
1111 """ MediaWiki markup """
1112 supported_mimetypes = ['text/x-mediawiki']
1113
1114
1115 class ReST(MarkupItem):
1116 """ ReStructured Text markup """
1117 supported_mimetypes = ['text/x-rst']
1118
1119
1120 class HTML(Text):
1121 """
1122 HTML markup
1123
1124 Note: As we use html_in converter to convert this to DOM and later some
1125 output converterter to produce output format (e.g. html_out for html
1126 output), all(?) unsafe stuff will get lost.
1127
1128 Note: If raw revision data is accessed, unsafe stuff might be present!
1129 """
1130 supported_mimetypes = ['text/html']
1131
1132 template = "modify_text_html.html"
1133
1134 def do_modify(self, template_name):
1135 form = TextChaizedForm.from_defaults()
1136 TextCha(form).amend_form()
1137 if template_name is None and isinstance(self.rev, DummyRev):
1138 return self._do_modify_show_templates()
1139 if template_name:
1140 item = Item.create(template_name)
1141 data_text = self.data_storage_to_internal(item.data)
1142 else:
1143 data_text = self.data_storage_to_internal(self.data)
1144 meta_text = self.meta_dict_to_text(self.meta)
1145 return render_template(self.template,
1146 item_name=self.name,
1147 rows_data=ROWS_DATA, rows_meta=ROWS_META, cols=COLS,
1148 revno=0,
1149 data_text=data_text,
1150 meta_text=meta_text,
1151 lang='en', direction='ltr',
1152 help=self.modify_help,
1153 form=form,
1154 gen=make_generator(),
1155 )
1156
1157
1158 class DocBook(MarkupItem):
1159 """ DocBook Document """
1160 supported_mimetypes = ['application/docbook+xml']
1161
1162 def _convert(self, doc):
1163 from emeraldtree import ElementTree as ET
1164 from MoinMoin.converter import default_registry as reg
1165 from MoinMoin.util.mime import Type, type_moin_document
1166 from MoinMoin.util.tree import docbook, xlink
1167
1168 doc = self._expand_document(doc)
1169
1170 # We convert the internal representation of the document
1171 # into a DocBook document
1172 conv = reg.get(type_moin_document, Type('application/docbook+xml'))
1173
1174 doc = conv(doc)
1175
1176 # We determine the different namespaces of the output form
1177 output_namespaces = {
1178 docbook.namespace: '',
1179 xlink.namespace: 'xlink',
1180 }
1181
1182 # We convert the result into a StringIO object
1183 # With the appropriate namespace
1184 # TODO: Some other operation should probably be done here too
1185 # like adding a doctype
1186 file_to_send = StringIO()
1187 tree = ET.ElementTree(doc)
1188 tree.write(file_to_send, namespaces=output_namespaces)
1189
1190 # We determine the different parameters for the reply
1191 mt = wikiutil.MimeType(mimestr='application/docbook+xml')
1192 content_disposition = mt.content_disposition(app.cfg)
1193 content_type = mt.content_type()
1194 # After creation of the StringIO, we are at the end of the file
1195 # so position is the size the file.
1196 # and then we should move it back at the beginning of the file
1197 content_length = file_to_send.tell()
1198 file_to_send.seek(0)
1199 # Important: empty filename keeps flask from trying to autodetect filename,
1200 # as this would not work for us, because our file's are not necessarily fs files.
1201 return send_file(file=file_to_send,
1202 mimetype=content_type,
1203 as_attachment=False, attachment_filename=None,
1204 cache_timeout=10, # wiki data can change rapidly
1205 add_etags=False, etag=None, conditional=True)
1206
1207
1208 class TWikiDraw(TarMixin, Image):
1209 """
1210 drawings by TWikiDraw applet. It creates three files which are stored as tar file.
1211 """
1212 supported_mimetypes = ["application/x-twikidraw"]
1213 modify_help = ""
1214 template = "modify_twikidraw.html"
1215
1216 def modify(self):
1217 # called from modify UI/POST
1218 file_upload = request.files.get('filepath')
1219 filename = request.form['filename']
1220 basepath, basename = os.path.split(filename)
1221 basename, ext = os.path.splitext(basename)
1222
1223 filecontent = file_upload.stream
1224 content_length = None
1225 if ext == '.draw': # TWikiDraw POSTs this first
1226 filecontent = filecontent.read() # read file completely into memory
1227 filecontent = filecontent.replace("\r", "")
1228 elif ext == '.map':
1229 filecontent = filecontent.read() # read file completely into memory
1230 filecontent = filecontent.strip()
1231 elif ext == '.png':
1232 #content_length = file_upload.content_length
1233 # XXX gives -1 for wsgiref, gives 0 for werkzeug :(
1234 # If this is fixed, we could use the file obj, without reading it into memory completely:
1235 filecontent = filecontent.read()
1236
1237 self.put_member('drawing' + ext, filecontent, content_length,
1238 expected_members=set(['drawing.draw', 'drawing.map', 'drawing.png']))
1239
1240 def do_modify(self, template_name):
1241 """
1242 Fills params into the template for initialzing of the the java applet.
1243 The applet is called for doing modifications.
1244 """
1245 form = TextChaizedForm.from_defaults()
1246 TextCha(form).amend_form()
1247 return render_template(self.template,
1248 item_name=self.name,
1249 rows_meta=ROWS_META, cols=COLS,
1250 revno=0,
1251 meta_text=self.meta_dict_to_text(self.meta),
1252 help=self.modify_help,
1253 form=form,
1254 gen=make_generator(),
1255 )
1256
1257 def _render_data(self):
1258 # TODO: this could be a converter -> dom, then transcluding this kind
1259 # of items and also rendering them with the code in base class could work
1260 item_name = self.name
1261 drawing_url = url_for('frontend.get_item', item_name=item_name, member='drawing.draw')
1262 png_url = url_for('frontend.get_item', item_name=item_name, member='drawing.png')
1263 title = _('Edit drawing %(filename)s (opens in new window)', filename=item_name)
1264
1265 mapfile = self.get_member('drawing.map')
1266 try:
1267 image_map = mapfile.read()
1268 mapfile.close()
1269 except (IOError, OSError):
1270 image_map = ''
1271 if image_map:
1272 # we have a image map. inline it and add a map ref to the img tag
1273 mapid = 'ImageMapOf' + item_name
1274 image_map = image_map.replace('%MAPNAME%', mapid)
1275 # add alt and title tags to areas
1276 image_map = re.sub(r'href\s*=\s*"((?!%TWIKIDRAW%).+?)"', r'href="\1" alt="\1" title="\1"', image_map)
1277 image_map = image_map.replace('%TWIKIDRAW%"', '%s" alt="%s" title="%s"' % (drawing_url, title, title))
1278 title = _('Clickable drawing: %(filename)s', filename=item_name)
1279
1280 return Markup(image_map + '<img src="%s" alt="%s" usemap="#%s" />' % (png_url, title, mapid))
1281 else:
1282 return Markup('<img src="%s" alt="%s" />' % (png_url, title))
1283
1284 class AnyWikiDraw(TarMixin, Image):
1285 """
1286 drawings by AnyWikiDraw applet. It creates three files which are stored as tar file.
1287 """
1288 supported_mimetypes = ["application/x-anywikidraw"]
1289 modify_help = ""
1290 template = "modify_anywikidraw.html"
1291
1292 def modify(self):
1293 # called from modify UI/POST
1294 file_upload = request.files.get('filepath')
1295 filename = request.form['filename']
1296 basepath, basename = os.path.split(filename)
1297 basename, ext = os.path.splitext(basename)
1298 filecontent = file_upload.stream
1299 content_length = None
1300 if ext == '.svg':
1301 filecontent = filecontent.read() # read file completely into memory
1302 filecontent = filecontent.replace("\r", "")
1303 elif ext == '.map':
1304 filecontent = filecontent.read() # read file completely into memory
1305 filecontent = filecontent.strip()
1306 elif ext == '.png':
1307 #content_length = file_upload.content_length
1308 # XXX gives -1 for wsgiref, gives 0 for werkzeug :(
1309 # If this is fixed, we could use the file obj, without reading it into memory completely:
1310 filecontent = filecontent.read()
1311 self.put_member('drawing' + ext, filecontent, content_length,
1312 expected_members=set(['drawing.svg', 'drawing.map', 'drawing.png']))
1313
1314 def do_modify(self, template_name):
1315 """
1316 Fills params into the template for initialzing of the the java applet.
1317 The applet is called for doing modifications.
1318 """
1319 form = TextChaizedForm.from_defaults()
1320 TextCha(form).amend_form()
1321 drawing_exists = 'drawing.svg' in self.list_members()
1322 return render_template(self.template,
1323 item_name=self.name,
1324 rows_meta=ROWS_META, cols=COLS,
1325 revno=0,
1326 meta_text=self.meta_dict_to_text(self.meta),
1327 help=self.modify_help,
1328 drawing_exists=drawing_exists,
1329 form=form,
1330 gen=make_generator(),
1331 )
1332
1333 def _render_data(self):
1334 # TODO: this could be a converter -> dom, then transcluding this kind
1335 # of items and also rendering them with the code in base class could work
1336 item_name = self.name
1337 drawing_url = url_for('frontend.get_item', item_name=item_name, member='drawing.svg')
1338 png_url = url_for('frontend.get_item', item_name=item_name, member='drawing.png')
1339 title = _('Edit drawing %(filename)s (opens in new window)', filename=self.name)
1340
1341 mapfile = self.get_member('drawing.map')
1342 try:
1343 image_map = mapfile.read()
1344 mapfile.close()
1345 except (IOError, OSError):
1346 image_map = ''
1347 if image_map:
1348 # ToDo mapid must become uniq
1349 # we have a image map. inline it and add a map ref to the img tag
1350 # we have also to set a unique ID
1351 mapid = 'ImageMapOf' + self.name
1352 image_map = image_map.replace(u'id="drawing.svg"', '')
1353 image_map = image_map.replace(u'name="drawing.svg"', u'name="%s"' % mapid)
1354 # unxml, because 4.01 concrete will not validate />
1355 image_map = image_map.replace(u'/>', u'>')
1356 title = _('Clickable drawing: %(filename)s', filename=self.name)
1357 return Markup(image_map + '<img src="%s" alt="%s" usemap="#%s" />' % (png_url, title, mapid))
1358 else:
1359 return Markup('<img src="%s" alt="%s" />' % (png_url, title))
1360
1361 class SvgDraw(TarMixin, Image):
1362 """ drawings by svg-edit. It creates two files (svg, png) which are stored as tar file. """
1363
1364 supported_mimetypes = ['application/x-svgdraw']
1365 modify_help = ""
1366 template = "modify_svg-edit.html"
1367
1368 def modify(self):
1369 # called from modify UI/POST
1370 file_upload = request.values.get('data')
1371 filename = request.form['filename']
1372 filecontent = file_upload.decode('base_64')
1373 basepath, basename = os.path.split(filename)
1374 basename, ext = os.path.splitext(basename)
1375 content_length = None
1376
1377 if ext == '.png':
1378 filecontent = base64.urlsafe_b64decode(filecontent.split(',')[1])
1379 self.put_member(filename, filecontent, content_length,
1380 expected_members=set(['drawing.svg', 'drawing.png']))
1381
1382 def do_modify(self, template_name):
1383 """
1384 Fills params into the template for initializing of the applet.
1385 """
1386 form = TextChaizedForm.from_defaults()
1387 TextCha(form).amend_form()
1388 return render_template(self.template,
1389 item_name=self.name,
1390 rows_meta=ROWS_META, cols=COLS,
1391 revno=0,
1392 meta_text=self.meta_dict_to_text(self.meta),
1393 help=self.modify_help,
1394 form=form,
1395 gen=make_generator(),
1396 )
1397
1398 def _render_data(self):
1399 # TODO: this could be a converter -> dom, then transcluding this kind
1400 # of items and also rendering them with the code in base class could work
1401 item_name = self.name
1402 drawing_url = url_for('frontend.get_item', item_name=item_name, member='drawing.svg')
1403 png_url = url_for('frontend.get_item', item_name=item_name, member='drawing.png')
1404 return Markup('<img src="%s" alt="%s" />' % (png_url, drawing_url))