1 # -*- coding: iso-8859-1 -*-
3 MoinMoin - AttachFile action
5 This action lets a page have multiple attachment files.
6 It creates a folder <data>/pages/<pagename>/attachments
7 and keeps everything in there.
9 Form values: action=Attachment
10 1. with no 'do' key: returns file upload form
11 2. do=attach: accept file upload and saves the file in
12 ../attachment/pagename/
13 3. /pagename/fname?action=Attachment&do=get[&mimetype=type]:
14 return contents of the attachment file with the name fname.
15 4. /pathname/fname, do=view[&mimetype=type]:create a page
16 to view the content of the file
18 To link to an attachment, use [[attachment:file.txt]],
19 to embed an attachment, use {{attachment:file.png}}.
21 @copyright: 2001 by Ken Sugino (sugino@mediaone.net),
22 2001-2004 by Juergen Hermann <jh@web.de>,
24 2005 DiegoOngaro at ETSZONE (diego@etszone.com),
27 @license: GNU GPL, see COPYING for details.
30 import os, time, zipfile, mimetypes, errno, datetime
31 from StringIO import StringIO
33 from MoinMoin import log
34 logging = log.getLogger(__name__)
36 from MoinMoin import config, wikiutil, packages
37 from MoinMoin.Page import Page
38 from MoinMoin.util import filesys, timefuncs
39 from MoinMoin.security.textcha import TextCha
40 from MoinMoin.events import FileAttachedEvent, send_event
41 from MoinMoin.support import tarfile
43 action_name = __name__.split('.')[-1]
45 #############################################################################
46 ### External interface - these are called from the core code
47 #############################################################################
49 class AttachmentAlreadyExists(Exception):
53 def getBasePath(request):
54 """ Get base path where page dirs for attachments are stored. """
55 return request.rootpage.getPagePath('pages')
58 def getAttachDir(request, pagename, create=0):
59 """ Get directory where attachments for page `pagename` are stored. """
60 if request.page and pagename == request.page.page_name:
61 page = request.page # reusing existing page obj is faster
63 page = Page(request, pagename)
64 return page.getPagePath("attachments", check_create=create)
67 def absoluteName(url, pagename):
68 """ Get (pagename, filename) of an attachment: link
69 @param url: PageName/filename.ext or filename.ext (unicode)
70 @param pagename: name of the currently processed page (unicode)
71 @rtype: tuple of unicode
72 @return: PageName, filename.ext
74 url = wikiutil.AbsPageName(pagename, url)
75 pieces = url.split(u'/')
77 return pagename, pieces[0]
79 return u"/".join(pieces[:-1]), pieces[-1]
81 def getAttachUrl(pagename, filename, request, addts=0, escaped=0, do='get', drawing='', upload=False):
82 """ Get URL that points to attachment `filename` of page `pagename`. """
85 url = request.href(pagename, rename=wikiutil.taintfilename(filename),
88 url = request.href(pagename, rename=wikiutil.taintfilename(filename),
89 drawing=drawing, action=action_name)
92 url = request.href(pagename, target=filename, action=action_name, do=do)
94 url = request.href(pagename, drawing=drawing, action=action_name)
98 def getIndicator(request, pagename):
99 """ Get an attachment indicator for a page (linked clip image) or
100 an empty string if not attachments exist.
103 attach_dir = getAttachDir(request, pagename)
104 if not os.path.exists(attach_dir):
107 files = os.listdir(attach_dir)
111 fmt = request.formatter
112 attach_count = _('[%d attachments]') % len(files)
113 attach_icon = request.theme.make_icon('attach', vars={'attach_count': attach_count})
114 attach_link = (fmt.url(1, request.href(pagename, action=action_name), rel='nofollow') +
120 def getFilename(request, pagename, filename):
121 """ make complete pathfilename of file "name" attached to some page "pagename"
122 @param request: request object
123 @param pagename: name of page where the file is attached to (unicode)
124 @param filename: filename of attached file (unicode)
125 @rtype: string (in config.charset encoding)
126 @return: complete path/filename of attached file
128 if isinstance(filename, unicode):
129 filename = filename.encode(config.charset)
130 return os.path.join(getAttachDir(request, pagename, create=1), filename)
133 def exists(request, pagename, filename):
134 """ check if page <pagename> has a file <filename> attached """
135 fpath = getFilename(request, pagename, filename)
136 return os.path.exists(fpath)
139 def size(request, pagename, filename):
140 """ return file size of file attachment """
141 fpath = getFilename(request, pagename, filename)
142 return os.path.getsize(fpath)
145 def info(pagename, request):
146 """ Generate snippet with info on the attachment for page `pagename`. """
149 attach_dir = getAttachDir(request, pagename)
151 if os.path.isdir(attach_dir):
152 files = os.listdir(attach_dir)
153 page = Page(request, pagename)
154 link = page.url(request, {'action': action_name})
155 attach_info = _('There are <a href="%(link)s">%(count)s attachment(s)</a> stored for this page.') % {
157 'link': wikiutil.escape(link)
159 return "\n<p>\n%s\n</p>\n" % attach_info
162 def _write_stream(content, stream, bufsize=8192):
163 if hasattr(content, 'read'): # looks file-like
165 shutil.copyfileobj(content, stream, bufsize)
166 elif isinstance(content, str):
167 stream.write(content)
169 logging.error("unsupported content object: %r" % content)
172 def add_attachment(request, pagename, target, filecontent, overwrite=0):
173 """ save <filecontent> to an attachment <target> of page <pagename>
175 filecontent can be either a str (in memory file content),
176 or an open file object (file content in e.g. a tempfile).
180 # replace illegal chars
181 target = wikiutil.taintfilename(target)
183 # get directory, and possibly create it
184 attach_dir = getAttachDir(request, pagename, create=1)
186 fpath = os.path.join(attach_dir, target).encode(config.charset)
187 exists = os.path.exists(fpath)
188 if exists and not overwrite:
189 raise AttachmentAlreadyExists
196 stream = open(fpath, 'wb')
198 _write_stream(filecontent, stream)
202 _addLogEntry(request, 'ATTNEW', pagename, target)
204 filesize = os.path.getsize(fpath)
205 event = FileAttachedEvent(request, pagename, target, filesize)
208 return target, filesize
211 #############################################################################
213 #############################################################################
215 def _addLogEntry(request, action, pagename, filename):
216 """ Add an entry to the edit log on uploads and deletes.
218 `action` should be "ATTNEW" or "ATTDEL"
220 from MoinMoin.logfile import editlog
221 t = wikiutil.timestamp2version(time.time())
222 fname = wikiutil.url_quote(filename)
224 # Write to global log
225 log = editlog.EditLog(request)
226 log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
229 log = editlog.EditLog(request, rootpagename=pagename)
230 log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
233 def _access_file(pagename, request):
234 """ Check form parameter `target` and return a tuple of
235 `(pagename, filename, filepath)` for an existing attachment.
237 Return `(pagename, None, None)` if an error occurs.
242 if not request.values.get('target'):
243 error = _("Filename of attachment not specified!")
245 filename = wikiutil.taintfilename(request.values['target'])
246 fpath = getFilename(request, pagename, filename)
248 if os.path.isfile(fpath):
249 return (pagename, filename, fpath)
250 error = _("Attachment '%(filename)s' does not exist!") % {'filename': filename}
252 error_msg(pagename, request, error)
253 return (pagename, None, None)
256 def _build_filelist(request, pagename, showheader, readonly, mime_type='*'):
258 fmt = request.html_formatter
261 attach_dir = getAttachDir(request, pagename)
262 files = _get_files(request, pagename)
265 files = [fname for fname in files if mime_type == mimetypes.guess_type(fname)[0]]
270 html.append(fmt.rawHTML(_(
271 "To refer to attachments on a page, use '''{{{attachment:filename}}}''', \n"
272 "as shown below in the list of files. \n"
273 "Do '''NOT''' use the URL of the {{{[get]}}} link, \n"
274 "since this is subject to change and can break easily.",
279 label_move = _("move")
281 label_edit = _("edit")
282 label_view = _("view")
283 label_unzip = _("unzip")
284 label_install = _("install")
286 html.append(fmt.bullet_list(1))
288 mt = wikiutil.MimeType(filename=file)
289 fullpath = os.path.join(attach_dir, file).encode(config.charset)
290 st = os.stat(fullpath)
291 base, ext = os.path.splitext(file)
292 parmdict = {'file': wikiutil.escape(file),
293 'fsize': "%.1f" % (float(st.st_size) / 1024),
294 'fmtime': request.user.getFormattedDateTime(st.st_mtime),
298 may_delete = request.user.may.delete(pagename)
299 if may_delete and not readonly:
300 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='del')) +
301 fmt.text(label_del) +
304 if may_delete and not readonly:
305 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='move')) +
306 fmt.text(label_move) +
309 links.append(fmt.url(1, getAttachUrl(pagename, file, request)) +
310 fmt.text(label_get) +
314 links.append(fmt.url(1, getAttachUrl(pagename, file, request, drawing=base)) +
315 fmt.text(label_edit) +
318 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
319 fmt.text(label_view) +
323 is_zipfile = zipfile.is_zipfile(fullpath)
325 is_package = packages.ZipPackage(request, fullpath).isPackage()
326 if is_package and request.user.isSuperUser():
327 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='install')) +
328 fmt.text(label_install) +
330 elif (not is_package and mt.minor == 'zip' and
332 request.user.may.read(pagename) and
333 request.user.may.write(pagename)):
334 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='unzip')) +
335 fmt.text(label_unzip) +
338 # We don't want to crash with a traceback here (an exception
339 # here could be caused by an uploaded defective zip file - and
340 # if we crash here, the user does not get a UI to remove the
341 # defective zip file again).
342 # RuntimeError is raised by zipfile stdlib module in case of
343 # problems (like inconsistent slash and backslash usage in the
345 logging.exception("An exception within zip file attachment handling occurred:")
347 html.append(fmt.listitem(1))
348 html.append("[%s]" % " | ".join(links))
349 html.append(" (%(fmtime)s, %(fsize)s KB) [[attachment:%(file)s]]" % parmdict)
350 html.append(fmt.listitem(0))
351 html.append(fmt.bullet_list(0))
355 html.append(fmt.paragraph(1))
356 html.append(fmt.text(_("No attachments stored for %(pagename)s") % {
357 'pagename': pagename}))
358 html.append(fmt.paragraph(0))
363 def _get_files(request, pagename):
364 attach_dir = getAttachDir(request, pagename)
365 if os.path.isdir(attach_dir):
366 files = [fn.decode(config.charset) for fn in os.listdir(attach_dir)]
373 def _get_filelist(request, pagename):
374 return _build_filelist(request, pagename, 1, 0)
377 def error_msg(pagename, request, msg):
378 request.theme.add_msg(msg, "error")
379 Page(request, pagename).send_page()
382 #############################################################################
383 ### Create parts of the Web interface
384 #############################################################################
386 def send_link_rel(request, pagename):
387 files = _get_files(request, pagename)
389 url = getAttachUrl(pagename, fname, request, do='view', escaped=1)
390 request.write(u'<link rel="Appendix" title="%s" href="%s">\n' % (
391 wikiutil.escape(fname, 1), url))
394 def send_hotdraw(pagename, request):
398 pubpath = request.cfg.url_prefix_static + "/applets/TWikiDrawPlugin"
399 basename = request.values['drawing']
400 ci = ContainerItem(request, pagename, basename + '.tdraw')
401 drawpath = ci.member_url(basename + '.draw')
402 pngpath = ci.member_url(basename + '.png')
403 pagelink = request.href(pagename, action=action_name, ts=now)
404 helplink = Page(request, "HelpOnActions/AttachFile").url(request)
405 savelink = request.href(pagename, action=action_name, do='savedrawing')
406 #savelink = Page(request, pagename).url(request) # XXX include target filename param here for twisted
407 # request, {'savename': request.values['drawing']+'.draw'}
408 #savelink = '/cgi-bin/dumpform.bat'
410 timestamp = '&ts=%s' % now
412 request.write('<h2>' + _("Edit drawing") + '</h2>')
415 <img src="%(pngpath)s%(timestamp)s">
416 <applet code="CH.ifa.draw.twiki.TWikiDraw.class"
417 archive="%(pubpath)s/twikidraw.jar" width="640" height="480">
418 <param name="drawpath" value="%(drawpath)s">
419 <param name="pngpath" value="%(pngpath)s">
420 <param name="savepath" value="%(savelink)s">
421 <param name="basename" value="%(basename)s">
422 <param name="viewpath" value="%(pagelink)s">
423 <param name="helppath" value="%(helplink)s">
424 <strong>NOTE:</strong> You need a Java enabled browser to edit the drawing example.
427 'pngpath': pngpath, 'timestamp': timestamp,
428 'pubpath': pubpath, 'drawpath': drawpath,
429 'savelink': savelink, 'pagelink': pagelink, 'helplink': helplink,
430 'basename': wikiutil.escape(basename, 1),
434 def send_uploadform(pagename, request):
435 """ Send the HTML code for the list of already stored attachments and
436 the file upload form.
440 if not request.user.may.read(pagename):
441 request.write('<p>%s</p>' % _('You are not allowed to view this page.'))
444 writeable = request.user.may.write(pagename)
446 # First send out the upload new attachment form on top of everything else.
447 # This avoids usability issues if you have to scroll down a lot to upload
448 # a new file when the page already has lots of attachments:
450 request.write('<h2>' + _("New Attachment") + '</h2>')
452 <form action="%(url)s" method="POST" enctype="multipart/form-data">
454 <dt>%(upload_label_file)s</dt>
455 <dd><input type="file" name="file" size="50"></dd>
456 <dt>%(upload_label_rename)s</dt>
457 <dd><input type="text" name="rename" size="50" value="%(rename)s"></dd>
458 <dt>%(upload_label_overwrite)s</dt>
459 <dd><input type="checkbox" name="overwrite" value="1" %(overwrite_checked)s></dd>
463 <input type="hidden" name="action" value="%(action_name)s">
464 <input type="hidden" name="do" value="upload">
465 <input type="submit" value="%(upload_button)s">
469 'url': request.href(pagename),
470 'action_name': action_name,
471 'upload_label_file': _('File to upload'),
472 'upload_label_rename': _('Rename to'),
473 'rename': wikiutil.escape(request.form.get('rename', ''), 1),
474 'upload_label_overwrite': _('Overwrite existing attachment of same name'),
475 'overwrite_checked': ('', 'checked')[request.form.get('overwrite', '0') == '1'],
476 'upload_button': _('Upload'),
477 'textcha': TextCha(request).render(),
480 request.write('<h2>' + _("Attached Files") + '</h2>')
481 request.write(_get_filelist(request, pagename))
484 request.write('<p>%s</p>' % _('You are not allowed to attach a file to this page.'))
486 if writeable and request.values.get('drawing'):
487 send_hotdraw(pagename, request)
490 #############################################################################
491 ### Web interface for file upload, viewing and deletion
492 #############################################################################
494 def execute(pagename, request):
495 """ Main dispatcher for the 'AttachFile' action. """
498 do = request.values.get('do', 'upload_form')
499 handler = globals().get('_do_%s' % do)
501 msg = handler(pagename, request)
503 msg = _('Unsupported AttachFile sub-action: %s') % wikiutil.escape(do)
505 error_msg(pagename, request, msg)
508 def _do_upload_form(pagename, request):
509 upload_form(pagename, request)
512 def upload_form(pagename, request, msg=''):
515 # Use user interface language for this generated page
516 request.setContentLanguage(request.lang)
517 request.theme.add_msg(msg, "dialog")
518 request.theme.send_title(_('Attachments for "%(pagename)s"') % {'pagename': pagename}, pagename=pagename)
519 request.write('<div id="content">\n') # start content div
520 send_uploadform(pagename, request)
521 request.write('</div>\n') # end content div
522 request.theme.send_footer(pagename)
523 request.theme.send_closing_html()
526 def preprocess_filename(filename):
527 """ preprocess the filename we got from upload form,
528 strip leading drive and path (IE misbehaviour)
530 if filename and len(filename) > 1 and (filename[1] == ':' or filename[0] == '\\'): # C:.... or \path... or \\server\...
531 bsindex = filename.rfind('\\')
533 filename = filename[bsindex+1:]
537 def _do_upload(pagename, request):
539 # Currently we only check TextCha for upload (this is what spammers ususally do),
540 # but it could be extended to more/all attachment write access
541 if not TextCha(request).check_answer_from_form():
542 return _('TextCha: Wrong answer! Go back and try again...')
546 file_upload = request.files.get('file')
548 # This might happen when trying to upload file names
549 # with non-ascii characters on Safari.
550 return _("No file content. Delete non ASCII characters from the file name and try again.")
553 overwrite = int(form.get('overwrite', '0'))
557 if not request.user.may.write(pagename):
558 return _('You are not allowed to attach a file to this page.')
560 if overwrite and not request.user.may.delete(pagename):
561 return _('You are not allowed to overwrite a file attachment of this page.')
563 rename = form.get('rename', u'').strip()
567 target = file_upload.filename
569 target = wikiutil.clean_input(target)
572 return _("Filename of attachment not specified!")
576 target, bytes = add_attachment(request, pagename, target, file_upload.stream, overwrite=overwrite)
577 msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
578 " with %(bytes)d bytes saved.") % {
579 'target': target, 'filename': file_upload.filename, 'bytes': bytes}
580 except AttachmentAlreadyExists:
581 msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
582 'target': target, 'filename': file_upload.filename}
584 # return attachment list
585 upload_form(pagename, request, msg)
589 """ A storage container (multiple objects in 1 tarfile) """
591 def __init__(self, request, pagename, containername):
592 self.request = request
593 self.pagename = pagename
594 self.containername = containername
595 self.container_filename = getFilename(request, pagename, containername)
597 def member_url(self, member):
598 """ return URL for accessing container member
599 (we use same URL for get (GET) and put (POST))
601 url = Page(self.request, self.pagename).url(self.request, {
602 'action': 'AttachFile',
603 'do': 'box', # shorter to type than 'container'
604 'target': self.containername,
607 return url + '&member=%s' % member
608 # member needs to be last in qs because twikidraw looks for "file extension" at the end
610 def get(self, member):
611 """ return a file-like object with the member file data
613 tf = tarfile.TarFile(self.container_filename)
614 return tf.extractfile(member)
616 def put(self, member, content, content_length=None):
617 """ save data into a container's member """
618 tf = tarfile.TarFile(self.container_filename, mode='a')
619 if isinstance(member, unicode):
620 member = member.encode('utf-8')
621 ti = tarfile.TarInfo(member)
622 if isinstance(content, str):
623 if content_length is None:
624 content_length = len(content)
625 content = StringIO(content) # we need a file obj
626 elif not hasattr(content, 'read'):
627 logging.error("unsupported content object: %r" % content)
629 assert content_length >= 0 # we don't want -1 interpreted as 4G-1
630 ti.size = content_length
631 tf.addfile(ti, content)
635 f = open(self.container_filename, 'w')
639 return os.path.exists(self.container_filename)
641 def _do_savedrawing(pagename, request):
644 if not request.user.may.write(pagename):
645 return _('You are not allowed to save a drawing on this page.')
647 file_upload = request.files.get('filepath')
649 # This might happen when trying to upload file names
650 # with non-ascii characters on Safari.
651 return _("No file content. Delete non ASCII characters from the file name and try again.")
653 filename = request.form['filename']
654 basepath, basename = os.path.split(filename)
655 basename, ext = os.path.splitext(basename)
657 ci = ContainerItem(request, pagename, basename + '.tdraw')
658 filecontent = file_upload.stream
659 content_length = None
660 if ext == '.draw': # TWikiDraw POSTs this first
661 _addLogEntry(request, 'ATTDRW', pagename, basename + '.tdraw')
663 filecontent = filecontent.read() # read file completely into memory
664 filecontent = filecontent.replace("\r", "")
666 # touch attachment directory to invalidate cache if new map is saved
667 attach_dir = getAttachDir(request, pagename)
668 os.utime(attach_dir, None)
669 filecontent = filecontent.read() # read file completely into memory
670 filecontent = filecontent.strip()
672 #content_length = file_upload.content_length
673 # XXX gives -1 for wsgiref :( If this is fixed, we could use the file obj,
674 # without reading it into memory completely:
675 filecontent = filecontent.read()
677 ci.put(basename + ext, filecontent, content_length)
682 def _do_del(pagename, request):
685 pagename, filename, fpath = _access_file(pagename, request)
686 if not request.user.may.delete(pagename):
687 return _('You are not allowed to delete attachments on this page.')
689 return # error msg already sent in _access_file
693 _addLogEntry(request, 'ATTDEL', pagename, filename)
695 if request.cfg.xapian_search:
696 from MoinMoin.search.Xapian import Index
697 index = Index(request)
699 index.remove_item(pagename, filename)
701 upload_form(pagename, request, msg=_("Attachment '%(filename)s' deleted.") % {'filename': filename})
704 def move_file(request, pagename, new_pagename, attachment, new_attachment):
707 newpage = Page(request, new_pagename)
708 if newpage.exists(includeDeleted=1) and request.user.may.write(new_pagename) and request.user.may.delete(pagename):
709 new_attachment_path = os.path.join(getAttachDir(request, new_pagename,
710 create=1), new_attachment).encode(config.charset)
711 attachment_path = os.path.join(getAttachDir(request, pagename),
712 attachment).encode(config.charset)
714 if os.path.exists(new_attachment_path):
715 upload_form(pagename, request,
716 msg=_("Attachment '%(new_pagename)s/%(new_filename)s' already exists.") % {
717 'new_pagename': new_pagename,
718 'new_filename': new_attachment})
721 if new_attachment_path != attachment_path:
723 filesys.rename(attachment_path, new_attachment_path)
724 _addLogEntry(request, 'ATTDEL', pagename, attachment)
725 _addLogEntry(request, 'ATTNEW', new_pagename, new_attachment)
726 upload_form(pagename, request,
727 msg=_("Attachment '%(pagename)s/%(filename)s' moved to '%(new_pagename)s/%(new_filename)s'.") % {
728 'pagename': pagename,
729 'filename': attachment,
730 'new_pagename': new_pagename,
731 'new_filename': new_attachment})
733 upload_form(pagename, request, msg=_("Nothing changed"))
735 upload_form(pagename, request, msg=_("Page '%(new_pagename)s' does not exist or you don't have enough rights.") % {
736 'new_pagename': new_pagename})
739 def _do_attachment_move(pagename, request):
742 if 'cancel' in request.form:
743 return _('Move aborted!')
744 if not wikiutil.checkTicket(request, request.form['ticket']):
745 return _('Please use the interactive user interface to move attachments!')
746 if not request.user.may.delete(pagename):
747 return _('You are not allowed to move attachments from this page.')
749 if 'newpagename' in request.form:
750 new_pagename = request.form.get('newpagename')
752 upload_form(pagename, request, msg=_("Move aborted because new page name is empty."))
753 if 'newattachmentname' in request.form:
754 new_attachment = request.form.get('newattachmentname')
755 if new_attachment != wikiutil.taintfilename(new_attachment):
756 upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
757 'filename': new_attachment})
760 upload_form(pagename, request, msg=_("Move aborted because new attachment name is empty."))
762 attachment = request.form.get('oldattachmentname')
763 move_file(request, pagename, new_pagename, attachment, new_attachment)
766 def _do_move(pagename, request):
769 pagename, filename, fpath = _access_file(pagename, request)
770 if not request.user.may.delete(pagename):
771 return _('You are not allowed to move attachments from this page.')
773 return # error msg already sent in _access_file
776 d = {'action': action_name,
777 'url': request.href(pagename),
778 'do': 'attachment_move',
779 'ticket': wikiutil.createTicket(request),
780 'pagename': pagename,
781 'attachment_name': filename,
783 'cancel': _('Cancel'),
784 'newname_label': _("New page name"),
785 'attachment_label': _("New attachment name"),
788 <form action="%(url)s" method="POST">
789 <input type="hidden" name="action" value="%(action)s">
790 <input type="hidden" name="do" value="%(do)s">
791 <input type="hidden" name="ticket" value="%(ticket)s">
794 <td class="label"><label>%(newname_label)s</label></td>
796 <input type="text" name="newpagename" value="%(pagename)s" size="80">
800 <td class="label"><label>%(attachment_label)s</label></td>
802 <input type="text" name="newattachmentname" value="%(attachment_name)s" size="80">
808 <input type="hidden" name="oldattachmentname" value="%(attachment_name)s">
809 <input type="submit" name="move" value="%(move)s">
810 <input type="submit" name="cancel" value="%(cancel)s">
815 thispage = Page(request, pagename)
816 request.theme.add_msg(formhtml, "dialog")
817 return thispage.send_page()
820 def _do_box(pagename, request):
823 pagename, filename, fpath = _access_file(pagename, request)
824 if not request.user.may.read(pagename):
825 return _('You are not allowed to get attachments from this page.')
827 return # error msg already sent in _access_file
829 timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
830 if_modified = request.if_modified_since
831 if if_modified and if_modified >= timestamp:
832 request.status_code = 304
834 ci = ContainerItem(request, pagename, filename)
835 filename = wikiutil.taintfilename(request.values['member'])
836 mt = wikiutil.MimeType(filename=filename)
837 content_type = mt.content_type()
838 mime_type = mt.mime_type()
840 # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
841 # There is no solution that is compatible to IE except stripping non-ascii chars
842 filename_enc = filename.encode(config.charset)
844 # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
845 # we just let the user store them to disk ('attachment').
846 # For safe files, we directly show them inline (this also works better for IE).
847 dangerous = mime_type in request.cfg.mimetypes_xss_protect
848 content_dispo = dangerous and 'attachment' or 'inline'
850 request.content_type = content_type
851 request.last_modified = timestamp
852 #request.content_length = os.path.getsize(fpath)
853 content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
854 request.headers.add('Content-Disposition', content_dispo_string)
857 request.send_file(ci.get(filename))
860 def _do_get(pagename, request):
863 pagename, filename, fpath = _access_file(pagename, request)
864 if not request.user.may.read(pagename):
865 return _('You are not allowed to get attachments from this page.')
867 return # error msg already sent in _access_file
869 timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
870 if_modified = request.if_modified_since
871 if if_modified and if_modified >= timestamp:
872 request.status_code = 304
874 mt = wikiutil.MimeType(filename=filename)
875 content_type = mt.content_type()
876 mime_type = mt.mime_type()
878 # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
879 # There is no solution that is compatible to IE except stripping non-ascii chars
880 filename_enc = filename.encode(config.charset)
882 # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
883 # we just let the user store them to disk ('attachment').
884 # For safe files, we directly show them inline (this also works better for IE).
885 dangerous = mime_type in request.cfg.mimetypes_xss_protect
886 content_dispo = dangerous and 'attachment' or 'inline'
888 request.content_type = content_type
889 request.last_modified = timestamp
890 request.content_length = os.path.getsize(fpath)
891 content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
892 request.headers.add('Content-Disposition', content_dispo_string)
895 request.send_file(open(fpath, 'rb'))
898 def _do_install(pagename, request):
901 pagename, target, targetpath = _access_file(pagename, request)
902 if not request.user.isSuperUser():
903 return _('You are not allowed to install files.')
907 package = packages.ZipPackage(request, targetpath)
909 if package.isPackage():
910 if package.installPackage():
911 msg = _("Attachment '%(filename)s' installed.") % {'filename': wikiutil.escape(target)}
913 msg = _("Installation of '%(filename)s' failed.") % {'filename': wikiutil.escape(target)}
915 msg += "<br><pre>%s</pre>" % wikiutil.escape(package.msg)
917 msg = _('The file %s is not a MoinMoin package file.') % wikiutil.escape(target)
919 upload_form(pagename, request, msg=msg)
922 def _do_unzip(pagename, request, overwrite=False):
924 pagename, filename, fpath = _access_file(pagename, request)
926 if not (request.user.may.delete(pagename) and request.user.may.read(pagename) and request.user.may.write(pagename)):
927 return _('You are not allowed to unzip attachments of this page.')
930 return # error msg already sent in _access_file
933 if not zipfile.is_zipfile(fpath):
934 return _('The file %(filename)s is not a .zip file.') % {'filename': filename}
936 # determine how which attachment names we have and how much space each is occupying
937 curr_fsizes = dict([(f, size(request, pagename, f)) for f in _get_files(request, pagename)])
939 # Checks for the existance of one common prefix path shared among
940 # all files in the zip file. If this is the case, remove the common prefix.
941 # We also prepare a dict of the new filenames->filesizes.
942 zip_path_sep = '/' # we assume '/' is as zip standard suggests
946 zf = zipfile.ZipFile(fpath)
947 for zi in zf.infolist():
949 if not name.endswith(zip_path_sep): # a file (not a directory)
950 if fname_index is None:
951 fname_index = name.rfind(zip_path_sep) + 1
952 path = name[:fname_index]
953 if (name.rfind(zip_path_sep) + 1 != fname_index # different prefix len
955 name[:fname_index] != path): # same len, but still different
956 mapping = [] # zip is not acceptable
958 if zi.file_size >= request.cfg.unzip_single_file_size: # file too big
959 mapping = [] # zip is not acceptable
961 finalname = name[fname_index:] # remove common path prefix
962 finalname = finalname.decode(config.charset, 'replace') # replaces trash with \uFFFD char
963 mapping.append((name, finalname))
964 new_fsizes[finalname] = zi.file_size
966 # now we either have an empty mapping (if the zip is not acceptable),
967 # an identity mapping (no subdirs in zip, just all flat), or
968 # a mapping (origname, finalname) where origname is the zip member filename
969 # (including some prefix path) and finalname is a simple filename.
971 # calculate resulting total file size / count after unzipping:
973 curr_fsizes.update(new_fsizes)
976 new_fsizes.update(curr_fsizes)
978 total_count = len(total)
979 total_size = sum(total.values())
982 msg = _("Attachment '%(filename)s' not unzipped because some files in the zip "
983 "are either not in the same directory or exceeded the single file size limit (%(maxsize_file)d kB)."
984 ) % {'filename': filename,
985 'maxsize_file': request.cfg.unzip_single_file_size / 1000, }
986 elif total_size > request.cfg.unzip_attachments_space:
987 msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
988 "the per page attachment storage size limit (%(size)d kB).") % {
989 'filename': filename,
990 'size': request.cfg.unzip_attachments_space / 1000, }
991 elif total_count > request.cfg.unzip_attachments_count:
992 msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
993 "the per page attachment count limit (%(count)d).") % {
994 'filename': filename,
995 'count': request.cfg.unzip_attachments_count, }
998 for origname, finalname in mapping:
1000 # Note: reads complete zip member file into memory. ZipFile does not offer block-wise reading:
1001 add_attachment(request, pagename, finalname, zf.read(origname), overwrite)
1002 except AttachmentAlreadyExists:
1003 not_overwritten.append(finalname)
1005 msg = _("Attachment '%(filename)s' partially unzipped (did not overwrite: %(filelist)s).") % {
1006 'filename': filename,
1007 'filelist': ', '.join(not_overwritten), }
1009 msg = _("Attachment '%(filename)s' unzipped.") % {'filename': filename}
1010 except RuntimeError, err:
1011 # We don't want to crash with a traceback here (an exception
1012 # here could be caused by an uploaded defective zip file - and
1013 # if we crash here, the user does not get a UI to remove the
1014 # defective zip file again).
1015 # RuntimeError is raised by zipfile stdlib module in case of
1016 # problems (like inconsistent slash and backslash usage in the
1018 logging.exception("An exception within zip file attachment handling occurred:")
1019 msg = _("A severe error occurred:") + ' ' + str(err)
1021 upload_form(pagename, request, msg=wikiutil.escape(msg))
1024 def send_viewfile(pagename, request):
1026 fmt = request.html_formatter
1028 pagename, filename, fpath = _access_file(pagename, request)
1032 request.write('<h2>' + _("Attachment '%(filename)s'") % {'filename': filename} + '</h2>')
1033 # show a download link above the content
1034 label = _('Download')
1035 link = (fmt.url(1, getAttachUrl(pagename, filename, request, do='get'), css_class="download") +
1038 request.write('%s<br><br>' % link)
1040 mt = wikiutil.MimeType(filename=filename)
1042 # destinguishs if browser need a plugin in place
1043 if mt.major == 'image' and mt.minor in config.browser_supported_images:
1044 request.write('<img src="%s" alt="%s">' % (
1045 getAttachUrl(pagename, filename, request, escaped=1),
1046 wikiutil.escape(filename, 1)))
1048 elif mt.major == 'text':
1049 ext = os.path.splitext(filename)[1]
1050 Parser = wikiutil.getParserForExtension(request.cfg, ext)
1051 if Parser is not None:
1053 content = file(fpath, 'r').read()
1054 content = wikiutil.decodeUnknownInput(content)
1055 colorizer = Parser(content, request, filename=filename)
1056 colorizer.format(request.formatter)
1061 request.write(request.formatter.preformatted(1))
1062 # If we have text but no colorizing parser we try to decode file contents.
1063 content = open(fpath, 'r').read()
1064 content = wikiutil.decodeUnknownInput(content)
1065 content = wikiutil.escape(content)
1066 request.write(request.formatter.text(content))
1067 request.write(request.formatter.preformatted(0))
1071 package = packages.ZipPackage(request, fpath)
1072 if package.isPackage():
1073 request.write("<pre><b>%s</b>\n%s</pre>" % (_("Package script:"), wikiutil.escape(package.getScript())))
1076 if zipfile.is_zipfile(fpath) and mt.minor == 'zip':
1077 zf = zipfile.ZipFile(fpath, mode='r')
1078 request.write("<pre>%-46s %19s %12s\n" % (_("File Name"), _("Modified")+" "*5, _("Size")))
1079 for zinfo in zf.filelist:
1080 date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time
1081 request.write(wikiutil.escape("%-46s %s %12d\n" % (zinfo.filename, date, zinfo.file_size)))
1082 request.write("</pre>")
1084 except RuntimeError:
1085 # We don't want to crash with a traceback here (an exception
1086 # here could be caused by an uploaded defective zip file - and
1087 # if we crash here, the user does not get a UI to remove the
1088 # defective zip file again).
1089 # RuntimeError is raised by zipfile stdlib module in case of
1090 # problems (like inconsistent slash and backslash usage in the
1092 logging.exception("An exception within zip file attachment handling occurred:")
1095 from MoinMoin import macro
1096 from MoinMoin.parser.text import Parser
1098 macro.request = request
1099 macro.formatter = request.html_formatter
1100 p = Parser("##\n", request)
1103 # use EmbedObject to view valid mime types
1105 request.write('<p>' + _("Unknown file type, cannot display this attachment inline.") + '</p>')
1106 link = (fmt.url(1, getAttachUrl(pagename, filename, request)) +
1107 fmt.text(filename) +
1109 request.write('For using an external program follow this link %s' % link)
1111 request.write(m.execute('EmbedObject', u'target=%s, pagename=%s' % (filename, pagename)))
1115 def _do_view(pagename, request):
1118 orig_pagename = pagename
1119 pagename, filename, fpath = _access_file(pagename, request)
1120 if not request.user.may.read(pagename):
1121 return _('You are not allowed to view attachments of this page.')
1125 # send header & title
1126 # Use user interface language for this generated page
1127 request.setContentLanguage(request.lang)
1128 title = _('attachment:%(filename)s of %(pagename)s') % {
1129 'filename': filename, 'pagename': pagename}
1130 request.theme.send_title(title, pagename=pagename)
1133 request.write(request.formatter.startContent())
1134 send_viewfile(orig_pagename, request)
1135 send_uploadform(pagename, request)
1136 request.write(request.formatter.endContent())
1138 request.theme.send_footer(pagename)
1139 request.theme.send_closing_html()
1142 #############################################################################
1143 ### File attachment administration
1144 #############################################################################
1146 def do_admin_browser(request):
1147 """ Browser for SystemAdmin macro. """
1148 from MoinMoin.util.dataset import TupleDataset, Column
1151 data = TupleDataset()
1153 Column('page', label=('Page')),
1154 Column('file', label=('Filename')),
1155 Column('size', label=_('Size'), align='right'),
1158 # iterate over pages that might have attachments
1159 pages = request.rootpage.getPageList()
1160 for pagename in pages:
1161 # check for attachments directory
1162 page_dir = getAttachDir(request, pagename)
1163 if os.path.isdir(page_dir):
1164 # iterate over files of the page
1165 files = os.listdir(page_dir)
1166 for filename in files:
1167 filepath = os.path.join(page_dir, filename)
1169 Page(request, pagename).link_to(request, querystr="action=AttachFile"),
1170 wikiutil.escape(filename.decode(config.charset)),
1171 os.path.getsize(filepath),
1175 from MoinMoin.widget.browser import DataBrowserWidget
1177 browser = DataBrowserWidget(request)
1178 browser.setData(data)
1179 return browser.render(method="GET")