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
32 from MoinMoin import log
33 logging = log.getLogger(__name__)
35 from MoinMoin import config, wikiutil, packages
36 from MoinMoin.Page import Page
37 from MoinMoin.util import filesys, timefuncs
38 from MoinMoin.security.textcha import TextCha
39 from MoinMoin.events import FileAttachedEvent, send_event
41 action_name = __name__.split('.')[-1]
43 #############################################################################
44 ### External interface - these are called from the core code
45 #############################################################################
47 class AttachmentAlreadyExists(Exception):
51 def getBasePath(request):
52 """ Get base path where page dirs for attachments are stored. """
53 return request.rootpage.getPagePath('pages')
56 def getAttachDir(request, pagename, create=0):
57 """ Get directory where attachments for page `pagename` are stored. """
58 if request.page and pagename == request.page.page_name:
59 page = request.page # reusing existing page obj is faster
61 page = Page(request, pagename)
62 return page.getPagePath("attachments", check_create=create)
65 def absoluteName(url, pagename):
66 """ Get (pagename, filename) of an attachment: link
67 @param url: PageName/filename.ext or filename.ext (unicode)
68 @param pagename: name of the currently processed page (unicode)
69 @rtype: tuple of unicode
70 @return: PageName, filename.ext
72 url = wikiutil.AbsPageName(pagename, url)
73 pieces = url.split(u'/')
75 return pagename, pieces[0]
77 return u"/".join(pieces[:-1]), pieces[-1]
80 def attachUrl(request, pagename, filename=None, **kw):
81 # filename is not used yet, but should be used later to make a sub-item url
83 qs = '?%s' % wikiutil.makeQueryString(kw, want_unicode=False)
86 return "%s/%s%s" % (request.getScriptname(), wikiutil.quoteWikinameURL(pagename), qs)
89 def getAttachUrl(pagename, filename, request, addts=0, escaped=0, do='get', drawing='', upload=False):
90 """ Get URL that points to attachment `filename` of page `pagename`. """
93 url = attachUrl(request, pagename, filename,
94 rename=wikiutil.taintfilename(filename), action=action_name)
96 url = attachUrl(request, pagename, filename,
97 rename=wikiutil.taintfilename(filename), drawing=drawing, action=action_name)
100 url = attachUrl(request, pagename, filename,
101 target=filename, action=action_name, do=do)
103 url = attachUrl(request, pagename, filename,
104 drawing=drawing, action=action_name)
106 url = wikiutil.escape(url)
110 def getIndicator(request, pagename):
111 """ Get an attachment indicator for a page (linked clip image) or
112 an empty string if not attachments exist.
115 attach_dir = getAttachDir(request, pagename)
116 if not os.path.exists(attach_dir):
119 files = os.listdir(attach_dir)
123 fmt = request.formatter
124 attach_count = _('[%d attachments]') % len(files)
125 attach_icon = request.theme.make_icon('attach', vars={'attach_count': attach_count})
126 attach_link = (fmt.url(1, attachUrl(request, pagename, action=action_name), rel='nofollow') +
132 def getFilename(request, pagename, filename):
133 """ make complete pathfilename of file "name" attached to some page "pagename"
134 @param request: request object
135 @param pagename: name of page where the file is attached to (unicode)
136 @param filename: filename of attached file (unicode)
137 @rtype: string (in config.charset encoding)
138 @return: complete path/filename of attached file
140 if isinstance(filename, unicode):
141 filename = filename.encode(config.charset)
142 return os.path.join(getAttachDir(request, pagename, create=1), filename)
145 def exists(request, pagename, filename):
146 """ check if page <pagename> has a file <filename> attached """
147 fpath = getFilename(request, pagename, filename)
148 return os.path.exists(fpath)
151 def size(request, pagename, filename):
152 """ return file size of file attachment """
153 fpath = getFilename(request, pagename, filename)
154 return os.path.getsize(fpath)
157 def info(pagename, request):
158 """ Generate snippet with info on the attachment for page `pagename`. """
161 attach_dir = getAttachDir(request, pagename)
163 if os.path.isdir(attach_dir):
164 files = os.listdir(attach_dir)
165 page = Page(request, pagename)
166 link = page.url(request, {'action': action_name})
167 attach_info = _('There are <a href="%(link)s">%(count)s attachment(s)</a> stored for this page.') % {
169 'link': wikiutil.escape(link)
171 return "\n<p>\n%s\n</p>\n" % attach_info
174 def _write_stream(content, stream, bufsize=8192):
175 if hasattr(content, 'read'): # looks file-like
177 shutil.copyfileobj(content, stream, bufsize)
178 elif isinstance(content, str):
179 stream.write(content)
181 logging.error("unsupported content object: %r" % content)
184 def add_attachment(request, pagename, target, filecontent, overwrite=0):
185 """ save <filecontent> to an attachment <target> of page <pagename>
187 filecontent can be either a str (in memory file content),
188 or an open file object (file content in e.g. a tempfile).
192 # replace illegal chars
193 target = wikiutil.taintfilename(target)
195 # get directory, and possibly create it
196 attach_dir = getAttachDir(request, pagename, create=1)
198 fpath = os.path.join(attach_dir, target).encode(config.charset)
199 exists = os.path.exists(fpath)
200 if exists and not overwrite:
201 raise AttachmentAlreadyExists
208 stream = open(fpath, 'wb')
210 _write_stream(filecontent, stream)
214 _addLogEntry(request, 'ATTNEW', pagename, target)
216 filesize = os.path.getsize(fpath)
217 event = FileAttachedEvent(request, pagename, target, filesize)
220 return target, filesize
223 #############################################################################
225 #############################################################################
227 def _addLogEntry(request, action, pagename, filename):
228 """ Add an entry to the edit log on uploads and deletes.
230 `action` should be "ATTNEW" or "ATTDEL"
232 from MoinMoin.logfile import editlog
233 t = wikiutil.timestamp2version(time.time())
234 fname = wikiutil.url_quote(filename, want_unicode=True)
236 # Write to global log
237 log = editlog.EditLog(request)
238 log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
241 log = editlog.EditLog(request, rootpagename=pagename)
242 log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
245 def _access_file(pagename, request):
246 """ Check form parameter `target` and return a tuple of
247 `(pagename, filename, filepath)` for an existing attachment.
249 Return `(pagename, None, None)` if an error occurs.
254 if not request.form.get('target', [''])[0]:
255 error = _("Filename of attachment not specified!")
257 filename = wikiutil.taintfilename(request.form['target'][0])
258 fpath = getFilename(request, pagename, filename)
260 if os.path.isfile(fpath):
261 return (pagename, filename, fpath)
262 error = _("Attachment '%(filename)s' does not exist!") % {'filename': filename}
264 error_msg(pagename, request, error)
265 return (pagename, None, None)
268 def _build_filelist(request, pagename, showheader, readonly, mime_type='*'):
270 fmt = request.html_formatter
273 attach_dir = getAttachDir(request, pagename)
274 files = _get_files(request, pagename)
277 files = [fname for fname in files if mime_type == mimetypes.guess_type(fname)[0]]
282 html.append(fmt.rawHTML(_(
283 "To refer to attachments on a page, use '''{{{attachment:filename}}}''', \n"
284 "as shown below in the list of files. \n"
285 "Do '''NOT''' use the URL of the {{{[get]}}} link, \n"
286 "since this is subject to change and can break easily.",
291 label_move = _("move")
293 label_edit = _("edit")
294 label_view = _("view")
295 label_unzip = _("unzip")
296 label_install = _("install")
298 html.append(fmt.bullet_list(1))
300 mt = wikiutil.MimeType(filename=file)
301 fullpath = os.path.join(attach_dir, file).encode(config.charset)
302 st = os.stat(fullpath)
303 base, ext = os.path.splitext(file)
304 parmdict = {'file': wikiutil.escape(file),
305 'fsize': "%.1f" % (float(st.st_size) / 1024),
306 'fmtime': request.user.getFormattedDateTime(st.st_mtime),
310 may_delete = request.user.may.delete(pagename)
311 if may_delete and not readonly:
312 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='del')) +
313 fmt.text(label_del) +
316 if may_delete and not readonly:
317 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='move')) +
318 fmt.text(label_move) +
321 links.append(fmt.url(1, getAttachUrl(pagename, file, request)) +
322 fmt.text(label_get) +
326 links.append(fmt.url(1, getAttachUrl(pagename, file, request, drawing=base)) +
327 fmt.text(label_edit) +
330 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
331 fmt.text(label_view) +
335 is_zipfile = zipfile.is_zipfile(fullpath)
337 is_package = packages.ZipPackage(request, fullpath).isPackage()
338 if is_package and request.user.isSuperUser():
339 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='install')) +
340 fmt.text(label_install) +
342 elif (not is_package and mt.minor == 'zip' and
344 request.user.may.read(pagename) and
345 request.user.may.write(pagename)):
346 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='unzip')) +
347 fmt.text(label_unzip) +
350 # We don't want to crash with a traceback here (an exception
351 # here could be caused by an uploaded defective zip file - and
352 # if we crash here, the user does not get a UI to remove the
353 # defective zip file again).
354 # RuntimeError is raised by zipfile stdlib module in case of
355 # problems (like inconsistent slash and backslash usage in the
357 logging.exception("An exception within zip file attachment handling occurred:")
359 html.append(fmt.listitem(1))
360 html.append("[%s]" % " | ".join(links))
361 html.append(" (%(fmtime)s, %(fsize)s KB) [[attachment:%(file)s]]" % parmdict)
362 html.append(fmt.listitem(0))
363 html.append(fmt.bullet_list(0))
367 html.append(fmt.paragraph(1))
368 html.append(fmt.text(_("No attachments stored for %(pagename)s") % {
369 'pagename': pagename}))
370 html.append(fmt.paragraph(0))
375 def _get_files(request, pagename):
376 attach_dir = getAttachDir(request, pagename)
377 if os.path.isdir(attach_dir):
378 files = [fn.decode(config.charset) for fn in os.listdir(attach_dir)]
385 def _get_filelist(request, pagename):
386 return _build_filelist(request, pagename, 1, 0)
389 def error_msg(pagename, request, msg):
390 request.theme.add_msg(msg, "error")
391 Page(request, pagename).send_page()
394 #############################################################################
395 ### Create parts of the Web interface
396 #############################################################################
398 def send_link_rel(request, pagename):
399 files = _get_files(request, pagename)
401 url = getAttachUrl(pagename, fname, request, do='view', escaped=1)
402 request.write(u'<link rel="Appendix" title="%s" href="%s">\n' % (
403 wikiutil.escape(fname), url))
406 def send_hotdraw(pagename, request):
410 pubpath = request.cfg.url_prefix_static + "/applets/TWikiDrawPlugin"
411 basename = request.form['drawing'][0]
412 drawpath = getAttachUrl(pagename, basename + '.draw', request, escaped=1)
413 pngpath = getAttachUrl(pagename, basename + '.png', request, escaped=1)
414 pagelink = attachUrl(request, pagename, '', action=action_name, ts=now)
415 helplink = Page(request, "HelpOnActions/AttachFile").url(request)
416 savelink = attachUrl(request, pagename, '', action=action_name, do='savedrawing')
417 #savelink = Page(request, pagename).url(request) # XXX include target filename param here for twisted
418 # request, {'savename': request.form['drawing'][0]+'.draw'}
419 #savelink = '/cgi-bin/dumpform.bat'
421 timestamp = '&ts=%s' % now
423 request.write('<h2>' + _("Edit drawing") + '</h2>')
426 <img src="%(pngpath)s%(timestamp)s">
427 <applet code="CH.ifa.draw.twiki.TWikiDraw.class"
428 archive="%(pubpath)s/twikidraw.jar" width="640" height="480">
429 <param name="drawpath" value="%(drawpath)s">
430 <param name="pngpath" value="%(pngpath)s">
431 <param name="savepath" value="%(savelink)s">
432 <param name="basename" value="%(basename)s">
433 <param name="viewpath" value="%(pagelink)s">
434 <param name="helppath" value="%(helplink)s">
435 <strong>NOTE:</strong> You need a Java enabled browser to edit the drawing example.
438 'pngpath': pngpath, 'timestamp': timestamp,
439 'pubpath': pubpath, 'drawpath': drawpath,
440 'savelink': savelink, 'pagelink': pagelink, 'helplink': helplink,
441 'basename': wikiutil.escape(basename),
445 def send_uploadform(pagename, request):
446 """ Send the HTML code for the list of already stored attachments and
447 the file upload form.
451 if not request.user.may.read(pagename):
452 request.write('<p>%s</p>' % _('You are not allowed to view this page.'))
455 writeable = request.user.may.write(pagename)
457 # First send out the upload new attachment form on top of everything else.
458 # This avoids usability issues if you have to scroll down a lot to upload
459 # a new file when the page already has lots of attachments:
461 request.write('<h2>' + _("New Attachment") + '</h2>')
463 <form action="%(baseurl)s/%(pagename)s" method="POST" enctype="multipart/form-data">
465 <dt>%(upload_label_file)s</dt>
466 <dd><input type="file" name="file" size="50"></dd>
467 <dt>%(upload_label_rename)s</dt>
468 <dd><input type="text" name="rename" size="50" value="%(rename)s"></dd>
469 <dt>%(upload_label_overwrite)s</dt>
470 <dd><input type="checkbox" name="overwrite" value="1" %(overwrite_checked)s></dd>
474 <input type="hidden" name="action" value="%(action_name)s">
475 <input type="hidden" name="do" value="upload">
476 <input type="submit" value="%(upload_button)s">
480 'baseurl': request.getScriptname(),
481 'pagename': wikiutil.quoteWikinameURL(pagename),
482 'action_name': action_name,
483 'upload_label_file': _('File to upload'),
484 'upload_label_rename': _('Rename to'),
485 'rename': wikiutil.escape(request.form.get('rename', [''])[0], 1),
486 'upload_label_overwrite': _('Overwrite existing attachment of same name'),
487 'overwrite_checked': ('', 'checked')[request.form.get('overwrite', ['0'])[0] == '1'],
488 'upload_button': _('Upload'),
489 'textcha': TextCha(request).render(),
492 request.write('<h2>' + _("Attached Files") + '</h2>')
493 request.write(_get_filelist(request, pagename))
496 request.write('<p>%s</p>' % _('You are not allowed to attach a file to this page.'))
498 if writeable and request.form.get('drawing', [None])[0]:
499 send_hotdraw(pagename, request)
502 #############################################################################
503 ### Web interface for file upload, viewing and deletion
504 #############################################################################
506 def execute(pagename, request):
507 """ Main dispatcher for the 'AttachFile' action. """
510 do = request.form.get('do', ['upload_form'])
511 handler = globals().get('_do_%s' % do[0])
513 msg = handler(pagename, request)
515 msg = _('Unsupported AttachFile sub-action: %s') % (wikiutil.escape(do[0]), )
517 error_msg(pagename, request, msg)
520 def _do_upload_form(pagename, request):
521 upload_form(pagename, request)
524 def upload_form(pagename, request, msg=''):
527 request.emit_http_headers()
528 # Use user interface language for this generated page
529 request.setContentLanguage(request.lang)
530 request.theme.add_msg(msg, "dialog")
531 request.theme.send_title(_('Attachments for "%(pagename)s"') % {'pagename': pagename}, pagename=pagename)
532 request.write('<div id="content">\n') # start content div
533 send_uploadform(pagename, request)
534 request.write('</div>\n') # end content div
535 request.theme.send_footer(pagename)
536 request.theme.send_closing_html()
539 def preprocess_filename(filename):
540 """ preprocess the filename we got from upload form,
541 strip leading drive and path (IE misbehaviour)
543 if filename and len(filename) > 1 and (filename[1] == ':' or filename[0] == '\\'): # C:.... or \path... or \\server\...
544 bsindex = filename.rfind('\\')
546 filename = filename[bsindex+1:]
550 def _do_upload(pagename, request):
552 # Currently we only check TextCha for upload (this is what spammers ususally do),
553 # but it could be extended to more/all attachment write access
554 if not TextCha(request).check_answer_from_form():
555 return _('TextCha: Wrong answer! Go back and try again...')
558 overwrite = form.get('overwrite', [u'0'])[0]
560 overwrite = int(overwrite)
564 if not request.user.may.write(pagename):
565 return _('You are not allowed to attach a file to this page.')
567 if overwrite and not request.user.may.delete(pagename):
568 return _('You are not allowed to overwrite a file attachment of this page.')
570 filename = form.get('file__filename__')
571 rename = form.get('rename', [u''])[0].strip()
577 target = preprocess_filename(target)
578 target = wikiutil.clean_input(target)
581 return _("Filename of attachment not specified!")
584 filecontent = request.form.get('file', [None])[0]
585 if filecontent is None:
586 # This might happen when trying to upload file names
587 # with non-ascii characters on Safari.
588 return _("No file content. Delete non ASCII characters from the file name and try again.")
592 target, bytes = add_attachment(request, pagename, target, filecontent, overwrite=overwrite)
593 msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
594 " with %(bytes)d bytes saved.") % {
595 'target': target, 'filename': filename, 'bytes': bytes}
596 except AttachmentAlreadyExists:
597 msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
598 'target': target, 'filename': filename}
600 # return attachment list
601 upload_form(pagename, request, msg)
604 def _do_savedrawing(pagename, request):
607 if not request.user.may.write(pagename):
608 return _('You are not allowed to save a drawing on this page.')
610 filename = request.form['filename'][0]
611 filecontent = request.form['filepath'][0]
613 basepath, basename = os.path.split(filename)
614 basename, ext = os.path.splitext(basename)
616 # get directory, and possibly create it
617 attach_dir = getAttachDir(request, pagename, create=1)
618 savepath = os.path.join(attach_dir, basename + ext)
621 _addLogEntry(request, 'ATTDRW', pagename, basename + ext)
622 filecontent = filecontent.read() # read file completely into memory
623 filecontent = filecontent.replace("\r", "")
625 filecontent = filecontent.read() # read file completely into memory
626 filecontent = filecontent.strip()
629 # filecontent is either a file or a non-empty string
630 stream = open(savepath, 'wb')
632 _write_stream(filecontent, stream)
636 # filecontent is empty string (e.g. empty map file), delete the target file
640 if err.errno != errno.ENOENT: # no such file
643 # touch attachment directory to invalidate cache if new map is saved
645 os.utime(attach_dir, None)
647 request.emit_http_headers()
651 def _do_del(pagename, request):
654 pagename, filename, fpath = _access_file(pagename, request)
655 if not request.user.may.delete(pagename):
656 return _('You are not allowed to delete attachments on this page.')
658 return # error msg already sent in _access_file
662 _addLogEntry(request, 'ATTDEL', pagename, filename)
664 if request.cfg.xapian_search:
665 from MoinMoin.search.Xapian import Index
666 index = Index(request)
668 index.remove_item(pagename, filename)
670 upload_form(pagename, request, msg=_("Attachment '%(filename)s' deleted.") % {'filename': filename})
673 def move_file(request, pagename, new_pagename, attachment, new_attachment):
676 newpage = Page(request, new_pagename)
677 if newpage.exists(includeDeleted=1) and request.user.may.write(new_pagename) and request.user.may.delete(pagename):
678 new_attachment_path = os.path.join(getAttachDir(request, new_pagename,
679 create=1), new_attachment).encode(config.charset)
680 attachment_path = os.path.join(getAttachDir(request, pagename),
681 attachment).encode(config.charset)
683 if os.path.exists(new_attachment_path):
684 upload_form(pagename, request,
685 msg=_("Attachment '%(new_pagename)s/%(new_filename)s' already exists.") % {
686 'new_pagename': new_pagename,
687 'new_filename': new_attachment})
690 if new_attachment_path != attachment_path:
692 filesys.rename(attachment_path, new_attachment_path)
693 _addLogEntry(request, 'ATTDEL', pagename, attachment)
694 _addLogEntry(request, 'ATTNEW', new_pagename, new_attachment)
695 upload_form(pagename, request,
696 msg=_("Attachment '%(pagename)s/%(filename)s' moved to '%(new_pagename)s/%(new_filename)s'.") % {
697 'pagename': pagename,
698 'filename': attachment,
699 'new_pagename': new_pagename,
700 'new_filename': new_attachment})
702 upload_form(pagename, request, msg=_("Nothing changed"))
704 upload_form(pagename, request, msg=_("Page '%(new_pagename)s' does not exist or you don't have enough rights.") % {
705 'new_pagename': new_pagename})
708 def _do_attachment_move(pagename, request):
711 if 'cancel' in request.form:
712 return _('Move aborted!')
713 if not wikiutil.checkTicket(request, request.form['ticket'][0]):
714 return _('Please use the interactive user interface to move attachments!')
715 if not request.user.may.delete(pagename):
716 return _('You are not allowed to move attachments from this page.')
718 if 'newpagename' in request.form:
719 new_pagename = request.form.get('newpagename')[0]
721 upload_form(pagename, request, msg=_("Move aborted because new page name is empty."))
722 if 'newattachmentname' in request.form:
723 new_attachment = request.form.get('newattachmentname')[0]
724 if new_attachment != wikiutil.taintfilename(new_attachment):
725 upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
726 'filename': new_attachment})
729 upload_form(pagename, request, msg=_("Move aborted because new attachment name is empty."))
731 attachment = request.form.get('oldattachmentname')[0]
732 move_file(request, pagename, new_pagename, attachment, new_attachment)
735 def _do_move(pagename, request):
738 pagename, filename, fpath = _access_file(pagename, request)
739 if not request.user.may.delete(pagename):
740 return _('You are not allowed to move attachments from this page.')
742 return # error msg already sent in _access_file
745 d = {'action': action_name,
746 'baseurl': request.getScriptname(),
747 'do': 'attachment_move',
748 'ticket': wikiutil.createTicket(request),
749 'pagename': pagename,
750 'pagename_quoted': wikiutil.quoteWikinameURL(pagename),
751 'attachment_name': filename,
753 'cancel': _('Cancel'),
754 'newname_label': _("New page name"),
755 'attachment_label': _("New attachment name"),
758 <form action="%(baseurl)s/%(pagename_quoted)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_get(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 = timefuncs.formathttpdate(int(os.path.getmtime(fpath)))
800 if request.if_modified_since == timestamp:
801 request.emit_http_headers(["Status: 304 Not modified"])
803 mt = wikiutil.MimeType(filename=filename)
804 content_type = mt.content_type()
805 mime_type = mt.mime_type()
807 # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
808 # There is no solution that is compatible to IE except stripping non-ascii chars
809 filename_enc = filename.encode(config.charset)
811 # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
812 # we just let the user store them to disk ('attachment').
813 # For safe files, we directly show them inline (this also works better for IE).
814 dangerous = mime_type in request.cfg.mimetypes_xss_protect
815 content_dispo = dangerous and 'attachment' or 'inline'
817 request.emit_http_headers([
818 'Content-Type: %s' % content_type,
819 'Last-Modified: %s' % timestamp,
820 'Content-Length: %d' % os.path.getsize(fpath),
821 'Content-Disposition: %s; filename="%s"' % (content_dispo, filename_enc),
825 request.send_file(open(fpath, 'rb'))
828 def _do_install(pagename, request):
831 pagename, target, targetpath = _access_file(pagename, request)
832 if not request.user.isSuperUser():
833 return _('You are not allowed to install files.')
837 package = packages.ZipPackage(request, targetpath)
839 if package.isPackage():
840 if package.installPackage():
841 msg = _("Attachment '%(filename)s' installed.") % {'filename': wikiutil.escape(target)}
843 msg = _("Installation of '%(filename)s' failed.") % {'filename': wikiutil.escape(target)}
845 msg += "<br><pre>%s</pre>" % wikiutil.escape(package.msg)
847 msg = _('The file %s is not a MoinMoin package file.') % wikiutil.escape(target)
849 upload_form(pagename, request, msg=msg)
852 def _do_unzip(pagename, request, overwrite=False):
854 pagename, filename, fpath = _access_file(pagename, request)
856 if not (request.user.may.delete(pagename) and request.user.may.read(pagename) and request.user.may.write(pagename)):
857 return _('You are not allowed to unzip attachments of this page.')
860 return # error msg already sent in _access_file
863 if not zipfile.is_zipfile(fpath):
864 return _('The file %(filename)s is not a .zip file.') % {'filename': filename}
866 # determine how which attachment names we have and how much space each is occupying
867 curr_fsizes = dict([(f, size(request, pagename, f)) for f in _get_files(request, pagename)])
869 # Checks for the existance of one common prefix path shared among
870 # all files in the zip file. If this is the case, remove the common prefix.
871 # We also prepare a dict of the new filenames->filesizes.
872 zip_path_sep = '/' # we assume '/' is as zip standard suggests
876 zf = zipfile.ZipFile(fpath)
877 for zi in zf.infolist():
879 if not name.endswith(zip_path_sep): # a file (not a directory)
880 if fname_index is None:
881 fname_index = name.rfind(zip_path_sep) + 1
882 path = name[:fname_index]
883 if (name.rfind(zip_path_sep) + 1 != fname_index # different prefix len
885 name[:fname_index] != path): # same len, but still different
886 mapping = [] # zip is not acceptable
888 if zi.file_size >= request.cfg.unzip_single_file_size: # file too big
889 mapping = [] # zip is not acceptable
891 finalname = name[fname_index:] # remove common path prefix
892 finalname = finalname.decode(config.charset, 'replace') # replaces trash with \uFFFD char
893 mapping.append((name, finalname))
894 new_fsizes[finalname] = zi.file_size
896 # now we either have an empty mapping (if the zip is not acceptable),
897 # an identity mapping (no subdirs in zip, just all flat), or
898 # a mapping (origname, finalname) where origname is the zip member filename
899 # (including some prefix path) and finalname is a simple filename.
901 # calculate resulting total file size / count after unzipping:
903 curr_fsizes.update(new_fsizes)
906 new_fsizes.update(curr_fsizes)
908 total_count = len(total)
909 total_size = sum(total.values())
912 msg = _("Attachment '%(filename)s' not unzipped because some files in the zip "
913 "are either not in the same directory or exceeded the single file size limit (%(maxsize_file)d kB)."
914 ) % {'filename': filename,
915 'maxsize_file': request.cfg.unzip_single_file_size / 1000, }
916 elif total_size > request.cfg.unzip_attachments_space:
917 msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
918 "the per page attachment storage size limit (%(size)d kB).") % {
919 'filename': filename,
920 'size': request.cfg.unzip_attachments_space / 1000, }
921 elif total_count > request.cfg.unzip_attachments_count:
922 msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
923 "the per page attachment count limit (%(count)d).") % {
924 'filename': filename,
925 'count': request.cfg.unzip_attachments_count, }
928 for origname, finalname in mapping:
930 # Note: reads complete zip member file into memory. ZipFile does not offer block-wise reading:
931 add_attachment(request, pagename, finalname, zf.read(origname), overwrite)
932 except AttachmentAlreadyExists:
933 not_overwritten.append(finalname)
935 msg = _("Attachment '%(filename)s' partially unzipped (did not overwrite: %(filelist)s).") % {
936 'filename': filename,
937 'filelist': ', '.join(not_overwritten), }
939 msg = _("Attachment '%(filename)s' unzipped.") % {'filename': filename}
940 except RuntimeError, err:
941 # We don't want to crash with a traceback here (an exception
942 # here could be caused by an uploaded defective zip file - and
943 # if we crash here, the user does not get a UI to remove the
944 # defective zip file again).
945 # RuntimeError is raised by zipfile stdlib module in case of
946 # problems (like inconsistent slash and backslash usage in the
948 logging.exception("An exception within zip file attachment handling occurred:")
949 msg = _("A severe error occurred:") + ' ' + str(err)
951 upload_form(pagename, request, msg=wikiutil.escape(msg))
954 def send_viewfile(pagename, request):
956 fmt = request.html_formatter
958 pagename, filename, fpath = _access_file(pagename, request)
962 request.write('<h2>' + _("Attachment '%(filename)s'") % {'filename': filename} + '</h2>')
963 # show a download link above the content
964 label = _('Download')
965 link = (fmt.url(1, getAttachUrl(pagename, filename, request, do='get'), css_class="download") +
968 request.write('%s<br><br>' % link)
970 mt = wikiutil.MimeType(filename=filename)
972 # destinguishs if browser need a plugin in place
973 if mt.major == 'image' and mt.minor in config.browser_supported_images:
974 request.write('<img src="%s" alt="%s">' % (
975 getAttachUrl(pagename, filename, request, escaped=1),
976 wikiutil.escape(filename, 1)))
978 elif mt.major == 'text':
979 ext = os.path.splitext(filename)[1]
980 Parser = wikiutil.getParserForExtension(request.cfg, ext)
981 if Parser is not None:
983 content = file(fpath, 'r').read()
984 content = wikiutil.decodeUnknownInput(content)
985 colorizer = Parser(content, request, filename=filename)
986 colorizer.format(request.formatter)
991 request.write(request.formatter.preformatted(1))
992 # If we have text but no colorizing parser we try to decode file contents.
993 content = open(fpath, 'r').read()
994 content = wikiutil.decodeUnknownInput(content)
995 content = wikiutil.escape(content)
996 request.write(request.formatter.text(content))
997 request.write(request.formatter.preformatted(0))
1001 package = packages.ZipPackage(request, fpath)
1002 if package.isPackage():
1003 request.write("<pre><b>%s</b>\n%s</pre>" % (_("Package script:"), wikiutil.escape(package.getScript())))
1006 if zipfile.is_zipfile(fpath) and mt.minor == 'zip':
1007 zf = zipfile.ZipFile(fpath, mode='r')
1008 request.write("<pre>%-46s %19s %12s\n" % (_("File Name"), _("Modified")+" "*5, _("Size")))
1009 for zinfo in zf.filelist:
1010 date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time
1011 request.write(wikiutil.escape("%-46s %s %12d\n" % (zinfo.filename, date, zinfo.file_size)))
1012 request.write("</pre>")
1014 except RuntimeError:
1015 # We don't want to crash with a traceback here (an exception
1016 # here could be caused by an uploaded defective zip file - and
1017 # if we crash here, the user does not get a UI to remove the
1018 # defective zip file again).
1019 # RuntimeError is raised by zipfile stdlib module in case of
1020 # problems (like inconsistent slash and backslash usage in the
1022 logging.exception("An exception within zip file attachment handling occurred:")
1025 from MoinMoin import macro
1026 from MoinMoin.parser.text import Parser
1028 macro.request = request
1029 macro.formatter = request.html_formatter
1030 p = Parser("##\n", request)
1033 # use EmbedObject to view valid mime types
1035 request.write('<p>' + _("Unknown file type, cannot display this attachment inline.") + '</p>')
1036 link = (fmt.url(1, getAttachUrl(pagename, filename, request)) +
1037 fmt.text(filename) +
1039 request.write('For using an external program follow this link %s' % link)
1041 request.write(m.execute('EmbedObject', u'target=%s, pagename=%s' % (filename, pagename)))
1045 def _do_view(pagename, request):
1048 orig_pagename = pagename
1049 pagename, filename, fpath = _access_file(pagename, request)
1050 if not request.user.may.read(pagename):
1051 return _('You are not allowed to view attachments of this page.')
1055 # send header & title
1056 request.emit_http_headers()
1057 # Use user interface language for this generated page
1058 request.setContentLanguage(request.lang)
1059 title = _('attachment:%(filename)s of %(pagename)s') % {
1060 'filename': filename, 'pagename': pagename}
1061 request.theme.send_title(title, pagename=pagename)
1064 request.write(request.formatter.startContent())
1065 send_viewfile(orig_pagename, request)
1066 send_uploadform(pagename, request)
1067 request.write(request.formatter.endContent())
1069 request.theme.send_footer(pagename)
1070 request.theme.send_closing_html()
1073 #############################################################################
1074 ### File attachment administration
1075 #############################################################################
1077 def do_admin_browser(request):
1078 """ Browser for SystemAdmin macro. """
1079 from MoinMoin.util.dataset import TupleDataset, Column
1082 data = TupleDataset()
1084 Column('page', label=('Page')),
1085 Column('file', label=('Filename')),
1086 Column('size', label=_('Size'), align='right'),
1089 # iterate over pages that might have attachments
1090 pages = request.rootpage.getPageList()
1091 for pagename in pages:
1092 # check for attachments directory
1093 page_dir = getAttachDir(request, pagename)
1094 if os.path.isdir(page_dir):
1095 # iterate over files of the page
1096 files = os.listdir(page_dir)
1097 for filename in files:
1098 filepath = os.path.join(page_dir, filename)
1100 Page(request, pagename).link_to(request, querystr="action=AttachFile"),
1101 wikiutil.escape(filename.decode(config.charset)),
1102 os.path.getsize(filepath),
1106 from MoinMoin.widget.browser import DataBrowserWidget
1108 browser = DataBrowserWidget(request)
1109 browser.setData(data)
1110 return browser.toHTML()