simplify auto scroll initialization; fix bug in IE init discovered when using IE7 on pages with wide tables
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, errno, datetime
31 from StringIO import StringIO
33 from werkzeug import http_date
35 from MoinMoin import log
36 logging = log.getLogger(__name__)
38 # keep both imports below as they are, order is important:
39 from MoinMoin import wikiutil
42 from MoinMoin import config, packages
43 from MoinMoin.Page import Page
44 from MoinMoin.util import filesys, timefuncs
45 from MoinMoin.security.textcha import TextCha
46 from MoinMoin.events import FileAttachedEvent, FileRemovedEvent, send_event
47 from MoinMoin.support import tarfile
49 action_name = __name__.split('.')[-1]
51 #############################################################################
52 ### External interface - these are called from the core code
53 #############################################################################
55 class AttachmentAlreadyExists(Exception):
59 def getBasePath(request):
60 """ Get base path where page dirs for attachments are stored. """
61 return request.rootpage.getPagePath('pages')
64 def getAttachDir(request, pagename, create=0):
65 """ Get directory where attachments for page `pagename` are stored. """
66 if request.page and pagename == request.page.page_name:
67 page = request.page # reusing existing page obj is faster
69 page = Page(request, pagename)
70 return page.getPagePath("attachments", check_create=create)
73 def absoluteName(url, pagename):
74 """ Get (pagename, filename) of an attachment: link
75 @param url: PageName/filename.ext or filename.ext (unicode)
76 @param pagename: name of the currently processed page (unicode)
77 @rtype: tuple of unicode
78 @return: PageName, filename.ext
80 url = wikiutil.AbsPageName(pagename, url)
81 pieces = url.split(u'/')
83 return pagename, pieces[0]
85 return u"/".join(pieces[:-1]), pieces[-1]
88 def get_action(request, filename, do):
89 generic_do_mapping = {
96 'install': action_name,
97 'upload_form': action_name,
99 basename, ext = os.path.splitext(filename)
100 do_mapping = request.cfg.extensions_mapping.get(ext, {})
101 action = do_mapping.get(do, None)
103 # we have no special support for this,
104 # look up whether we have generic support:
105 action = generic_do_mapping.get(do, None)
109 def getAttachUrl(pagename, filename, request, addts=0, do='get'):
110 """ Get URL that points to attachment `filename` of page `pagename`.
111 For upload url, call with do='upload_form'.
112 Returns the URL to do the specified "do" action or None,
113 if this action is not supported.
115 action = get_action(request, filename, do)
117 args = dict(action=action, do=do, target=filename)
118 if do not in ['get', 'view', # harmless
119 'modify', # just renders the applet html, which has own ticket
120 'move', # renders rename form, which has own ticket
122 # create a ticket for the not so harmless operations
123 # we need action= here because the current action (e.g. "show" page
124 # with a macro AttachList) may not be the linked-to action, e.g.
125 # "AttachFile". Also, AttachList can list attachments of another page,
126 # thus we need to give pagename= also.
127 args['ticket'] = wikiutil.createTicket(request,
128 pagename=pagename, action=action_name)
129 url = request.href(pagename, **args)
133 def getIndicator(request, pagename):
134 """ Get an attachment indicator for a page (linked clip image) or
135 an empty string if not attachments exist.
138 attach_dir = getAttachDir(request, pagename)
139 if not os.path.exists(attach_dir):
142 files = os.listdir(attach_dir)
146 fmt = request.formatter
147 attach_count = _('[%d attachments]') % len(files)
148 attach_icon = request.theme.make_icon('attach', vars={'attach_count': attach_count})
149 attach_link = (fmt.url(1, request.href(pagename, action=action_name), rel='nofollow') +
155 def getFilename(request, pagename, filename):
156 """ make complete pathfilename of file "name" attached to some page "pagename"
157 @param request: request object
158 @param pagename: name of page where the file is attached to (unicode)
159 @param filename: filename of attached file (unicode)
160 @rtype: string (in config.charset encoding)
161 @return: complete path/filename of attached file
163 if isinstance(filename, unicode):
164 filename = filename.encode(config.charset)
165 return os.path.join(getAttachDir(request, pagename, create=1), filename)
168 def exists(request, pagename, filename):
169 """ check if page <pagename> has a file <filename> attached """
170 fpath = getFilename(request, pagename, filename)
171 return os.path.exists(fpath)
174 def size(request, pagename, filename):
175 """ return file size of file attachment """
176 fpath = getFilename(request, pagename, filename)
177 return os.path.getsize(fpath)
180 def info(pagename, request):
181 """ Generate snippet with info on the attachment for page `pagename`. """
184 attach_dir = getAttachDir(request, pagename)
186 if os.path.isdir(attach_dir):
187 files = os.listdir(attach_dir)
188 page = Page(request, pagename)
189 link = page.url(request, {'action': action_name})
190 attach_info = _('There are <a href="%(link)s">%(count)s attachment(s)</a> stored for this page.') % {
192 'link': wikiutil.escape(link)
194 return "\n<p>\n%s\n</p>\n" % attach_info
197 def _write_stream(content, stream, bufsize=8192):
198 if hasattr(content, 'read'): # looks file-like
200 shutil.copyfileobj(content, stream, bufsize)
201 elif isinstance(content, str):
202 stream.write(content)
204 logging.error("unsupported content object: %r" % content)
207 def add_attachment(request, pagename, target, filecontent, overwrite=0):
208 """ save <filecontent> to an attachment <target> of page <pagename>
210 filecontent can be either a str (in memory file content),
211 or an open file object (file content in e.g. a tempfile).
213 # replace illegal chars
214 target = wikiutil.taintfilename(target)
216 # get directory, and possibly create it
217 attach_dir = getAttachDir(request, pagename, create=1)
218 fpath = os.path.join(attach_dir, target).encode(config.charset)
220 exists = os.path.exists(fpath)
223 remove_attachment(request, pagename, target)
225 raise AttachmentAlreadyExists
228 stream = open(fpath, 'wb')
230 _write_stream(filecontent, stream)
234 _addLogEntry(request, 'ATTNEW', pagename, target)
236 filesize = os.path.getsize(fpath)
237 event = FileAttachedEvent(request, pagename, target, filesize)
240 return target, filesize
243 def remove_attachment(request, pagename, target):
244 """ remove attachment <target> of page <pagename>
246 # replace illegal chars
247 target = wikiutil.taintfilename(target)
249 # get directory, do not create it
250 attach_dir = getAttachDir(request, pagename, create=0)
252 fpath = os.path.join(attach_dir, target).encode(config.charset)
254 filesize = os.path.getsize(fpath)
257 # either it is gone already or we have no rights - not much we can do about it
260 _addLogEntry(request, 'ATTDEL', pagename, target)
262 event = FileRemovedEvent(request, pagename, target, filesize)
265 return target, filesize
268 #############################################################################
270 #############################################################################
272 def _addLogEntry(request, action, pagename, filename):
273 """ Add an entry to the edit log on uploads and deletes.
275 `action` should be "ATTNEW" or "ATTDEL"
277 from MoinMoin.logfile import editlog
278 t = wikiutil.timestamp2version(time.time())
279 fname = wikiutil.url_quote(filename)
281 # Write to global log
282 log = editlog.EditLog(request)
283 log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
286 log = editlog.EditLog(request, rootpagename=pagename)
287 log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
290 def _access_file(pagename, request):
291 """ Check form parameter `target` and return a tuple of
292 `(pagename, filename, filepath)` for an existing attachment.
294 Return `(pagename, None, None)` if an error occurs.
299 if not request.values.get('target'):
300 error = _("Filename of attachment not specified!")
302 filename = wikiutil.taintfilename(request.values['target'])
303 fpath = getFilename(request, pagename, filename)
305 if os.path.isfile(fpath):
306 return (pagename, filename, fpath)
307 error = _("Attachment '%(filename)s' does not exist!") % {'filename': filename}
309 error_msg(pagename, request, error)
310 return (pagename, None, None)
313 def _build_filelist(request, pagename, showheader, readonly, mime_type='*'):
315 fmt = request.html_formatter
318 attach_dir = getAttachDir(request, pagename)
319 files = _get_files(request, pagename)
322 files = [fname for fname in files if mime_type == mimetypes.guess_type(fname)[0]]
327 html.append(fmt.rawHTML(_(
328 "To refer to attachments on a page, use '''{{{attachment:filename}}}''', \n"
329 "as shown below in the list of files. \n"
330 "Do '''NOT''' use the URL of the {{{[get]}}} link, \n"
331 "since this is subject to change and can break easily.",
336 label_move = _("move")
338 label_edit = _("edit")
339 label_view = _("view")
340 label_unzip = _("unzip")
341 label_install = _("install")
343 may_read = request.user.may.read(pagename)
344 may_write = request.user.may.write(pagename)
345 may_delete = request.user.may.delete(pagename)
347 html.append(fmt.bullet_list(1))
349 mt = wikiutil.MimeType(filename=file)
350 fullpath = os.path.join(attach_dir, file).encode(config.charset)
351 st = os.stat(fullpath)
352 base, ext = os.path.splitext(file)
353 parmdict = {'file': wikiutil.escape(file),
354 'fsize': "%.1f" % (float(st.st_size) / 1024),
355 'fmtime': request.user.getFormattedDateTime(st.st_mtime),
359 if may_delete and not readonly:
360 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='del')) +
361 fmt.text(label_del) +
364 if may_delete and not readonly:
365 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='move')) +
366 fmt.text(label_move) +
369 links.append(fmt.url(1, getAttachUrl(pagename, file, request)) +
370 fmt.text(label_get) +
373 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
374 fmt.text(label_view) +
377 if may_write and not readonly:
378 edit_url = getAttachUrl(pagename, file, request, do='modify')
380 links.append(fmt.url(1, edit_url) +
381 fmt.text(label_edit) +
385 is_zipfile = zipfile.is_zipfile(fullpath)
386 if is_zipfile and not readonly:
387 is_package = packages.ZipPackage(request, fullpath).isPackage()
388 if is_package and request.user.isSuperUser():
389 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='install')) +
390 fmt.text(label_install) +
392 elif (not is_package and mt.minor == 'zip' and
393 may_read and may_write and may_delete):
394 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='unzip')) +
395 fmt.text(label_unzip) +
398 # We don't want to crash with a traceback here (an exception
399 # here could be caused by an uploaded defective zip file - and
400 # if we crash here, the user does not get a UI to remove the
401 # defective zip file again).
402 # RuntimeError is raised by zipfile stdlib module in case of
403 # problems (like inconsistent slash and backslash usage in the
405 logging.exception("An exception within zip file attachment handling occurred:")
407 html.append(fmt.listitem(1))
408 html.append("[%s]" % " | ".join(links))
409 html.append(" (%(fmtime)s, %(fsize)s KB) [[attachment:%(file)s]]" % parmdict)
410 html.append(fmt.listitem(0))
411 html.append(fmt.bullet_list(0))
415 html.append(fmt.paragraph(1))
416 html.append(fmt.text(_("No attachments stored for %(pagename)s") % {
417 'pagename': pagename}))
418 html.append(fmt.paragraph(0))
423 def _get_files(request, pagename):
424 attach_dir = getAttachDir(request, pagename)
425 if os.path.isdir(attach_dir):
426 files = [fn.decode(config.charset) for fn in os.listdir(attach_dir)]
433 def _get_filelist(request, pagename):
434 return _build_filelist(request, pagename, 1, 0)
437 def error_msg(pagename, request, msg):
438 msg = wikiutil.escape(msg)
439 request.theme.add_msg(msg, "error")
440 Page(request, pagename).send_page()
443 #############################################################################
444 ### Create parts of the Web interface
445 #############################################################################
447 def send_link_rel(request, pagename):
448 files = _get_files(request, pagename)
450 url = getAttachUrl(pagename, fname, request, do='view')
451 request.write(u'<link rel="Appendix" title="%s" href="%s">\n' % (
452 wikiutil.escape(fname, 1),
453 wikiutil.escape(url, 1)))
455 def send_uploadform(pagename, request):
456 """ Send the HTML code for the list of already stored attachments and
457 the file upload form.
461 if not request.user.may.read(pagename):
462 request.write('<p>%s</p>' % _('You are not allowed to view this page.'))
465 writeable = request.user.may.write(pagename)
467 # First send out the upload new attachment form on top of everything else.
468 # This avoids usability issues if you have to scroll down a lot to upload
469 # a new file when the page already has lots of attachments:
471 request.write('<h2>' + _("New Attachment") + '</h2>')
473 <form action="%(url)s" method="POST" enctype="multipart/form-data">
475 <dt>%(upload_label_file)s</dt>
476 <dd><input type="file" name="file" size="50"></dd>
477 <dt>%(upload_label_target)s</dt>
478 <dd><input type="text" name="target" size="50" value="%(target)s"></dd>
479 <dt>%(upload_label_overwrite)s</dt>
480 <dd><input type="checkbox" name="overwrite" value="1" %(overwrite_checked)s></dd>
484 <input type="hidden" name="action" value="%(action_name)s">
485 <input type="hidden" name="do" value="upload">
486 <input type="hidden" name="ticket" value="%(ticket)s">
487 <input type="submit" value="%(upload_button)s">
491 'url': request.href(pagename),
492 'action_name': action_name,
493 'upload_label_file': _('File to upload'),
494 'upload_label_target': _('Rename to'),
495 'target': wikiutil.escape(request.values.get('target', ''), 1),
496 'upload_label_overwrite': _('Overwrite existing attachment of same name'),
497 'overwrite_checked': ('', 'checked')[request.form.get('overwrite', '0') == '1'],
498 'upload_button': _('Upload'),
499 'textcha': TextCha(request).render(),
500 'ticket': wikiutil.createTicket(request),
503 request.write('<h2>' + _("Attached Files") + '</h2>')
504 request.write(_get_filelist(request, pagename))
507 request.write('<p>%s</p>' % _('You are not allowed to attach a file to this page.'))
509 #############################################################################
510 ### Web interface for file upload, viewing and deletion
511 #############################################################################
513 def execute(pagename, request):
514 """ Main dispatcher for the 'AttachFile' action. """
517 do = request.values.get('do', 'upload_form')
518 handler = globals().get('_do_%s' % do)
520 msg = handler(pagename, request)
522 msg = _('Unsupported AttachFile sub-action: %s') % do
524 error_msg(pagename, request, msg)
527 def _do_upload_form(pagename, request):
528 upload_form(pagename, request)
531 def upload_form(pagename, request, msg=''):
533 msg = wikiutil.escape(msg)
536 # Use user interface language for this generated page
537 request.setContentLanguage(request.lang)
538 request.theme.add_msg(msg, "dialog")
539 request.theme.send_title(_('Attachments for "%(pagename)s"') % {'pagename': pagename}, pagename=pagename)
540 request.write('<div id="content">\n') # start content div
541 send_uploadform(pagename, request)
542 request.write('</div>\n') # end content div
543 request.theme.send_footer(pagename)
544 request.theme.send_closing_html()
547 def _do_upload(pagename, request):
550 if not wikiutil.checkTicket(request, request.form.get('ticket', '')):
551 return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.upload' }
553 # Currently we only check TextCha for upload (this is what spammers ususally do),
554 # but it could be extended to more/all attachment write access
555 if not TextCha(request).check_answer_from_form():
556 return _('TextCha: Wrong answer! Go back and try again...')
560 file_upload = request.files.get('file')
562 # This might happen when trying to upload file names
563 # with non-ascii characters on Safari.
564 return _("No file content. Delete non ASCII characters from the file name and try again.")
567 overwrite = int(form.get('overwrite', '0'))
571 if not request.user.may.write(pagename):
572 return _('You are not allowed to attach a file to this page.')
574 if overwrite and not request.user.may.delete(pagename):
575 return _('You are not allowed to overwrite a file attachment of this page.')
577 target = form.get('target', u'').strip()
579 target = file_upload.filename or u''
581 target = wikiutil.clean_input(target)
584 return _("Filename of attachment not specified!")
588 target, bytes = add_attachment(request, pagename, target, file_upload.stream, overwrite=overwrite)
589 msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
590 " with %(bytes)d bytes saved.") % {
591 'target': target, 'filename': file_upload.filename, 'bytes': bytes}
592 except AttachmentAlreadyExists:
593 msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
594 'target': target, 'filename': file_upload.filename}
596 # return attachment list
597 upload_form(pagename, request, msg)
601 """ A storage container (multiple objects in 1 tarfile) """
603 def __init__(self, request, pagename, containername):
604 self.request = request
605 self.pagename = pagename
606 self.containername = containername
607 self.container_filename = getFilename(request, pagename, containername)
609 def member_url(self, member):
610 """ return URL for accessing container member
611 (we use same URL for get (GET) and put (POST))
613 url = Page(self.request, self.pagename).url(self.request, {
614 'action': 'AttachFile',
615 'do': 'box', # shorter to type than 'container'
616 'target': self.containername,
619 return url + '&member=%s' % member
620 # member needs to be last in qs because twikidraw looks for "file extension" at the end
622 def get(self, member):
623 """ return a file-like object with the member file data
625 tf = tarfile.TarFile(self.container_filename)
626 return tf.extractfile(member)
628 def put(self, member, content, content_length=None):
629 """ save data into a container's member """
630 tf = tarfile.TarFile(self.container_filename, mode='a')
631 if isinstance(member, unicode):
632 member = member.encode('utf-8')
633 ti = tarfile.TarInfo(member)
634 if isinstance(content, str):
635 if content_length is None:
636 content_length = len(content)
637 content = StringIO(content) # we need a file obj
638 elif not hasattr(content, 'read'):
639 logging.error("unsupported content object: %r" % content)
641 assert content_length >= 0 # we don't want -1 interpreted as 4G-1
642 ti.size = content_length
643 tf.addfile(ti, content)
647 f = open(self.container_filename, 'w')
651 return os.path.exists(self.container_filename)
653 def _do_del(pagename, request):
656 if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
657 return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.del' }
659 pagename, filename, fpath = _access_file(pagename, request)
660 if not request.user.may.delete(pagename):
661 return _('You are not allowed to delete attachments on this page.')
663 return # error msg already sent in _access_file
665 remove_attachment(request, pagename, filename)
667 upload_form(pagename, request, msg=_("Attachment '%(filename)s' deleted.") % {'filename': filename})
670 def move_file(request, pagename, new_pagename, attachment, new_attachment):
673 newpage = Page(request, new_pagename)
674 if newpage.exists(includeDeleted=1) and request.user.may.write(new_pagename) and request.user.may.delete(pagename):
675 new_attachment_path = os.path.join(getAttachDir(request, new_pagename,
676 create=1), new_attachment).encode(config.charset)
677 attachment_path = os.path.join(getAttachDir(request, pagename),
678 attachment).encode(config.charset)
680 if os.path.exists(new_attachment_path):
681 upload_form(pagename, request,
682 msg=_("Attachment '%(new_pagename)s/%(new_filename)s' already exists.") % {
683 'new_pagename': new_pagename,
684 'new_filename': new_attachment})
687 if new_attachment_path != attachment_path:
688 filesize = os.path.getsize(attachment_path)
689 filesys.rename(attachment_path, new_attachment_path)
690 _addLogEntry(request, 'ATTDEL', pagename, attachment)
691 event = FileRemovedEvent(request, pagename, attachment, filesize)
693 _addLogEntry(request, 'ATTNEW', new_pagename, new_attachment)
694 event = FileAttachedEvent(request, new_pagename, new_attachment, filesize)
696 upload_form(pagename, request,
697 msg=_("Attachment '%(pagename)s/%(filename)s' moved to '%(new_pagename)s/%(new_filename)s'.") % {
698 'pagename': pagename,
699 'filename': attachment,
700 'new_pagename': new_pagename,
701 'new_filename': new_attachment})
703 upload_form(pagename, request, msg=_("Nothing changed"))
705 upload_form(pagename, request, msg=_("Page '%(new_pagename)s' does not exist or you don't have enough rights.") % {
706 'new_pagename': new_pagename})
709 def _do_attachment_move(pagename, request):
712 if 'cancel' in request.form:
713 return _('Move aborted!')
714 if not wikiutil.checkTicket(request, request.form.get('ticket', '')):
715 return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.move' }
716 if not request.user.may.delete(pagename):
717 return _('You are not allowed to move attachments from this page.')
719 if 'newpagename' in request.form:
720 new_pagename = request.form.get('newpagename')
722 upload_form(pagename, request, msg=_("Move aborted because new page name is empty."))
723 if 'newattachmentname' in request.form:
724 new_attachment = request.form.get('newattachmentname')
725 if new_attachment != wikiutil.taintfilename(new_attachment):
726 upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
727 'filename': new_attachment})
730 upload_form(pagename, request, msg=_("Move aborted because new attachment name is empty."))
732 attachment = request.form.get('oldattachmentname')
733 move_file(request, pagename, new_pagename, attachment, new_attachment)
736 def _do_move(pagename, request):
739 pagename, filename, fpath = _access_file(pagename, request)
740 if not request.user.may.delete(pagename):
741 return _('You are not allowed to move attachments from this page.')
743 return # error msg already sent in _access_file
746 d = {'action': action_name,
747 'url': request.href(pagename),
748 'do': 'attachment_move',
749 'ticket': wikiutil.createTicket(request),
750 'pagename': wikiutil.escape(pagename, 1),
751 'attachment_name': wikiutil.escape(filename, 1),
753 'cancel': _('Cancel'),
754 'newname_label': _("New page name"),
755 'attachment_label': _("New attachment name"),
758 <form action="%(url)s" method="POST">
759 <input type="hidden" name="action" value="%(action)s">
760 <input type="hidden" name="do" value="%(do)s">
761 <input type="hidden" name="ticket" value="%(ticket)s">
764 <td class="label"><label>%(newname_label)s</label></td>
766 <input type="text" name="newpagename" value="%(pagename)s" size="80">
770 <td class="label"><label>%(attachment_label)s</label></td>
772 <input type="text" name="newattachmentname" value="%(attachment_name)s" size="80">
778 <input type="hidden" name="oldattachmentname" value="%(attachment_name)s">
779 <input type="submit" name="move" value="%(move)s">
780 <input type="submit" name="cancel" value="%(cancel)s">
785 thispage = Page(request, pagename)
786 request.theme.add_msg(formhtml, "dialog")
787 return thispage.send_page()
790 def _do_box(pagename, request):
793 pagename, filename, fpath = _access_file(pagename, request)
794 if not request.user.may.read(pagename):
795 return _('You are not allowed to get attachments from this page.')
797 return # error msg already sent in _access_file
799 timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
800 if_modified = request.if_modified_since
801 if if_modified and if_modified >= timestamp:
802 request.status_code = 304
804 ci = ContainerItem(request, pagename, filename)
805 filename = wikiutil.taintfilename(request.values['member'])
806 mt = wikiutil.MimeType(filename=filename)
807 content_type = mt.content_type()
808 mime_type = mt.mime_type()
810 # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
811 # There is no solution that is compatible to IE except stripping non-ascii chars
812 filename_enc = filename.encode(config.charset)
814 # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
815 # we just let the user store them to disk ('attachment').
816 # For safe files, we directly show them inline (this also works better for IE).
817 dangerous = mime_type in request.cfg.mimetypes_xss_protect
818 content_dispo = dangerous and 'attachment' or 'inline'
821 request.headers['Date'] = http_date(now)
822 request.headers['Content-Type'] = content_type
823 request.headers['Last-Modified'] = http_date(timestamp)
824 request.headers['Expires'] = http_date(now - 365 * 24 * 3600)
825 #request.headers['Content-Length'] = os.path.getsize(fpath)
826 content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
827 request.headers['Content-Disposition'] = content_dispo_string
830 request.send_file(ci.get(filename))
833 def _do_get(pagename, request):
836 pagename, filename, fpath = _access_file(pagename, request)
837 if not request.user.may.read(pagename):
838 return _('You are not allowed to get attachments from this page.')
840 return # error msg already sent in _access_file
842 timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
843 if_modified = request.if_modified_since
844 if if_modified and if_modified >= timestamp:
845 request.status_code = 304
847 mt = wikiutil.MimeType(filename=filename)
848 content_type = mt.content_type()
849 mime_type = mt.mime_type()
851 # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
852 # There is no solution that is compatible to IE except stripping non-ascii chars
853 filename_enc = filename.encode(config.charset)
855 # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
856 # we just let the user store them to disk ('attachment').
857 # For safe files, we directly show them inline (this also works better for IE).
858 dangerous = mime_type in request.cfg.mimetypes_xss_protect
859 content_dispo = dangerous and 'attachment' or 'inline'
862 request.headers['Date'] = http_date(now)
863 request.headers['Content-Type'] = content_type
864 request.headers['Last-Modified'] = http_date(timestamp)
865 request.headers['Expires'] = http_date(now - 365 * 24 * 3600)
866 request.headers['Content-Length'] = os.path.getsize(fpath)
867 content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
868 request.headers['Content-Disposition'] = content_dispo_string
871 request.send_file(open(fpath, 'rb'))
874 def _do_install(pagename, request):
877 if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
878 return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.install' }
880 pagename, target, targetpath = _access_file(pagename, request)
881 if not request.user.isSuperUser():
882 return _('You are not allowed to install files.')
886 package = packages.ZipPackage(request, targetpath)
888 if package.isPackage():
889 if package.installPackage():
890 msg = _("Attachment '%(filename)s' installed.") % {'filename': target}
892 msg = _("Installation of '%(filename)s' failed.") % {'filename': target}
894 msg += " " + package.msg
896 msg = _('The file %s is not a MoinMoin package file.') % target
898 upload_form(pagename, request, msg=msg)
901 def _do_unzip(pagename, request, overwrite=False):
904 if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
905 return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.unzip' }
907 pagename, filename, fpath = _access_file(pagename, request)
908 if not (request.user.may.delete(pagename) and request.user.may.read(pagename) and request.user.may.write(pagename)):
909 return _('You are not allowed to unzip attachments of this page.')
912 return # error msg already sent in _access_file
915 if not zipfile.is_zipfile(fpath):
916 return _('The file %(filename)s is not a .zip file.') % {'filename': filename}
918 # determine how which attachment names we have and how much space each is occupying
919 curr_fsizes = dict([(f, size(request, pagename, f)) for f in _get_files(request, pagename)])
921 # Checks for the existance of one common prefix path shared among
922 # all files in the zip file. If this is the case, remove the common prefix.
923 # We also prepare a dict of the new filenames->filesizes.
924 zip_path_sep = '/' # we assume '/' is as zip standard suggests
928 zf = zipfile.ZipFile(fpath)
929 for zi in zf.infolist():
931 if not name.endswith(zip_path_sep): # a file (not a directory)
932 if fname_index is None:
933 fname_index = name.rfind(zip_path_sep) + 1
934 path = name[:fname_index]
935 if (name.rfind(zip_path_sep) + 1 != fname_index # different prefix len
937 name[:fname_index] != path): # same len, but still different
938 mapping = [] # zip is not acceptable
940 if zi.file_size >= request.cfg.unzip_single_file_size: # file too big
941 mapping = [] # zip is not acceptable
943 finalname = name[fname_index:] # remove common path prefix
944 finalname = finalname.decode(config.charset, 'replace') # replaces trash with \uFFFD char
945 mapping.append((name, finalname))
946 new_fsizes[finalname] = zi.file_size
948 # now we either have an empty mapping (if the zip is not acceptable),
949 # an identity mapping (no subdirs in zip, just all flat), or
950 # a mapping (origname, finalname) where origname is the zip member filename
951 # (including some prefix path) and finalname is a simple filename.
953 # calculate resulting total file size / count after unzipping:
955 curr_fsizes.update(new_fsizes)
958 new_fsizes.update(curr_fsizes)
960 total_count = len(total)
961 total_size = sum(total.values())
964 msg = _("Attachment '%(filename)s' not unzipped because some files in the zip "
965 "are either not in the same directory or exceeded the single file size limit (%(maxsize_file)d kB)."
966 ) % {'filename': filename,
967 'maxsize_file': request.cfg.unzip_single_file_size / 1000, }
968 elif total_size > request.cfg.unzip_attachments_space:
969 msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
970 "the per page attachment storage size limit (%(size)d kB).") % {
971 'filename': filename,
972 'size': request.cfg.unzip_attachments_space / 1000, }
973 elif total_count > request.cfg.unzip_attachments_count:
974 msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
975 "the per page attachment count limit (%(count)d).") % {
976 'filename': filename,
977 'count': request.cfg.unzip_attachments_count, }
980 for origname, finalname in mapping:
982 # Note: reads complete zip member file into memory. ZipFile does not offer block-wise reading:
983 add_attachment(request, pagename, finalname, zf.read(origname), overwrite)
984 except AttachmentAlreadyExists:
985 not_overwritten.append(finalname)
987 msg = _("Attachment '%(filename)s' partially unzipped (did not overwrite: %(filelist)s).") % {
988 'filename': filename,
989 'filelist': ', '.join(not_overwritten), }
991 msg = _("Attachment '%(filename)s' unzipped.") % {'filename': filename}
992 except RuntimeError, err:
993 # We don't want to crash with a traceback here (an exception
994 # here could be caused by an uploaded defective zip file - and
995 # if we crash here, the user does not get a UI to remove the
996 # defective zip file again).
997 # RuntimeError is raised by zipfile stdlib module in case of
998 # problems (like inconsistent slash and backslash usage in the
1000 logging.exception("An exception within zip file attachment handling occurred:")
1001 msg = _("A severe error occurred:") + ' ' + str(err)
1003 upload_form(pagename, request, msg=msg)
1006 def send_viewfile(pagename, request):
1008 fmt = request.html_formatter
1010 pagename, filename, fpath = _access_file(pagename, request)
1014 request.write('<h2>' + _("Attachment '%(filename)s'") % {'filename': filename} + '</h2>')
1015 # show a download link above the content
1016 label = _('Download')
1017 link = (fmt.url(1, getAttachUrl(pagename, filename, request, do='get'), css_class="download") +
1020 request.write('%s<br><br>' % link)
1022 if filename.endswith('.tdraw') or filename.endswith('.adraw'):
1023 request.write(fmt.attachment_drawing(filename, ''))
1026 mt = wikiutil.MimeType(filename=filename)
1028 # destinguishs if browser need a plugin in place
1029 if mt.major == 'image' and mt.minor in config.browser_supported_images:
1030 url = getAttachUrl(pagename, filename, request)
1031 request.write('<img src="%s" alt="%s">' % (
1032 wikiutil.escape(url, 1),
1033 wikiutil.escape(filename, 1)))
1035 elif mt.major == 'text':
1036 ext = os.path.splitext(filename)[1]
1037 Parser = wikiutil.getParserForExtension(request.cfg, ext)
1038 if Parser is not None:
1040 content = file(fpath, 'r').read()
1041 content = wikiutil.decodeUnknownInput(content)
1042 colorizer = Parser(content, request, filename=filename)
1043 colorizer.format(request.formatter)
1048 request.write(request.formatter.preformatted(1))
1049 # If we have text but no colorizing parser we try to decode file contents.
1050 content = open(fpath, 'r').read()
1051 content = wikiutil.decodeUnknownInput(content)
1052 content = wikiutil.escape(content)
1053 request.write(request.formatter.text(content))
1054 request.write(request.formatter.preformatted(0))
1058 package = packages.ZipPackage(request, fpath)
1059 if package.isPackage():
1060 request.write("<pre><b>%s</b>\n%s</pre>" % (_("Package script:"), wikiutil.escape(package.getScript())))
1063 if zipfile.is_zipfile(fpath) and mt.minor == 'zip':
1064 zf = zipfile.ZipFile(fpath, mode='r')
1065 request.write("<pre>%-46s %19s %12s\n" % (_("File Name"), _("Modified")+" "*5, _("Size")))
1066 for zinfo in zf.filelist:
1067 date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time
1068 request.write(wikiutil.escape("%-46s %s %12d\n" % (zinfo.filename, date, zinfo.file_size)))
1069 request.write("</pre>")
1071 except RuntimeError:
1072 # We don't want to crash with a traceback here (an exception
1073 # here could be caused by an uploaded defective zip file - and
1074 # if we crash here, the user does not get a UI to remove the
1075 # defective zip file again).
1076 # RuntimeError is raised by zipfile stdlib module in case of
1077 # problems (like inconsistent slash and backslash usage in the
1079 logging.exception("An exception within zip file attachment handling occurred:")
1082 from MoinMoin import macro
1083 from MoinMoin.parser.text import Parser
1085 macro.request = request
1086 macro.formatter = request.html_formatter
1087 p = Parser("##\n", request)
1090 # use EmbedObject to view valid mime types
1092 request.write('<p>' + _("Unknown file type, cannot display this attachment inline.") + '</p>')
1093 link = (fmt.url(1, getAttachUrl(pagename, filename, request)) +
1094 fmt.text(filename) +
1096 request.write('For using an external program follow this link %s' % link)
1098 request.write(m.execute('EmbedObject', u'target="%s", pagename="%s"' % (filename, pagename)))
1102 def _do_view(pagename, request):
1105 orig_pagename = pagename
1106 pagename, filename, fpath = _access_file(pagename, request)
1107 if not request.user.may.read(pagename):
1108 return _('You are not allowed to view attachments of this page.')
1112 request.formatter.page = Page(request, pagename)
1114 # send header & title
1115 # Use user interface language for this generated page
1116 request.setContentLanguage(request.lang)
1117 title = _('attachment:%(filename)s of %(pagename)s') % {
1118 'filename': filename, 'pagename': pagename}
1119 request.theme.send_title(title, pagename=pagename)
1122 request.write(request.formatter.startContent())
1123 send_viewfile(orig_pagename, request)
1124 send_uploadform(pagename, request)
1125 request.write(request.formatter.endContent())
1127 request.theme.send_footer(pagename)
1128 request.theme.send_closing_html()
1131 #############################################################################
1132 ### File attachment administration
1133 #############################################################################
1135 def do_admin_browser(request):
1136 """ Browser for SystemAdmin macro. """
1137 from MoinMoin.util.dataset import TupleDataset, Column
1140 data = TupleDataset()
1142 Column('page', label=('Page')),
1143 Column('file', label=('Filename')),
1144 Column('size', label=_('Size'), align='right'),
1147 # iterate over pages that might have attachments
1148 pages = request.rootpage.getPageList()
1149 for pagename in pages:
1150 # check for attachments directory
1151 page_dir = getAttachDir(request, pagename)
1152 if os.path.isdir(page_dir):
1153 # iterate over files of the page
1154 files = os.listdir(page_dir)
1155 for filename in files:
1156 filepath = os.path.join(page_dir, filename)
1158 (Page(request, pagename).link_to(request,
1159 querystr="action=AttachFile"), wikiutil.escape(pagename, 1)),
1160 wikiutil.escape(filename.decode(config.charset)),
1161 os.path.getsize(filepath),
1165 from MoinMoin.widget.browser import DataBrowserWidget
1167 browser = DataBrowserWidget(request)
1168 browser.setData(data, sort_columns=[0, 1])
1169 return browser.render(method="GET")