comparison MoinMoin/items/__init__.py @ 1451:c32ff2e07e7a

Introduce itemtype. itemtype is used to affect the overall frontend view of an item, and to affect the indexer by offering a set of metadata keys. The original items.Item hierarchy was mosted turned into the new Content hierarchy, leaving code affecting the overview in the new Item and Default (which is an Item descent) classes. Item instances now have a `content` property which is what used to be an Item instance (now a Content instance). This is the first itemtype changeset which just moves codes around ensure they are not broken. More changesets follow soon.
author Cheer Xiao <xiaqqaix@gmail.com>
date Sat, 28 Jul 2012 01:22:44 +0800
parents 9f36555901db
children 35024b2245c5
comparison
equal deleted inserted replaced
1450:90d02867e144 1451:c32ff2e07e7a
6 # Copyright: 2010 MoinMoin:ValentinJaniaut 6 # Copyright: 2010 MoinMoin:ValentinJaniaut
7 # Copyright: 2010 MoinMoin:DiogenesAugusto 7 # Copyright: 2010 MoinMoin:DiogenesAugusto
8 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details. 8 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
9 9
10 """ 10 """
11 MoinMoin - misc. mimetype items 11 MoinMoin - high-level (frontend) items
12 12
13 While MoinMoin.storage cares for backend storage of items, 13 While MoinMoin.storage cares for backend storage of items,
14 this module cares for more high-level, frontend items, 14 this module cares for more high-level, frontend items,
15 e.g. showing, editing, etc. of wiki items. 15 e.g. showing, editing, etc. of wiki items.
16
17 Each class in this module corresponds to an itemtype.
16 """ 18 """
17 # TODO: split this huge module into multiple ones after code has stabilized 19
18 20 import re, time
19 import os, re, time, datetime, base64
20 import tarfile
21 import zipfile
22 import tempfile
23 import itertools 21 import itertools
24 from StringIO import StringIO 22 from StringIO import StringIO
25 from array import array 23
26 24 from flatland import Form
27 from flatland import Form, String, Integer, Boolean, Enum 25 from flatland.validation import Validator
28 from flatland.validation import Validator, Present, IsEmail, ValueBetween, URLValidator, Converted
29 26
30 from whoosh.query import Term, And, Prefix 27 from whoosh.query import Term, And, Prefix
31 28
32 from MoinMoin.forms import RequiredText, OptionalText, File, Submit 29 from MoinMoin.forms import RequiredText, OptionalText, OptionalMultilineText, Tags, Submit
33 30
34 from MoinMoin.security.textcha import TextCha, TextChaizedForm, TextChaValid 31 from MoinMoin.security.textcha import TextCha, TextChaizedForm
35 from MoinMoin.signalling import item_modified 32 from MoinMoin.signalling import item_modified
36 from MoinMoin.util.mimetype import MimeType 33 from MoinMoin.util.mime import Type
37 from MoinMoin.util.mime import Type, type_moin_document
38 from MoinMoin.util.tree import moin_page, html, xlink, docbook
39 from MoinMoin.util.iri import Iri
40 from MoinMoin.util.crypto import cache_key
41 from MoinMoin.storage.middleware.protecting import AccessDenied 34 from MoinMoin.storage.middleware.protecting import AccessDenied
42
43 try:
44 import PIL
45 from PIL import Image as PILImage
46 from PIL.ImageChops import difference as PILdiff
47 except ImportError:
48 PIL = None
49 35
50 from MoinMoin import log 36 from MoinMoin import log
51 logging = log.getLogger(__name__) 37 logging = log.getLogger(__name__)
52 38
53 try: 39 try:
56 import simplejson as json 42 import simplejson as json
57 43
58 from flask import current_app as app 44 from flask import current_app as app
59 from flask import g as flaskg 45 from flask import g as flaskg
60 46
61 from flask import request, url_for, flash, Response, redirect, abort, escape 47 from flask import request, Response, redirect, abort, escape
62 48
63 from werkzeug import is_resource_modified 49 from werkzeug import is_resource_modified
64 from jinja2 import Markup 50
65 51 from MoinMoin.i18n import L_
66 from MoinMoin.i18n import _, L_, N_
67 from MoinMoin.themes import render_template 52 from MoinMoin.themes import render_template
68 from MoinMoin import wikiutil, config, user
69 from MoinMoin.util.send_file import send_file
70 from MoinMoin.util.interwiki import url_for_item 53 from MoinMoin.util.interwiki import url_for_item
71 from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError, StorageError 54 from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError, StorageError
72 from MoinMoin.config import NAME, NAME_OLD, NAME_EXACT, WIKINAME, MTIME, REVERTED_TO, ACL, \ 55 from MoinMoin.util.registry import RegistryBase
73 IS_SYSITEM, SYSITEM_VERSION, USERGROUP, SOMEDICT, \ 56 from MoinMoin.constants.keys import (
74 CONTENTTYPE, SIZE, LANGUAGE, ITEMLINKS, ITEMTRANSCLUSIONS, \ 57 NAME, NAME_OLD, NAME_EXACT, WIKINAME, MTIME, SYSITEM_VERSION, ITEMTYPE,
75 TAGS, ACTION, ADDRESS, HOSTNAME, USERID, EXTRA, COMMENT, \ 58 CONTENTTYPE, SIZE, TAGS, ACTION, ADDRESS, HOSTNAME, USERID, COMMENT,
76 HASH_ALGORITHM, CONTENTTYPE_GROUPS, ITEMID, REVID, DATAID, \ 59 HASH_ALGORITHM, ITEMID, REVID, DATAID, CURRENT, PARENTID
77 CURRENT, PARENTID 60 )
61 from MoinMoin.constants.contenttypes import charset, CONTENTTYPE_GROUPS
62
63 #from .content import content_registry
64
78 65
79 COLS = 80 66 COLS = 80
80 ROWS_DATA = 20
81 ROWS_META = 10 67 ROWS_META = 10
82
83
84 from ..util.registry import RegistryBase
85 68
86 69
87 class RegistryItem(RegistryBase): 70 class RegistryItem(RegistryBase):
88 class Entry(object): 71 class Entry(object):
89 def __init__(self, factory, content_type, priority): 72 def __init__(self, factory, itemtype, priority):
90 self.factory = factory 73 self.factory = factory
91 self.content_type = content_type 74 self.itemtype = itemtype
92 self.priority = priority 75 self.priority = priority
93 76
94 def __call__(self, name, content_type, kw): 77 def __call__(self, name, itemtype, kw):
95 if self.content_type.issupertype(content_type): 78 if self.itemtype == itemtype:
96 return self.factory(name, content_type, **kw) 79 return self.factory(name, itemtype, **kw)
97 80
98 def __eq__(self, other): 81 def __eq__(self, other):
99 if isinstance(other, self.__class__): 82 if isinstance(other, self.__class__):
100 return (self.factory == other.factory and 83 return (self.factory == other.factory and
101 self.content_type == other.content_type and 84 self.itemtype == other.itemtype and
102 self.priority == other.priority) 85 self.priority == other.priority)
103 return NotImplemented 86 return NotImplemented
104 87
105 def __lt__(self, other): 88 def __lt__(self, other):
106 if isinstance(other, self.__class__): 89 if isinstance(other, self.__class__):
107 if self.priority < other.priority: 90 if self.priority < other.priority:
108 return True 91 return True
109 if self.content_type != other.content_type: 92 return self.itemtype == other.itemtype
110 return other.content_type.issupertype(self.content_type)
111 return False
112 return NotImplemented 93 return NotImplemented
113 94
114 def __repr__(self): 95 def __repr__(self):
115 return '<{0}: {1}, prio {2} [{3!r}]>'.format(self.__class__.__name__, 96 return '<{0}: {1}, prio {2} [{3!r}]>'.format(self.__class__.__name__,
116 self.content_type, 97 self.itemtype,
117 self.priority, 98 self.priority,
118 self.factory) 99 self.factory)
119 100
120 def get(self, name, content_type, **kw): 101 def get(self, name, itemtype, **kw):
121 for entry in self._entries: 102 for entry in self._entries:
122 item = entry(name, content_type, kw) 103 item = entry(name, itemtype, kw)
123 if item is not None: 104 if item is not None:
124 return item 105 return item
125 106
126 def register(self, factory, content_type, priority=RegistryBase.PRIORITY_MIDDLE): 107 def register(self, factory, itemtype, priority=RegistryBase.PRIORITY_MIDDLE):
127 """ 108 """
128 Register a factory 109 Register a factory
129 110
130 :param factory: Factory to register. Callable, must return an object. 111 :param factory: Factory to register. Callable, must return an object.
131 """ 112 """
132 return self._register(self.Entry(factory, content_type, priority)) 113 return self._register(self.Entry(factory, itemtype, priority))
133 114
134 115
135 item_registry = RegistryItem() 116 item_registry = RegistryItem()
136
137
138 def conv_serialize(doc, namespaces, method='polyglot'):
139 out = array('u')
140 flaskg.clock.start('conv_serialize')
141 doc.write(out.fromunicode, namespaces=namespaces, method=method)
142 out = out.tounicode()
143 flaskg.clock.stop('conv_serialize')
144 return out
145 117
146 118
147 class DummyRev(dict): 119 class DummyRev(dict):
148 """ if we have no stored Revision, we use this dummy """ 120 """ if we have no stored Revision, we use this dummy """
149 def __init__(self, item, contenttype): 121 def __init__(self, item, contenttype):
161 return [] # same as an empty Item 133 return [] # same as an empty Item
162 def destroy_all_revisions(self): 134 def destroy_all_revisions(self):
163 return True 135 return True
164 136
165 137
138 # XXX To code reviewers: Code chunks within {{{ }}} were moved verbatim from
139 # somewhere else. Wherever I declare it to be "untouched", "untouched except
140 # for ..." I double checked by isolating the same chunk in old and new
141 # revisions and did a manual `diff` on that. FYI, the workflow is (in Vim):
142
143 # 1. in the new revision select the chunk in visual line mode (V)
144 # 2. :'<,'>w! /tmp/chunk.new
145 # 3. do the same with the old revision, writing to /tmp/chunk.old
146 # 4. :!diff /tmp/chunk.old /tmp/chunk.new
147
148 # So trust me :-P
149
150
151 # XXX Moved verbatim from below Item untouched {{{
152 class ValidJSON(Validator):
153 """Validator for JSON
154 """
155 invalid_json_msg = L_('Invalid JSON.')
156
157 def validate(self, element, state):
158 try:
159 json.loads(element.value)
160 except:
161 return self.note_error(element, state, 'invalid_json_msg')
162 return True
163
164
165 class BaseChangeForm(TextChaizedForm):
166 comment = OptionalText.using(label=L_('Comment')).with_properties(placeholder=L_("Comment about your change"))
167 submit = Submit
168 # }}}
169
170
166 class Item(object): 171 class Item(object):
167 """ Highlevel (not storage) Item """ 172 """ Highlevel (not storage) Item, wraps around a storage Revision"""
168 @classmethod 173 @classmethod
169 def _factory(cls, name=u'', contenttype=None, **kw): 174 def _factory(cls, name=u'', itemtype=None, **kw):
170 return cls(name, contenttype=unicode(contenttype), **kw) 175 return cls(name, **kw)
171 176
177 # TODO split Content creation to Content.create
172 @classmethod 178 @classmethod
173 def create(cls, name=u'', contenttype=None, rev_id=CURRENT, item=None): 179 def create(cls, name=u'', itemtype=None, contenttype=None, rev_id=CURRENT, item=None):
180 """
181 Create a highlevel Item by looking up :name or directly wrapping
182 :item and extract the Revision designated by :rev_id revision.
183
184 The highlevel Item is created by creating an instance of Content
185 subclass according to the item's contenttype metadata entry; The
186 :contenttype argument can be used to override contenttype. It is used
187 only when handling +convert (when deciding the contenttype of target
188 item), +modify (when creating a new item whose contenttype is not yet
189 decided), +diff and +diffraw (to coerce the Content to a common
190 super-contenttype of both revisions).
191
192 After that the Content instance, an instance of Item subclass is
193 created according to the item's itemtype metadata entry, and the
194 previously created Content instance is assigned to its content
195 property.
196 """
174 if contenttype is None: 197 if contenttype is None:
175 contenttype = u'application/x-nonexistent' 198 contenttype = u'application/x-nonexistent'
176 199 if itemtype is None:
200 itemtype = u'nonexistent'
177 if 1: # try: 201 if 1: # try:
178 if item is None: 202 if item is None:
179 item = flaskg.storage[name] 203 item = flaskg.storage[name]
180 else: 204 else:
181 name = item.name 205 name = item.name
187 else: 211 else:
188 logging.debug("Got item: {0!r}".format(name)) 212 logging.debug("Got item: {0!r}".format(name))
189 try: 213 try:
190 rev = item.get_revision(rev_id) 214 rev = item.get_revision(rev_id)
191 contenttype = u'application/octet-stream' # it exists 215 contenttype = u'application/octet-stream' # it exists
216 itemtype = u'default' # default itemtype to u'default' for compatibility
192 except KeyError: # NoSuchRevisionError: 217 except KeyError: # NoSuchRevisionError:
193 try: 218 try:
194 rev = item.get_revision(CURRENT) # fall back to current revision 219 rev = item.get_revision(CURRENT) # fall back to current revision
195 # XXX add some message about invalid revision 220 # XXX add some message about invalid revision
196 except KeyError: # NoSuchRevisionError: 221 except KeyError: # NoSuchRevisionError:
200 logging.debug("Got item {0!r}, revision: {1!r}".format(name, rev_id)) 225 logging.debug("Got item {0!r}, revision: {1!r}".format(name, rev_id))
201 contenttype = rev.meta.get(CONTENTTYPE) or contenttype # use contenttype in case our metadata does not provide CONTENTTYPE 226 contenttype = rev.meta.get(CONTENTTYPE) or contenttype # use contenttype in case our metadata does not provide CONTENTTYPE
202 logging.debug("Item {0!r}, got contenttype {1!r} from revision meta".format(name, contenttype)) 227 logging.debug("Item {0!r}, got contenttype {1!r} from revision meta".format(name, contenttype))
203 #logging.debug("Item %r, rev meta dict: %r" % (name, dict(rev.meta))) 228 #logging.debug("Item %r, rev meta dict: %r" % (name, dict(rev.meta)))
204 229
205 item = item_registry.get(name, Type(contenttype), rev=rev) 230 # XXX Cannot pass item=item to Content.__init__ via
206 logging.debug("ItemClass {0!r} handles {1!r}".format(item.__class__, contenttype)) 231 # content_registry.get yet, have to patch it later.
232 content = content_registry.get(name, Type(contenttype))
233 logging.debug("Content class {0!r} handles {1!r}".format(content.__class__, contenttype))
234
235 itemtype = rev.meta.get(ITEMTYPE) or itemtype
236 logging.debug("Item {0!r}, got itemtype {1!r} from revision meta".format(name, itemtype))
237
238 item = item_registry.get(name, itemtype, rev=rev, content=content)
239 logging.debug("Item class {0!r} handles {1!r}".format(item.__class__, itemtype))
240
241 content.item = item
242
207 return item 243 return item
208 244
209 def __init__(self, name, rev=None, contenttype=None): 245 def __init__(self, name, rev=None, content=None):
210 self.name = name 246 self.name = name
211 self.rev = rev 247 self.rev = rev
212 self.contenttype = contenttype 248 self.content = content
213 249
214 def get_meta(self): 250 def get_meta(self):
215 return self.rev.meta 251 return self.rev.meta
216 meta = property(fget=get_meta) 252 meta = property(fget=get_meta)
217 253
254 # XXX Backward compatibility, remove soon
255 @property
256 def contenttype(self):
257 return self.content.contenttype if self.content else None
258
218 def _render_meta(self): 259 def _render_meta(self):
219 # override this in child classes 260 return "<pre>{0}</pre>".format(escape(self.meta_dict_to_text(self.meta, use_filter=False)))
220 return '' 261
221 262 def meta_filter(self, meta):
263 """ kill metadata entries that we set automatically when saving """
264 kill_keys = [# shall not get copied from old rev to new rev
265 SYSITEM_VERSION,
266 NAME_OLD,
267 # are automatically implanted when saving
268 NAME,
269 ITEMID, REVID, DATAID,
270 HASH_ALGORITHM,
271 SIZE,
272 COMMENT,
273 MTIME,
274 ACTION,
275 ADDRESS, HOSTNAME, USERID,
276 ]
277 for key in kill_keys:
278 meta.pop(key, None)
279 return meta
280
281 def meta_text_to_dict(self, text):
282 """ convert meta data from a text fragment to a dict """
283 meta = json.loads(text)
284 return self.meta_filter(meta)
285
286 def meta_dict_to_text(self, meta, use_filter=True):
287 """ convert meta data from a dict to a text fragment """
288 meta = dict(meta)
289 if use_filter:
290 meta = self.meta_filter(meta)
291 return json.dumps(meta, sort_keys=True, indent=2, ensure_ascii=False)
292
293 def prepare_meta_for_modify(self, meta):
294 """
295 transform the meta dict of the current revision into a meta dict
296 that can be used for savind next revision (after "modify").
297 """
298 meta = dict(meta)
299 revid = meta.pop(REVID, None)
300 if revid is not None:
301 meta[PARENTID] = revid
302 return meta
303
304 def _rename(self, name, comment, action):
305 self._save(self.meta, self.content.data, name=name, action=action, comment=comment)
306 for child in self.get_index():
307 item = Item.create(child[0])
308 item._save(item.meta, item.content.data, name='/'.join((name, child[1])), action=action, comment=comment)
309
310 def rename(self, name, comment=u''):
311 """
312 rename this item to item <name>
313 """
314 return self._rename(name, comment, action=u'RENAME')
315
316 def delete(self, comment=u''):
317 """
318 delete this item
319 """
320 trash_prefix = u'Trash/' # XXX move to config
321 now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime())
322 # make trash name unique by including timestamp:
323 trashname = u'{0}{1} ({2} UTC)'.format(trash_prefix, self.name, now)
324 return self._rename(trashname, comment, action=u'TRASH')
325
326 def revert(self, comment=u''):
327 return self._save(self.meta, self.content.data, action=u'REVERT', comment=comment)
328
329 def destroy(self, comment=u'', destroy_item=False):
330 # called from destroy UI/POST
331 if destroy_item:
332 # destroy complete item with all revisions, metadata, etc.
333 self.rev.item.destroy_all_revisions()
334 else:
335 # just destroy this revision
336 self.rev.item.destroy_revision(self.rev.revid)
337
338 def modify(self, meta, data, comment=u'', contenttype_guessed=None, contenttype_qs=None):
339 if contenttype_qs:
340 # we use querystring param to FORCE content type
341 meta[CONTENTTYPE] = contenttype_qs
342
343 return self._save(meta, data, contenttype_guessed=contenttype_guessed, comment=comment)
344
345 class _ModifyForm(BaseChangeForm):
346 """Base class for ModifyForm of Item subclasses."""
347 meta_text = OptionalMultilineText.using(label=L_("MetaData (JSON)")).with_properties(rows=ROWS_META, cols=COLS).validated_by(ValidJSON())
348
349 def _load(self, item):
350 self['meta_text'] = item.meta_dict_to_text(item.prepare_meta_for_modify(item.meta))
351 self['content_form']._load(item.content)
352
353 def _dump(self, item):
354 meta = item.meta_text_to_dict(self['meta_text'].value)
355 data, contenttype_guessed = self['content_form']._dump(item.content)
356 comment = self['comment'].value
357 return meta, data, contenttype_guessed, comment
358
359 @classmethod
360 def from_item(cls, item):
361 form = cls.from_defaults()
362 TextCha(form).amend_form()
363 form._load(item)
364 return form
365
366 @classmethod
367 def from_request(cls, request):
368 form = cls.from_flat(request.form.items() + request.files.items())
369 TextCha(form).amend_form()
370 return form
371
372 def do_modify(self):
373 """
374 Handle +modify requests, both GET and POST.
375
376 This method should be overridden in subclasses, providing polymorphic
377 behavior for the +modify view.
378 """
379 raise NotImplementedError
380
381 def _save(self, meta, data=None, name=None, action=u'SAVE', contenttype_guessed=None, comment=u'', overwrite=False):
382 backend = flaskg.storage
383 storage_item = backend[self.name]
384 try:
385 currentrev = storage_item.get_revision(CURRENT)
386 rev_id = currentrev.revid
387 contenttype_current = currentrev.meta.get(CONTENTTYPE)
388 except KeyError: # XXX was: NoSuchRevisionError:
389 currentrev = None
390 rev_id = None
391 contenttype_current = None
392
393 meta = dict(meta) # we may get a read-only dict-like, copy it
394
395 # we store the previous (if different) and current item name into revision metadata
396 # this is useful for rename history and backends that use item uids internally
397 if name is None:
398 name = self.name
399 oldname = meta.get(NAME)
400 if oldname and oldname != name:
401 meta[NAME_OLD] = oldname
402 meta[NAME] = name
403
404 if comment:
405 meta[COMMENT] = unicode(comment)
406
407 if not overwrite and REVID in meta:
408 # we usually want to create a new revision, thus we must remove the existing REVID
409 del meta[REVID]
410
411 if data is None:
412 if currentrev is not None:
413 # we don't have (new) data, just copy the old one.
414 # a valid usecase of this is to just edit metadata.
415 data = currentrev.data
416 else:
417 data = ''
418
419 if isinstance(data, unicode):
420 data = data.encode(charset) # XXX wrong! if contenttype gives a coding, we MUST use THAT.
421
422 if isinstance(data, str):
423 data = StringIO(data)
424
425 newrev = storage_item.store_revision(meta, data, overwrite=overwrite,
426 action=unicode(action),
427 contenttype_current=contenttype_current,
428 contenttype_guessed=contenttype_guessed,
429 )
430 item_modified.send(app._get_current_object(), item_name=name)
431 return newrev.revid, newrev.meta[SIZE]
432
433 def get_index(self):
434 """ create an index of sub items of this item """
435 if self.name:
436 prefix = self.name + u'/'
437 query = And([Term(WIKINAME, app.cfg.interwikiname), Prefix(NAME_EXACT, prefix)])
438 else:
439 # trick: an item of empty name can be considered as "virtual root item",
440 # that has all wiki items as sub items
441 prefix = u''
442 query = Term(WIKINAME, app.cfg.interwikiname)
443 # We only want the sub-item part of the item names, not the whole item objects.
444 prefix_len = len(prefix)
445 revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
446 items = [(rev.meta[NAME], rev.meta[NAME][prefix_len:], rev.meta[CONTENTTYPE])
447 for rev in revs]
448 return items
449
450 def _connect_levels(self, index):
451 new_index = []
452 last = self.name
453 for item in index:
454 name = item[0]
455
456 while not name.startswith(last):
457 last = last.rpartition('/')[0]
458
459 missing_layers = name.split('/')[last.count('/')+1:-1]
460
461 for layer in missing_layers:
462 last = '/'.join([last, layer])
463 new_index.append((last, last[len(self.name)+1:], u'application/x-nonexistent'))
464
465 last = item[0]
466 new_index.append(item)
467
468 return new_index
469
470 def flat_index(self, startswith=None, selected_groups=None):
471 """
472 creates a top level index of sub items of this item
473 if startswith is set, filtering is done on the basis of starting letter of item name
474 if selected_groups is set, items whose contentype belonging to the selected contenttype_groups, are filtered.
475 """
476 index = self.get_index()
477 index = self._connect_levels(index)
478
479 all_ctypes = [[ctype for ctype, clabel in contenttypes]
480 for gname, contenttypes in CONTENTTYPE_GROUPS]
481 all_ctypes_chain = itertools.chain(*all_ctypes)
482 all_contenttypes = list(all_ctypes_chain)
483 contenttypes_without_encoding = [contenttype[:contenttype.index(u';')]
484 for contenttype in all_contenttypes
485 if u';' in contenttype]
486 all_contenttypes.extend(contenttypes_without_encoding) # adding more mime-types without the encoding term
487
488 if selected_groups:
489 ctypes = [[ctype for ctype, clabel in contenttypes]
490 for gname, contenttypes in CONTENTTYPE_GROUPS
491 if gname in selected_groups]
492 ctypes_chain = itertools.chain(*ctypes)
493 selected_contenttypes = list(ctypes_chain)
494 contenttypes_without_encoding = [contenttype[:contenttype.index(u';')]
495 for contenttype in selected_contenttypes
496 if u';' in contenttype]
497 selected_contenttypes.extend(contenttypes_without_encoding)
498 else:
499 selected_contenttypes = all_contenttypes
500
501 unknown_item_group = "unknown items"
502 if startswith:
503 startswith = (u'{0}'.format(startswith), u'{0}'.format(startswith.swapcase()))
504 if not selected_groups or unknown_item_group in selected_groups:
505 index = [(fullname, relname, contenttype)
506 for fullname, relname, contenttype in index
507 if u'/' not in relname
508 and relname.startswith(startswith)
509 and (contenttype not in all_contenttypes or contenttype in selected_contenttypes)]
510 # If an item's contenttype not present in the default contenttype list,
511 # then it will be shown without going through any filter.
512 else:
513 index = [(fullname, relname, contenttype)
514 for fullname, relname, contenttype in index
515 if u'/' not in relname
516 and relname.startswith(startswith)
517 and (contenttype in selected_contenttypes)]
518
519 else:
520 if not selected_groups or unknown_item_group in selected_groups:
521 index = [(fullname, relname, contenttype)
522 for fullname, relname, contenttype in index
523 if u'/' not in relname
524 and (contenttype not in all_contenttypes or contenttype in selected_contenttypes)]
525 else:
526 index = [(fullname, relname, contenttype)
527 for fullname, relname, contenttype in index
528 if u'/' not in relname
529 and contenttype in selected_contenttypes]
530
531 return index
532
533 index_template = 'index.html'
534
535 def get_detailed_index(self, index):
536 """ appends a flag in the index of items indicating that the parent has sub items """
537 detailed_index = []
538 all_item_index = self.get_index()
539 all_item_text = "\n".join(item_info[1] for item_info in all_item_index)
540 for fullname, relname, contenttype in index:
541 hassubitem = False
542 subitem_name_re = u"^{0}/[^/]+$".format(re.escape(relname))
543 regex = re.compile(subitem_name_re, re.UNICODE|re.M)
544 if regex.search(all_item_text):
545 hassubitem = True
546 detailed_index.append((fullname, relname, contenttype, hassubitem))
547 return detailed_index
548
549 def name_initial(self, names=None):
550 initials = [(name[1][0])
551 for name in names]
552 return initials
553
554 delete_template = 'delete.html'
555 destroy_template = 'destroy.html'
556 diff_template = 'diff.html'
557 rename_template = 'rename.html'
558 revert_template = 'revert.html'
559
560
561 class Contentful(Item):
562 """
563 Base class for Item subclasses that have content.
564 """
565 @property
566 def ModifyForm(self):
567 class C(Item._ModifyForm):
568 content_form = self.content.ModifyForm
569 C.__name__ = 'ModifyForm'
570 return C
571
572
573 # TODO better name and clearer definition
574 class Default(Contentful):
575 """
576 A "conventional" wiki item.
577 """
578 # XXX this method was moved from Item, untouched except for the addition
579 # of itemtype keyword argument plus the comment above it.
580 def _do_modify_show_templates(self):
581 # call this if the item is still empty
582 rev_ids = []
583 item_templates = self.content.get_templates(self.contenttype)
584 return render_template('modify_show_template_selection.html',
585 item_name=self.name,
586 # XXX avoid the magic string
587 itemtype=u'default',
588 rev=self.rev,
589 contenttype=self.contenttype,
590 templates=item_templates,
591 first_rev_id=rev_ids and rev_ids[0],
592 last_rev_id=rev_ids and rev_ids[-1],
593 meta_rendered='',
594 data_rendered='',
595 )
596
597 # To code reviewers: this method was mostly merged from Item.do_modify and
598 # Draw, with modifications.
599 def do_modify(self):
600 method = request.method
601 if method == 'GET':
602 if isinstance(self.content, NonExistentContent):
603 return render_template('modify_show_contenttype_selection.html',
604 item_name=self.name,
605 # XXX avoid the magic string
606 itemtype=u'default',
607 contenttype_groups=CONTENTTYPE_GROUPS,
608 )
609 item = self
610 if isinstance(self.rev, DummyRev):
611 template_name = request.values.get('template')
612 if template_name is None:
613 return self._do_modify_show_templates()
614 elif template_name:
615 item = Item.create(template_name)
616 form = self.ModifyForm.from_item(item)
617 elif method == 'POST':
618 # XXX workaround for *Draw items
619 if isinstance(self.content, Draw):
620 try:
621 self.content.handle_post()
622 except AccessDenied:
623 abort(403)
624 else:
625 # *Draw Applets POSTs more than once, redirecting would
626 # break them
627 return "OK"
628 form = self.ModifyForm.from_request(request)
629 if form.validate():
630 meta, data, contenttype_guessed, comment = form._dump(self)
631 contenttype_qs = request.values.get('contenttype')
632 try:
633 self.modify(meta, data, comment, contenttype_guessed, contenttype_qs)
634 except AccessDenied:
635 abort(403)
636 else:
637 return redirect(url_for_item(self.name))
638 return render_template(self.modify_template,
639 item_name=self.name,
640 rows_meta=str(ROWS_META), cols=str(COLS),
641 form=form,
642 search_form=None,
643 )
644
645 modify_template = 'modify.html'
646
647 item_registry.register(Default._factory, u'default')
648
649
650 class Ticket(Contentful):
651 """
652 Stub for ticket item class.
653 """
654
655 item_registry.register(Ticket._factory, u'ticket')
656
657
658 class Userprofile(Item):
659 """
660 Currently userprofile is implemented as a contenttype. This is a stub of an
661 itemtype implementation of userprofile.
662 """
663
664 item_registry.register(Userprofile._factory, u'userprofile')
665
666
667 class NonExistent(Item):
668 def _convert(self, doc):
669 abort(404)
670
671 def do_modify(self):
672 # First, check if the current user has the required privileges
673 if not flaskg.user.may.create(self.name):
674 abort(403)
675
676 # TODO Construct this list from the item_registry. Two more fields (ie.
677 # display name and description) are needed in the registry then to
678 # support the automatic construction.
679 ITEMTYPES = [
680 (u'default', u'Default', 'Wiki item'),
681 (u'ticket', u'Ticket', 'Ticket item'),
682 ]
683
684 return render_template('modify_show_itemtype_selection.html',
685 item_name=self.name,
686 itemtypes=ITEMTYPES,
687 )
688
689 item_registry.register(NonExistent._factory, u'nonexistent')
690
691
692 # This should be a separate module items/content.py. Included here to
693 # faciliate codereview.
694 """
695 MoinMoin - item contents
696
697 Classes handling the content part of items (ie. minus metadata). The
698 content part is sometimes called the "data" part in other places, but is
699 always called content in this module to avoid confusion.
700
701 Each class in this module corresponds to a contenttype value.
702 """
703
704 import os, re, base64
705 import tarfile
706 import zipfile
707 import tempfile
708 from StringIO import StringIO
709 from array import array
710
711 from flatland import Form, String
712
713 from whoosh.query import Term, And
714
715 from MoinMoin.forms import File
716
717 from MoinMoin.util.mimetype import MimeType
718 from MoinMoin.util.mime import Type, type_moin_document
719 from MoinMoin.util.tree import moin_page, html, xlink, docbook
720 from MoinMoin.util.iri import Iri
721 from MoinMoin.util.crypto import cache_key
722 from MoinMoin.storage.middleware.protecting import AccessDenied
723
724 try:
725 import PIL
726 from PIL import Image as PILImage
727 from PIL.ImageChops import difference as PILdiff
728 except ImportError:
729 PIL = None
730
731 from MoinMoin import log
732 logging = log.getLogger(__name__)
733
734 from flask import current_app as app
735 from flask import g as flaskg
736
737 from flask import request, url_for, Response, abort, escape
738
739 from jinja2 import Markup
740
741 from MoinMoin.i18n import _, L_
742 from MoinMoin.themes import render_template
743 from MoinMoin import wikiutil, config
744 from MoinMoin.util.send_file import send_file
745 from MoinMoin.util.interwiki import url_for_item
746 from MoinMoin.storage.error import StorageError
747 from MoinMoin.util.registry import RegistryBase
748 from MoinMoin.constants.keys import (
749 NAME, NAME_EXACT, WIKINAME, CONTENTTYPE, SIZE, TAGS, HASH_ALGORITHM
750 )
751
752
753 COLS = 80
754 ROWS_DATA = 20
755
756
757 # XXX Too much boilerplate in Entry implementation. Maybe use namedtuple
758 # as a starting point?
759 # Renamed from old RegistryItem, whole class untouched
760 class RegistryContent(RegistryBase):
761 class Entry(object):
762 def __init__(self, factory, content_type, priority):
763 self.factory = factory
764 self.content_type = content_type
765 self.priority = priority
766
767 def __call__(self, name, content_type, kw):
768 if self.content_type.issupertype(content_type):
769 return self.factory(name, content_type, **kw)
770
771 def __eq__(self, other):
772 if isinstance(other, self.__class__):
773 return (self.factory == other.factory and
774 self.content_type == other.content_type and
775 self.priority == other.priority)
776 return NotImplemented
777
778 def __lt__(self, other):
779 if isinstance(other, self.__class__):
780 if self.priority < other.priority:
781 return True
782 if self.content_type != other.content_type:
783 return other.content_type.issupertype(self.content_type)
784 return False
785 return NotImplemented
786
787 def __repr__(self):
788 return '<{0}: {1}, prio {2} [{3!r}]>'.format(self.__class__.__name__,
789 self.content_type,
790 self.priority,
791 self.factory)
792
793 def get(self, name, content_type, **kw):
794 for entry in self._entries:
795 item = entry(name, content_type, kw)
796 if item is not None:
797 return item
798
799 def register(self, factory, content_type, priority=RegistryBase.PRIORITY_MIDDLE):
800 """
801 Register a factory
802
803 :param factory: Factory to register. Callable, must return an object.
804 """
805 return self._register(self.Entry(factory, content_type, priority))
806
807
808 content_registry = RegistryContent()
809
810
811 def conv_serialize(doc, namespaces, method='polyglot'):
812 out = array('u')
813 flaskg.clock.start('conv_serialize')
814 doc.write(out.fromunicode, namespaces=namespaces, method=method)
815 out = out.tounicode()
816 flaskg.clock.stop('conv_serialize')
817 return out
818
819
820 class Content(object):
821 """
822 Base for content classes defining some helpers, agnostic about content
823 data.
824 """
825 @classmethod
826 def _factory(cls, name=u'', contenttype=None, **kw):
827 return cls(name, contenttype=unicode(contenttype), **kw)
828
829 def __init__(self, item, contenttype=None):
830 self.item = item
831 # TODO gradually remove self.contenttype as theoretically there is
832 # one-to-one correspondance of contenttype and Content type
833 # (pun intended :)
834 self.contenttype = contenttype
835
836 # XXX For backward-compatibility (so code can be moved from Item
837 # untouched), remove soon
838 @property
839 def rev(self):
840 return self.item.rev
841
842 @property
843 def name(self):
844 return self.item.name
845
846 def get_data(self):
847 return '' # TODO create a better method for binary stuff
848 data = property(fget=get_data)
849
850 # Moved from Item, untouched {{{
222 def internal_representation(self, converters=['smiley']): 851 def internal_representation(self, converters=['smiley']):
223 """ 852 """
224 Return the internal representation of a document using a DOM Tree 853 Return the internal representation of a document using a DOM Tree
225 """ 854 """
226 flaskg.clock.start('conv_in_dom') 855 flaskg.clock.start('conv_in_dom')
299 'xml') 928 'xml')
300 929
301 def _render_data_highlight(self): 930 def _render_data_highlight(self):
302 # override this in child classes 931 # override this in child classes
303 return '' 932 return ''
304 933 # }}}
305 def _do_modify_show_templates(self): 934
306 # call this if the item is still empty 935 # Moved from Binary, untouched {{{
307 rev_ids = []
308 item_templates = self.get_templates(self.contenttype)
309 return render_template('modify_show_template_selection.html',
310 item_name=self.name,
311 rev=self.rev,
312 contenttype=self.contenttype,
313 templates=item_templates,
314 first_rev_id=rev_ids and rev_ids[0],
315 last_rev_id=rev_ids and rev_ids[-1],
316 meta_rendered='',
317 data_rendered='',
318 )
319
320 def meta_filter(self, meta):
321 """ kill metadata entries that we set automatically when saving """
322 kill_keys = [# shall not get copied from old rev to new rev
323 SYSITEM_VERSION,
324 NAME_OLD,
325 # are automatically implanted when saving
326 NAME,
327 ITEMID, REVID, DATAID,
328 HASH_ALGORITHM,
329 SIZE,
330 COMMENT,
331 MTIME,
332 ACTION,
333 ADDRESS, HOSTNAME, USERID,
334 ]
335 for key in kill_keys:
336 meta.pop(key, None)
337 return meta
338
339 def meta_text_to_dict(self, text):
340 """ convert meta data from a text fragment to a dict """
341 meta = json.loads(text)
342 return self.meta_filter(meta)
343
344 def meta_dict_to_text(self, meta, use_filter=True):
345 """ convert meta data from a dict to a text fragment """
346 meta = dict(meta)
347 if use_filter:
348 meta = self.meta_filter(meta)
349 return json.dumps(meta, sort_keys=True, indent=2, ensure_ascii=False)
350
351 def prepare_meta_for_modify(self, meta):
352 """
353 transform the meta dict of the current revision into a meta dict
354 that can be used for savind next revision (after "modify").
355 """
356 meta = dict(meta)
357 revid = meta.pop(REVID, None)
358 if revid is not None:
359 meta[PARENTID] = revid
360 return meta
361
362 def get_data(self):
363 return '' # TODO create a better method for binary stuff
364 data = property(fget=get_data)
365
366 def _write_stream(self, content, new_rev, bufsize=8192):
367 written = 0
368 if hasattr(content, "read"):
369 while True:
370 buf = content.read(bufsize)
371 if not buf:
372 break
373 new_rev.data.write(buf)
374 written += len(buf)
375 elif isinstance(content, str):
376 new_rev.data.write(content)
377 written += len(content)
378 else:
379 raise StorageError("unsupported content object: {0!r}".format(content))
380 return written
381
382 def _rename(self, name, comment, action):
383 self._save(self.meta, self.data, name=name, action=action, comment=comment)
384 for child in self.get_index():
385 item = Item.create(child[0])
386 item._save(item.meta, item.data, name='/'.join((name, child[1])), action=action, comment=comment)
387
388 def rename(self, name, comment=u''):
389 """
390 rename this item to item <name>
391 """
392 return self._rename(name, comment, action=u'RENAME')
393
394 def delete(self, comment=u''):
395 """
396 delete this item
397 """
398 trash_prefix = u'Trash/' # XXX move to config
399 now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime())
400 # make trash name unique by including timestamp:
401 trashname = u'{0}{1} ({2} UTC)'.format(trash_prefix, self.name, now)
402 return self._rename(trashname, comment, action=u'TRASH')
403
404 def revert(self, comment=u''):
405 return self._save(self.meta, self.data, action=u'REVERT', comment=comment)
406
407 def destroy(self, comment=u'', destroy_item=False):
408 # called from destroy UI/POST
409 if destroy_item:
410 # destroy complete item with all revisions, metadata, etc.
411 self.rev.item.destroy_all_revisions()
412 else:
413 # just destroy this revision
414 self.rev.item.destroy_revision(self.rev.revid)
415
416 def modify(self, meta, data, comment=u'', contenttype_guessed=None, contenttype_qs=None):
417 if contenttype_qs:
418 # we use querystring param to FORCE content type
419 meta[CONTENTTYPE] = contenttype_qs
420
421 return self._save(meta, data, contenttype_guessed=contenttype_guessed, comment=comment)
422
423 def _save(self, meta, data=None, name=None, action=u'SAVE', contenttype_guessed=None, comment=u'', overwrite=False):
424 backend = flaskg.storage
425 storage_item = backend[self.name]
426 try:
427 currentrev = storage_item.get_revision(CURRENT)
428 rev_id = currentrev.revid
429 contenttype_current = currentrev.meta.get(CONTENTTYPE)
430 except KeyError: # XXX was: NoSuchRevisionError:
431 currentrev = None
432 rev_id = None
433 contenttype_current = None
434
435 meta = dict(meta) # we may get a read-only dict-like, copy it
436
437 # we store the previous (if different) and current item name into revision metadata
438 # this is useful for rename history and backends that use item uids internally
439 if name is None:
440 name = self.name
441 oldname = meta.get(NAME)
442 if oldname and oldname != name:
443 meta[NAME_OLD] = oldname
444 meta[NAME] = name
445
446 if comment:
447 meta[COMMENT] = unicode(comment)
448
449 if not overwrite and REVID in meta:
450 # we usually want to create a new revision, thus we must remove the existing REVID
451 del meta[REVID]
452
453 if data is None:
454 if currentrev is not None:
455 # we don't have (new) data, just copy the old one.
456 # a valid usecase of this is to just edit metadata.
457 data = currentrev.data
458 else:
459 data = ''
460
461 if isinstance(data, unicode):
462 data = data.encode(config.charset) # XXX wrong! if contenttype gives a coding, we MUST use THAT.
463
464 if isinstance(data, str):
465 data = StringIO(data)
466
467 newrev = storage_item.store_revision(meta, data, overwrite=overwrite,
468 action=unicode(action),
469 contenttype_current=contenttype_current,
470 contenttype_guessed=contenttype_guessed,
471 )
472 item_modified.send(app._get_current_object(), item_name=name)
473 return newrev.revid, newrev.meta[SIZE]
474
475 def get_index(self):
476 """ create an index of sub items of this item """
477 if self.name:
478 prefix = self.name + u'/'
479 query = And([Term(WIKINAME, app.cfg.interwikiname), Prefix(NAME_EXACT, prefix)])
480 else:
481 # trick: an item of empty name can be considered as "virtual root item",
482 # that has all wiki items as sub items
483 prefix = u''
484 query = Term(WIKINAME, app.cfg.interwikiname)
485 # We only want the sub-item part of the item names, not the whole item objects.
486 prefix_len = len(prefix)
487 revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
488 items = [(rev.meta[NAME], rev.meta[NAME][prefix_len:], rev.meta[CONTENTTYPE])
489 for rev in revs]
490 return items
491
492 def _connect_levels(self, index):
493 new_index = []
494 last = self.name
495 for item in index:
496 name = item[0]
497
498 while not name.startswith(last):
499 last = last.rpartition('/')[0]
500
501 missing_layers = name.split('/')[last.count('/')+1:-1]
502
503 for layer in missing_layers:
504 last = '/'.join([last, layer])
505 new_index.append((last, last[len(self.name)+1:], u'application/x-nonexistent'))
506
507 last = item[0]
508 new_index.append(item)
509
510 return new_index
511
512 def flat_index(self, startswith=None, selected_groups=None):
513 """
514 creates a top level index of sub items of this item
515 if startswith is set, filtering is done on the basis of starting letter of item name
516 if selected_groups is set, items whose contentype belonging to the selected contenttype_groups, are filtered.
517 """
518 index = self.get_index()
519 index = self._connect_levels(index)
520
521 all_ctypes = [[ctype for ctype, clabel in contenttypes]
522 for gname, contenttypes in CONTENTTYPE_GROUPS]
523 all_ctypes_chain = itertools.chain(*all_ctypes)
524 all_contenttypes = list(all_ctypes_chain)
525 contenttypes_without_encoding = [contenttype[:contenttype.index(u';')]
526 for contenttype in all_contenttypes
527 if u';' in contenttype]
528 all_contenttypes.extend(contenttypes_without_encoding) # adding more mime-types without the encoding term
529
530 if selected_groups:
531 ctypes = [[ctype for ctype, clabel in contenttypes]
532 for gname, contenttypes in CONTENTTYPE_GROUPS
533 if gname in selected_groups]
534 ctypes_chain = itertools.chain(*ctypes)
535 selected_contenttypes = list(ctypes_chain)
536 contenttypes_without_encoding = [contenttype[:contenttype.index(u';')]
537 for contenttype in selected_contenttypes
538 if u';' in contenttype]
539 selected_contenttypes.extend(contenttypes_without_encoding)
540 else:
541 selected_contenttypes = all_contenttypes
542
543 unknown_item_group = "unknown items"
544 if startswith:
545 startswith = (u'{0}'.format(startswith), u'{0}'.format(startswith.swapcase()))
546 if not selected_groups or unknown_item_group in selected_groups:
547 index = [(fullname, relname, contenttype)
548 for fullname, relname, contenttype in index
549 if u'/' not in relname
550 and relname.startswith(startswith)
551 and (contenttype not in all_contenttypes or contenttype in selected_contenttypes)]
552 # If an item's contenttype not present in the default contenttype list,
553 # then it will be shown without going through any filter.
554 else:
555 index = [(fullname, relname, contenttype)
556 for fullname, relname, contenttype in index
557 if u'/' not in relname
558 and relname.startswith(startswith)
559 and (contenttype in selected_contenttypes)]
560
561 else:
562 if not selected_groups or unknown_item_group in selected_groups:
563 index = [(fullname, relname, contenttype)
564 for fullname, relname, contenttype in index
565 if u'/' not in relname
566 and (contenttype not in all_contenttypes or contenttype in selected_contenttypes)]
567 else:
568 index = [(fullname, relname, contenttype)
569 for fullname, relname, contenttype in index
570 if u'/' not in relname
571 and contenttype in selected_contenttypes]
572
573 return index
574
575 index_template = 'index.html'
576
577 def get_detailed_index(self, index):
578 """ appends a flag in the index of items indicating that the parent has sub items """
579 detailed_index = []
580 all_item_index = self.get_index()
581 all_item_text = "\n".join(item_info[1] for item_info in all_item_index)
582 for fullname, relname, contenttype in index:
583 hassubitem = False
584 subitem_name_re = u"^{0}/[^/]+$".format(re.escape(relname))
585 regex = re.compile(subitem_name_re, re.UNICODE|re.M)
586 if regex.search(all_item_text):
587 hassubitem = True
588 detailed_index.append((fullname, relname, contenttype, hassubitem))
589 return detailed_index
590
591 def name_initial(self, names=None):
592 initials = [(name[1][0])
593 for name in names]
594 return initials
595
596 delete_template = 'delete.html'
597 destroy_template = 'destroy.html'
598 diff_template = 'diff.html'
599 rename_template = 'rename.html'
600 revert_template = 'revert.html'
601
602 class NonExistent(Item):
603 def do_get(self, force_attachment=False, mimetype=None):
604 abort(404)
605
606 def _convert(self, doc):
607 abort(404)
608
609 def do_modify(self, contenttype, template_name):
610 # First, check if the current user has the required privileges
611 if not flaskg.user.may.create(self.name):
612 abort(403)
613
614 return render_template('modify_show_type_selection.html',
615 item_name=self.name,
616 contenttype_groups=CONTENTTYPE_GROUPS,
617 )
618
619 item_registry.register(NonExistent._factory, Type('application/x-nonexistent'))
620
621 class ValidJSON(Validator):
622 """Validator for JSON
623 """
624 invalid_json_msg = L_('Invalid JSON.')
625
626 def validate(self, element, state):
627 try:
628 json.loads(element.value)
629 except:
630 return self.note_error(element, state, 'invalid_json_msg')
631 return True
632
633
634 class BaseChangeForm(TextChaizedForm):
635 comment = OptionalText.using(label=L_('Comment')).with_properties(placeholder=L_("Comment about your change"))
636 submit = Submit
637
638
639 class Binary(Item):
640 """ An arbitrary binary item, fallback class for every item mimetype. """
641 modify_help = """\
642 There is no help, you're doomed!
643 """
644
645 template = "modify_binary.html"
646
647 # XXX reads item rev data into memory!
648 def get_data(self):
649 if self.rev is not None:
650 return self.rev.data.read()
651 else:
652 return ''
653 data = property(fget=get_data)
654
655 def _render_meta(self):
656 return "<pre>{0}</pre>".format(escape(self.meta_dict_to_text(self.meta, use_filter=False)))
657
658 def get_templates(self, contenttype=None): 936 def get_templates(self, contenttype=None):
659 """ create a list of templates (for some specific contenttype) """ 937 """ create a list of templates (for some specific contenttype) """
660 terms = [Term(WIKINAME, app.cfg.interwikiname), Term(TAGS, u'template')] 938 terms = [Term(WIKINAME, app.cfg.interwikiname), Term(TAGS, u'template')]
661 if contenttype is not None: 939 if contenttype is not None:
662 terms.append(Term(CONTENTTYPE, contenttype)) 940 terms.append(Term(CONTENTTYPE, contenttype))
663 query = And(terms) 941 query = And(terms)
664 revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None) 942 revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
665 return [rev.meta[NAME] for rev in revs] 943 return [rev.meta[NAME] for rev in revs]
666 944 # }}}
667 class ModifyForm(BaseChangeForm): 945
668 """Base class for ModifyForm of Binary's subclasses.""" 946
669 meta_text = RequiredText.with_properties(placeholder=L_("MetaData (JSON)")).validated_by(ValidJSON()) 947 class NonExistentContent(Content):
948 """Dummy Content to use with NonExistent."""
949 def do_get(self, force_attachment=False, mimetype=None):
950 abort(404)
951
952 def _convert(self, doc):
953 abort(404)
954
955
956 content_registry.register(NonExistentContent._factory, Type('application/x-nonexistent'))
957
958
959 class Binary(Content):
960 """ An arbitrary binary item, fallback class for every item mimetype. """
961
962 # XXX reads item rev data into memory!
963 def get_data(self):
964 if self.rev is not None:
965 return self.rev.data.read()
966 else:
967 return ''
968 data = property(fget=get_data)
969
970 class ModifyForm(Form):
971 template = 'modify_binary.html'
972 help = """\
973 There is no help, you're doomed!
974 """
670 data_file = File.using(optional=True, label=L_('Upload file:')) 975 data_file = File.using(optional=True, label=L_('Upload file:'))
671 976
672 def _load(self, item): 977 def _load(self, item):
673 self['meta_text'] = item.meta_dict_to_text(item.prepare_meta_for_modify(item.meta)) 978 pass
674 979
675 def _dump(self, item): 980 def _dump(self, item):
676 data = meta = contenttype_guessed = None
677 data_file = self['data_file'].value 981 data_file = self['data_file'].value
678 if data_file: 982 if data_file:
679 data = data_file.stream 983 data = data_file.stream
680 # this is likely a guess by the browser, based on the filename 984 # this is likely a guess by the browser, based on the filename
681 contenttype_guessed = data_file.content_type # comes from form multipart data 985 contenttype_guessed = data_file.content_type # comes from form multipart data
682 meta = item.meta_text_to_dict(self['meta_text'].value) 986 return data, contenttype_guessed
683 comment = self['comment'].value 987 else:
684 return meta, data, contenttype_guessed, comment 988 return None, None
685
686 extra_template_args = {}
687
688 @classmethod
689 def from_item(cls, item):
690 form = cls.from_defaults()
691 TextCha(form).amend_form()
692 form._load(item)
693 return form
694
695 @classmethod
696 def from_request(cls, request):
697 form = cls.from_flat(request.form.items() + request.files.items())
698 TextCha(form).amend_form()
699 return form
700
701 def do_modify(self, contenttype, template_name):
702 """
703 Handle +modify requests, both GET and POST.
704
705 This method can be overridden in subclasses, providing polymorphic
706 behavior for the +modify view.
707 """
708 method = request.method
709 if method == 'GET':
710 item = self
711 if isinstance(self.rev, DummyRev):
712 if template_name is None:
713 return self._do_modify_show_templates()
714 elif template_name:
715 item = Item.create(template_name)
716 form = self.ModifyForm.from_item(item)
717 elif method == 'POST':
718 form = self.ModifyForm.from_request(request)
719 if form.validate():
720 meta, data, contenttype_guessed, comment = form._dump(self)
721 contenttype_qs = request.values.get('contenttype')
722 try:
723 self.modify(meta, data, comment, contenttype_guessed, contenttype_qs)
724 except AccessDenied:
725 abort(403)
726 else:
727 return redirect(url_for_item(self.name))
728 return render_template(self.template,
729 item_name=self.name,
730 rows_meta=str(ROWS_META), cols=str(COLS),
731 help=self.modify_help,
732 form=form,
733 search_form=None,
734 **form.extra_template_args
735 )
736 989
737 def _render_data_diff(self, oldrev, newrev): 990 def _render_data_diff(self, oldrev, newrev):
738 hash_name = HASH_ALGORITHM 991 hash_name = HASH_ALGORITHM
739 if oldrev.meta[hash_name] == newrev.meta[hash_name]: 992 if oldrev.meta[hash_name] == newrev.meta[hash_name]:
740 return _("The items have the same data hash code (that means they very likely have the same data).") 993 return _("The items have the same data hash code (that means they very likely have the same data).")
790 mimetype=content_type, 1043 mimetype=content_type,
791 as_attachment=as_attachment, attachment_filename=filename, 1044 as_attachment=as_attachment, attachment_filename=filename,
792 cache_timeout=10, # wiki data can change rapidly 1045 cache_timeout=10, # wiki data can change rapidly
793 add_etags=True, etag=hash, conditional=True) 1046 add_etags=True, etag=hash, conditional=True)
794 1047
795 item_registry.register(Binary._factory, Type('*/*')) 1048 content_registry.register(Binary._factory, Type('*/*'))
796 1049
797 1050
798 class RenderableBinary(Binary): 1051 class RenderableBinary(Binary):
799 """ Base class for some binary stuff that renders with a object tag. """ 1052 """ Base class for some binary stuff that renders with a object tag. """
800 1053
864 raise StorageError(msg) 1117 raise StorageError(msg)
865 if tf_members == expected_members: 1118 if tf_members == expected_members:
866 # everything we expected has been added to the tar file, save the container as revision 1119 # everything we expected has been added to the tar file, save the container as revision
867 meta = {CONTENTTYPE: self.contenttype} 1120 meta = {CONTENTTYPE: self.contenttype}
868 data = open(temp_fname, 'rb') 1121 data = open(temp_fname, 'rb')
869 self._save(meta, data, name=self.name, action=u'SAVE', comment='') 1122 self.item._save(meta, data, name=self.name, action=u'SAVE', comment='')
870 data.close() 1123 data.close()
871 os.remove(temp_fname) 1124 os.remove(temp_fname)
872 1125
873 1126
874 class ApplicationXTar(TarMixin, Application): 1127 class ApplicationXTar(TarMixin, Application):
875 """ 1128 """
876 Tar items 1129 Tar items
877 """ 1130 """
878 1131
879 item_registry.register(ApplicationXTar._factory, Type('application/x-tar')) 1132 content_registry.register(ApplicationXTar._factory, Type('application/x-tar'))
880 item_registry.register(ApplicationXTar._factory, Type('application/x-gtar')) 1133 content_registry.register(ApplicationXTar._factory, Type('application/x-gtar'))
881 1134
882 1135
883 class ZipMixin(object): 1136 class ZipMixin(object):
884 """ 1137 """
885 ZipMixin offers additional functionality for zip-like items to list and 1138 ZipMixin offers additional functionality for zip-like items to list and
910 class ApplicationZip(ZipMixin, Application): 1163 class ApplicationZip(ZipMixin, Application):
911 """ 1164 """
912 Zip items 1165 Zip items
913 """ 1166 """
914 1167
915 item_registry.register(ApplicationZip._factory, Type('application/zip')) 1168 content_registry.register(ApplicationZip._factory, Type('application/zip'))
916 1169
917 1170
918 class PDF(Application): 1171 class PDF(Application):
919 """ PDF """ 1172 """ PDF """
920 1173
921 item_registry.register(PDF._factory, Type('application/pdf')) 1174 content_registry.register(PDF._factory, Type('application/pdf'))
922 1175
923 1176
924 class Video(Binary): 1177 class Video(Binary):
925 """ Base class for video/* """ 1178 """ Base class for video/* """
926 1179
927 item_registry.register(Video._factory, Type('video/*')) 1180 content_registry.register(Video._factory, Type('video/*'))
928 1181
929 1182
930 class Audio(Binary): 1183 class Audio(Binary):
931 """ Base class for audio/* """ 1184 """ Base class for audio/* """
932 1185
933 item_registry.register(Audio._factory, Type('audio/*')) 1186 content_registry.register(Audio._factory, Type('audio/*'))
934 1187
935 1188
936 class Image(Binary): 1189 class Image(Binary):
937 """ Base class for image/* """ 1190 """ Base class for image/* """
938 1191
939 item_registry.register(Image._factory, Type('image/*')) 1192 content_registry.register(Image._factory, Type('image/*'))
940 1193
941 1194
942 class RenderableImage(RenderableBinary): 1195 class RenderableImage(RenderableBinary):
943 """ Base class for renderable Image mimetypes """ 1196 """ Base class for renderable Image mimetypes """
944 1197
945 1198
946 class SvgImage(RenderableImage): 1199 class SvgImage(RenderableImage):
947 """ SVG images use <object> tag mechanism from RenderableBinary base class """ 1200 """ SVG images use <object> tag mechanism from RenderableBinary base class """
948 1201
949 item_registry.register(SvgImage._factory, Type('image/svg+xml')) 1202 content_registry.register(SvgImage._factory, Type('image/svg+xml'))
950 1203
951 1204
952 class RenderableBitmapImage(RenderableImage): 1205 class RenderableBitmapImage(RenderableImage):
953 """ PNG/JPEG/GIF images use <img> tag (better browser support than <object>) """ 1206 """ PNG/JPEG/GIF images use <img> tag (better browser support than <object>) """
954 # if mimetype is also transformable, please register in TransformableImage ONLY! 1207 # if mimetype is also transformable, please register in TransformableImage ONLY!
1105 return Response(data, headers=headers) 1358 return Response(data, headers=headers)
1106 1359
1107 def _render_data_diff_text(self, oldrev, newrev): 1360 def _render_data_diff_text(self, oldrev, newrev):
1108 return super(TransformableBitmapImage, self)._render_data_diff_text(oldrev, newrev) 1361 return super(TransformableBitmapImage, self)._render_data_diff_text(oldrev, newrev)
1109 1362
1110 item_registry.register(TransformableBitmapImage._factory, Type('image/png')) 1363 content_registry.register(TransformableBitmapImage._factory, Type('image/png'))
1111 item_registry.register(TransformableBitmapImage._factory, Type('image/jpeg')) 1364 content_registry.register(TransformableBitmapImage._factory, Type('image/jpeg'))
1112 item_registry.register(TransformableBitmapImage._factory, Type('image/gif')) 1365 content_registry.register(TransformableBitmapImage._factory, Type('image/gif'))
1113 1366
1114 1367
1115 class Text(Binary): 1368 class Text(Binary):
1116 """ Base class for text/* """ 1369 """ Base class for text/* """
1117 template = "modify_text.html"
1118 1370
1119 class ModifyForm(Binary.ModifyForm): 1371 class ModifyForm(Binary.ModifyForm):
1372 template = 'modify_text.html'
1120 data_text = String.using(strip=False, optional=True).with_properties(placeholder=L_("Type your text here")) 1373 data_text = String.using(strip=False, optional=True).with_properties(placeholder=L_("Type your text here"))
1374 rows = ROWS_DATA
1375 cols = COLS
1121 1376
1122 def _load(self, item): 1377 def _load(self, item):
1123 super(Text.ModifyForm, self)._load(item) 1378 super(Text.ModifyForm, self)._load(item)
1124 data = item.data 1379 data = item.data
1125 data = item.data_storage_to_internal(data) 1380 data = item.data_storage_to_internal(data)
1126 data = item.data_internal_to_form(data) 1381 data = item.data_internal_to_form(data)
1127 self['data_text'] = data 1382 self['data_text'] = data
1128 1383
1129 def _dump(self, item): 1384 def _dump(self, item):
1130 meta, data, contenttype_guessed, comment = super(Text.ModifyForm, self)._dump(item) 1385 data, contenttype_guessed = super(Text.ModifyForm, self)._dump(item)
1131 if data is None: 1386 if data is None:
1132 data = self['data_text'].value 1387 data = self['data_text'].value
1133 data = item.data_form_to_internal(data) 1388 data = item.data_form_to_internal(data)
1134 data = item.data_internal_to_storage(data) 1389 data = item.data_internal_to_storage(data)
1135 # we know it is text and utf-8 - XXX is there a way to get the charset of the form? 1390 # we know it is text and utf-8 - XXX is there a way to get the charset of the form?
1136 contenttype_guessed = u'text/plain;charset=utf-8' 1391 contenttype_guessed = u'text/plain;charset=utf-8'
1137 return meta, data, contenttype_guessed, comment 1392 return data, contenttype_guessed
1138
1139 extra_template_args = {'rows_data': str(ROWS_DATA)}
1140 1393
1141 # text/plain mandates crlf - but in memory, we want lf only 1394 # text/plain mandates crlf - but in memory, we want lf only
1142 def data_internal_to_form(self, text): 1395 def data_internal_to_form(self, text):
1143 """ convert data from memory format to form format """ 1396 """ convert data from memory format to form format """
1144 return text.replace(u'\n', u'\r\n') 1397 return text.replace(u'\n', u'\r\n')
1194 # TODO: Real output format 1447 # TODO: Real output format
1195 html_conv = reg.get(type_moin_document, Type('application/x-xhtml-moin-page')) 1448 html_conv = reg.get(type_moin_document, Type('application/x-xhtml-moin-page'))
1196 doc = html_conv(doc) 1449 doc = html_conv(doc)
1197 return conv_serialize(doc, {html.namespace: ''}) 1450 return conv_serialize(doc, {html.namespace: ''})
1198 1451
1199 item_registry.register(Text._factory, Type('text/*')) 1452 content_registry.register(Text._factory, Type('text/*'))
1200 1453
1201 1454
1202 class MarkupItem(Text): 1455 class MarkupItem(Text):
1203 """ 1456 """
1204 some kind of item with markup 1457 some kind of item with markup
1207 1460
1208 1461
1209 class MoinWiki(MarkupItem): 1462 class MoinWiki(MarkupItem):
1210 """ MoinMoin wiki markup """ 1463 """ MoinMoin wiki markup """
1211 1464
1212 item_registry.register(MoinWiki._factory, Type('text/x.moin.wiki')) 1465 content_registry.register(MoinWiki._factory, Type('text/x.moin.wiki'))
1213 1466
1214 1467
1215 class CreoleWiki(MarkupItem): 1468 class CreoleWiki(MarkupItem):
1216 """ Creole wiki markup """ 1469 """ Creole wiki markup """
1217 1470
1218 item_registry.register(CreoleWiki._factory, Type('text/x.moin.creole')) 1471 content_registry.register(CreoleWiki._factory, Type('text/x.moin.creole'))
1219 1472
1220 1473
1221 class MediaWiki(MarkupItem): 1474 class MediaWiki(MarkupItem):
1222 """ MediaWiki markup """ 1475 """ MediaWiki markup """
1223 1476
1224 item_registry.register(MediaWiki._factory, Type('text/x-mediawiki')) 1477 content_registry.register(MediaWiki._factory, Type('text/x-mediawiki'))
1225 1478
1226 1479
1227 class ReST(MarkupItem): 1480 class ReST(MarkupItem):
1228 """ ReStructured Text markup """ 1481 """ ReStructured Text markup """
1229 1482
1230 item_registry.register(ReST._factory, Type('text/x-rst')) 1483 content_registry.register(ReST._factory, Type('text/x-rst'))
1231 1484
1232 1485
1233 class HTML(Text): 1486 class HTML(Text):
1234 """ 1487 """
1235 HTML markup 1488 HTML markup
1238 output converterter to produce output format (e.g. html_out for html 1491 output converterter to produce output format (e.g. html_out for html
1239 output), all(?) unsafe stuff will get lost. 1492 output), all(?) unsafe stuff will get lost.
1240 1493
1241 Note: If raw revision data is accessed, unsafe stuff might be present! 1494 Note: If raw revision data is accessed, unsafe stuff might be present!
1242 """ 1495 """
1243 template = "modify_text_html.html" 1496 class ModifyForm(Text.ModifyForm):
1244 1497 template = "modify_text_html.html"
1245 item_registry.register(HTML._factory, Type('text/html')) 1498
1499 content_registry.register(HTML._factory, Type('text/html'))
1246 1500
1247 1501
1248 class DocBook(MarkupItem): 1502 class DocBook(MarkupItem):
1249 """ DocBook Document """ 1503 """ DocBook Document """
1250 def _convert(self, doc): 1504 def _convert(self, doc):
1288 mimetype=content_type, 1542 mimetype=content_type,
1289 as_attachment=as_attachment, attachment_filename=None, 1543 as_attachment=as_attachment, attachment_filename=None,
1290 cache_timeout=10, # wiki data can change rapidly 1544 cache_timeout=10, # wiki data can change rapidly
1291 add_etags=False, etag=None, conditional=True) 1545 add_etags=False, etag=None, conditional=True)
1292 1546
1293 item_registry.register(DocBook._factory, Type('application/docbook+xml')) 1547 content_registry.register(DocBook._factory, Type('application/docbook+xml'))
1294 1548
1295 1549
1296 class Draw(TarMixin, Image): 1550 class Draw(TarMixin, Image):
1297 """ 1551 """
1298 Base class for *Draw that use special Java/Javascript applets to modify and store data in a tar file. 1552 Base class for *Draw that use special Java/Javascript applets to modify and store data in a tar file.
1299 """ 1553 """
1300 class ModifyForm(Binary.ModifyForm): 1554 class ModifyForm(Binary.ModifyForm):
1301 pass 1555 # Set the workaround flag respected in modify.html
1556 is_draw = True
1302 1557
1303 def handle_post(): 1558 def handle_post():
1304 raise NotImplementedError 1559 raise NotImplementedError
1305 1560
1306 def do_modify(self, contenttype, template_name):
1307 # XXX as the "saving" POSTs come from *Draw applets (not the form),
1308 # they need to be handled specially for each applet. Besides, editing
1309 # meta_text doesn't work
1310 if request.method == 'POST':
1311 try:
1312 self.handle_post()
1313 except AccessDenied:
1314 abort(403)
1315 else:
1316 # *Draw Applets POSTs more than once, redirecting would break them
1317 return "OK"
1318 else:
1319 return super(Draw, self).do_modify(contenttype, template_name)
1320
1321 1561
1322 class TWikiDraw(Draw): 1562 class TWikiDraw(Draw):
1323 """ 1563 """
1324 drawings by TWikiDraw applet. It creates three files which are stored as tar file. 1564 drawings by TWikiDraw applet. It creates three files which are stored as tar file.
1325 """ 1565 """
1326 modify_help = "" 1566
1327 template = "modify_twikidraw.html" 1567 class ModifyForm(Draw.ModifyForm):
1568 template = "modify_twikidraw.html"
1569 help = ""
1328 1570
1329 def handle_post(self): 1571 def handle_post(self):
1330 # called from modify UI/POST 1572 # called from modify UI/POST
1331 file_upload = request.files.get('filepath') 1573 file_upload = request.files.get('filepath')
1332 filename = request.form['filename'] 1574 filename = request.form['filename']
1375 1617
1376 return Markup(image_map + u'<img src="{0}" alt="{1}" usemap="#{2}" />'.format(png_url, title, mapid)) 1618 return Markup(image_map + u'<img src="{0}" alt="{1}" usemap="#{2}" />'.format(png_url, title, mapid))
1377 else: 1619 else:
1378 return Markup(u'<img src="{0}" alt="{1}" />'.format(png_url, title)) 1620 return Markup(u'<img src="{0}" alt="{1}" />'.format(png_url, title))
1379 1621
1380 item_registry.register(TWikiDraw._factory, Type('application/x-twikidraw')) 1622 content_registry.register(TWikiDraw._factory, Type('application/x-twikidraw'))
1381 1623
1382 1624
1383 class AnyWikiDraw(Draw): 1625 class AnyWikiDraw(Draw):
1384 """ 1626 """
1385 drawings by AnyWikiDraw applet. It creates three files which are stored as tar file. 1627 drawings by AnyWikiDraw applet. It creates three files which are stored as tar file.
1386 """ 1628 """
1387 modify_help = ""
1388 template = "modify_anywikidraw.html"
1389 1629
1390 class ModifyForm(Draw.ModifyForm): 1630 class ModifyForm(Draw.ModifyForm):
1631 template = "modify_anywikidraw.html"
1632 help = ""
1391 def _load(self, item): 1633 def _load(self, item):
1392 super(AnyWikiDraw.ModifyForm, self)._load(item) 1634 super(AnyWikiDraw.ModifyForm, self)._load(item)
1393 try: 1635 try:
1394 drawing_exists = 'drawing.svg' in item.list_members() 1636 drawing_exists = 'drawing.svg' in item.list_members()
1395 except tarfile.TarError: # item doesn't exist yet 1637 except tarfile.TarError: # item doesn't exist yet
1396 drawing_exists = False 1638 drawing_exists = False
1397 self.extra_template_args = {'drawing_exists': drawing_exists} 1639 self.drawing_exists = drawing_exists
1398 1640
1399 def handle_post(self): 1641 def handle_post(self):
1400 # called from modify UI/POST 1642 # called from modify UI/POST
1401 file_upload = request.files.get('filepath') 1643 file_upload = request.files.get('filepath')
1402 filename = request.form['filename'] 1644 filename = request.form['filename']
1444 title = _('Clickable drawing: %(filename)s', filename=self.name) 1686 title = _('Clickable drawing: %(filename)s', filename=self.name)
1445 return Markup(image_map + u'<img src="{0}" alt="{1}" usemap="#{2}" />'.format(png_url, title, mapid)) 1687 return Markup(image_map + u'<img src="{0}" alt="{1}" usemap="#{2}" />'.format(png_url, title, mapid))
1446 else: 1688 else:
1447 return Markup(u'<img src="{0}" alt="{1}" />'.format(png_url, title)) 1689 return Markup(u'<img src="{0}" alt="{1}" />'.format(png_url, title))
1448 1690
1449 item_registry.register(AnyWikiDraw._factory, Type('application/x-anywikidraw')) 1691 content_registry.register(AnyWikiDraw._factory, Type('application/x-anywikidraw'))
1450 1692
1451 1693
1452 class SvgDraw(Draw): 1694 class SvgDraw(Draw):
1453 """ drawings by svg-edit. It creates two files (svg, png) which are stored as tar file. """ 1695 """ drawings by svg-edit. It creates two files (svg, png) which are stored as tar file. """
1454 modify_help = "" 1696
1455 template = "modify_svg-edit.html" 1697 class ModifyForm(Draw.ModifyForm):
1698 template = "modify_svg-edit.html"
1699 help = ""
1456 1700
1457 def handle_post(self): 1701 def handle_post(self):
1458 # called from modify UI/POST 1702 # called from modify UI/POST
1459 png_upload = request.values.get('png_data') 1703 png_upload = request.values.get('png_data')
1460 svg_upload = request.values.get('filepath') 1704 svg_upload = request.values.get('filepath')
1474 item_name = self.name 1718 item_name = self.name
1475 drawing_url = url_for('frontend.get_item', item_name=item_name, member='drawing.svg', rev=self.rev.revid) 1719 drawing_url = url_for('frontend.get_item', item_name=item_name, member='drawing.svg', rev=self.rev.revid)
1476 png_url = url_for('frontend.get_item', item_name=item_name, member='drawing.png', rev=self.rev.revid) 1720 png_url = url_for('frontend.get_item', item_name=item_name, member='drawing.png', rev=self.rev.revid)
1477 return Markup(u'<img src="{0}" alt="{1}" />'.format(png_url, drawing_url)) 1721 return Markup(u'<img src="{0}" alt="{1}" />'.format(png_url, drawing_url))
1478 1722
1479 item_registry.register(SvgDraw._factory, Type('application/x-svgdraw')) 1723 content_registry.register(SvgDraw._factory, Type('application/x-svgdraw'))