MoinMoin/action/AttachFile.py
author Thomas Waldmann <tw AT waldmann-edv DOT de>
Fri, 17 Oct 2014 21:44:40 +0200
changeset 6086 dbe605c5867c
parent 6018 b85dc05a75e1
child 6088 371fb8e44d41
permissions -rw-r--r--
updated underlay
     1 # -*- coding: iso-8859-1 -*-
     2 """
     3     MoinMoin - AttachFile action
     4 
     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.
     8 
     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
    17 
    18     To link to an attachment, use [[attachment:file.txt]],
    19     to embed an attachment, use {{attachment:file.png}}.
    20 
    21     @copyright: 2001 by Ken Sugino (sugino@mediaone.net),
    22                 2001-2004 by Juergen Hermann <jh@web.de>,
    23                 2005 MoinMoin:AlexanderSchremmer,
    24                 2005 DiegoOngaro at ETSZONE (diego@etszone.com),
    25                 2005-2013 MoinMoin:ReimarBauer,
    26                 2007-2008 MoinMoin:ThomasWaldmann
    27     @license: GNU GPL, see COPYING for details.
    28 """
    29 
    30 import os, time, zipfile, errno, datetime
    31 from StringIO import StringIO
    32 
    33 from werkzeug import http_date
    34 
    35 from MoinMoin import log
    36 logging = log.getLogger(__name__)
    37 
    38 # keep both imports below as they are, order is important:
    39 from MoinMoin import wikiutil
    40 import mimetypes
    41 
    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
    48 
    49 action_name = __name__.split('.')[-1]
    50 
    51 #############################################################################
    52 ### External interface - these are called from the core code
    53 #############################################################################
    54 
    55 class AttachmentAlreadyExists(Exception):
    56     pass
    57 
    58 
    59 def getBasePath(request):
    60     """ Get base path where page dirs for attachments are stored. """
    61     return request.rootpage.getPagePath('pages')
    62 
    63 
    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
    68     else:
    69         page = Page(request, pagename)
    70     return page.getPagePath("attachments", check_create=create)
    71 
    72 
    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
    79     """
    80     url = wikiutil.AbsPageName(pagename, url)
    81     pieces = url.split(u'/')
    82     if len(pieces) == 1:
    83         return pagename, pieces[0]
    84     else:
    85         return u"/".join(pieces[:-1]), pieces[-1]
    86 
    87 
    88 def get_action(request, filename, do):
    89     generic_do_mapping = {
    90         # do -> action
    91         'get': action_name,
    92         'view': action_name,
    93         'move': action_name,
    94         'del': action_name,
    95         'unzip': action_name,
    96         'install': action_name,
    97         'upload_form': action_name,
    98     }
    99     basename, ext = os.path.splitext(filename)
   100     do_mapping = request.cfg.extensions_mapping.get(ext, {})
   101     action = do_mapping.get(do, None)
   102     if action is 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)
   106     return action
   107 
   108 
   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.
   114     """
   115     action = get_action(request, filename, do)
   116     if action:
   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
   121             ]:
   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)
   130         return url
   131 
   132 
   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.
   136     """
   137     _ = request.getText
   138     attach_dir = getAttachDir(request, pagename)
   139     if not os.path.exists(attach_dir):
   140         return ''
   141 
   142     files = os.listdir(attach_dir)
   143     if not files:
   144         return ''
   145 
   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') +
   150                    attach_icon +
   151                    fmt.url(0))
   152     return attach_link
   153 
   154 
   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
   162     """
   163     if isinstance(filename, unicode):
   164         filename = filename.encode(config.charset)
   165     return os.path.join(getAttachDir(request, pagename, create=1), filename)
   166 
   167 
   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)
   172 
   173 
   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)
   178 
   179 
   180 def info(pagename, request):
   181     """ Generate snippet with info on the attachment for page `pagename`. """
   182     _ = request.getText
   183 
   184     attach_dir = getAttachDir(request, pagename)
   185     files = []
   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.') % {
   191         'count': len(files),
   192         'link': wikiutil.escape(link)
   193         }
   194     return "\n<p>\n%s\n</p>\n" % attach_info
   195 
   196 
   197 def _write_stream(content, stream, bufsize=8192):
   198     if hasattr(content, 'read'): # looks file-like
   199         import shutil
   200         shutil.copyfileobj(content, stream, bufsize)
   201     elif isinstance(content, str):
   202         stream.write(content)
   203     else:
   204         logging.error("unsupported content object: %r" % content)
   205         raise
   206 
   207 def add_attachment(request, pagename, target, filecontent, overwrite=0):
   208     """ save <filecontent> to an attachment <target> of page <pagename>
   209 
   210         filecontent can be either a str (in memory file content),
   211         or an open file object (file content in e.g. a tempfile).
   212     """
   213     # replace illegal chars
   214     target = wikiutil.taintfilename(target)
   215 
   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)
   219 
   220     exists = os.path.exists(fpath)
   221     if exists:
   222         if overwrite:
   223             remove_attachment(request, pagename, target)
   224         else:
   225             raise AttachmentAlreadyExists
   226 
   227     # save file
   228     stream = open(fpath, 'wb')
   229     try:
   230         _write_stream(filecontent, stream)
   231     finally:
   232         stream.close()
   233 
   234     _addLogEntry(request, 'ATTNEW', pagename, target)
   235 
   236     filesize = os.path.getsize(fpath)
   237     event = FileAttachedEvent(request, pagename, target, filesize)
   238     send_event(event)
   239 
   240     return target, filesize
   241 
   242 
   243 def remove_attachment(request, pagename, target):
   244     """ remove attachment <target> of page <pagename>
   245     """
   246     # replace illegal chars
   247     target = wikiutil.taintfilename(target)
   248 
   249     # get directory, do not create it
   250     attach_dir = getAttachDir(request, pagename, create=0)
   251     # remove file
   252     fpath = os.path.join(attach_dir, target).encode(config.charset)
   253     try:
   254         filesize = os.path.getsize(fpath)
   255         os.remove(fpath)
   256     except:
   257         # either it is gone already or we have no rights - not much we can do about it
   258         filesize = 0
   259     else:
   260         _addLogEntry(request, 'ATTDEL', pagename, target)
   261 
   262         event = FileRemovedEvent(request, pagename, target, filesize)
   263         send_event(event)
   264 
   265     return target, filesize
   266 
   267 
   268 class SamePath(Exception):
   269     """
   270     raised if an attachment move is attempted to same target path
   271     """
   272 
   273 class DestPathExists(Exception):
   274     """
   275     raised if an attachment move is attempted to an existing target path
   276     """
   277 
   278 
   279 def move_attachment(request, pagename, dest_pagename, target, dest_target,
   280                     overwrite=False):
   281     """ move attachment <target> of page <pagename>
   282         to attachment <dest_target> of page <dest_pagename>
   283 
   284         note: this is lowlevel code, acl permissions need to be checked before
   285               and also the target page should somehow exist (can be "deleted",
   286               but the pagedir should be there)
   287     """
   288     # replace illegal chars
   289     target = wikiutil.taintfilename(target)
   290     dest_target = wikiutil.taintfilename(dest_target)
   291 
   292     attachment_path = os.path.join(getAttachDir(request, pagename),
   293                                    target).encode(config.charset)
   294     dest_attachment_path = os.path.join(getAttachDir(request, dest_pagename, create=1),
   295                                         dest_target).encode(config.charset)
   296     if not overwrite and os.path.exists(dest_attachment_path):
   297         raise DestPathExists
   298     if dest_attachment_path == attachment_path:
   299         raise SamePath
   300     filesize = os.path.getsize(attachment_path)
   301     try:
   302         filesys.rename(attachment_path, dest_attachment_path)
   303     except Exception:
   304         raise
   305     else:
   306         _addLogEntry(request, 'ATTDEL', pagename, target)
   307         event = FileRemovedEvent(request, pagename, target, filesize)
   308         send_event(event)
   309         _addLogEntry(request, 'ATTNEW', dest_pagename, dest_target)
   310         event = FileAttachedEvent(request, dest_pagename, dest_target, filesize)
   311         send_event(event)
   312 
   313     return dest_target, filesize
   314 
   315 
   316 #############################################################################
   317 ### Internal helpers
   318 #############################################################################
   319 
   320 def _addLogEntry(request, action, pagename, filename):
   321     """ Add an entry to the edit log on uploads and deletes.
   322 
   323         `action` should be "ATTNEW" or "ATTDEL"
   324     """
   325     from MoinMoin.logfile import editlog
   326     t = wikiutil.timestamp2version(time.time())
   327     fname = wikiutil.url_quote(filename)
   328 
   329     # Write to global log
   330     log = editlog.EditLog(request)
   331     log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
   332 
   333     # Write to local log
   334     log = editlog.EditLog(request, rootpagename=pagename)
   335     log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
   336 
   337 
   338 def _access_file(pagename, request):
   339     """ Check form parameter `target` and return a tuple of
   340         `(pagename, filename, filepath)` for an existing attachment.
   341 
   342         Return `(pagename, None, None)` if an error occurs.
   343     """
   344     _ = request.getText
   345 
   346     error = None
   347     if not request.values.get('target'):
   348         error = _("Filename of attachment not specified!")
   349     else:
   350         filename = wikiutil.taintfilename(request.values['target'])
   351         fpath = getFilename(request, pagename, filename)
   352 
   353         if os.path.isfile(fpath):
   354             return (pagename, filename, fpath)
   355         error = _("Attachment '%(filename)s' does not exist!") % {'filename': filename}
   356 
   357     error_msg(pagename, request, error)
   358     return (pagename, None, None)
   359 
   360 
   361 def _build_filelist(request, pagename, showheader, readonly, mime_type='*', filterfn=None):
   362     _ = request.getText
   363     fmt = request.html_formatter
   364 
   365     # access directory
   366     attach_dir = getAttachDir(request, pagename)
   367     files = _get_files(request, pagename)
   368 
   369     if mime_type != '*':
   370         files = [fname for fname in files if mime_type == mimetypes.guess_type(fname)[0]]
   371     if filterfn is not None:
   372         files = [fname for fname in files if filterfn(fname)]
   373 
   374     html = []
   375     if files:
   376         if showheader:
   377             html.append(fmt.rawHTML(_(
   378                 "To refer to attachments on a page, use '''{{{attachment:filename}}}''', \n"
   379                 "as shown below in the list of files. \n"
   380                 "Do '''NOT''' use the URL of the {{{[get]}}} link, \n"
   381                 "since this is subject to change and can break easily.",
   382                 wiki=True
   383             )))
   384 
   385         label_del = _("del")
   386         label_move = _("move")
   387         label_get = _("get")
   388         label_edit = _("edit")
   389         label_view = _("view")
   390         label_unzip = _("unzip")
   391         label_install = _("install")
   392 
   393         may_read = request.user.may.read(pagename)
   394         may_write = request.user.may.write(pagename)
   395         may_delete = request.user.may.delete(pagename)
   396 
   397         html.append(u"""\
   398 <script>
   399 function checkAll(bx, targets_name) {
   400   var cbs = document.getElementsByTagName('input');
   401   for(var i=0; i < cbs.length; i++) {
   402     if(cbs[i].type == 'checkbox' && cbs[i].name == targets_name) {
   403       cbs[i].checked = bx.checked;
   404     }
   405   }
   406 }
   407 </script>
   408 <form method="POST">
   409 <input type="hidden" name="action" value="AttachFile">
   410 <input type="hidden" name="do" value="multifile">
   411 """)
   412 
   413         html.append(fmt.bullet_list(1))
   414         for file in files:
   415             mt = wikiutil.MimeType(filename=file)
   416             fullpath = os.path.join(attach_dir, file).encode(config.charset)
   417             st = os.stat(fullpath)
   418             base, ext = os.path.splitext(file)
   419             parmdict = {'file': wikiutil.escape(file),
   420                         'fsize': "%.1f" % (float(st.st_size) / 1024),
   421                         'fmtime': request.user.getFormattedDateTime(st.st_mtime),
   422                        }
   423 
   424             links = []
   425             if may_delete and not readonly:
   426                 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='del')) +
   427                              fmt.text(label_del) +
   428                              fmt.url(0))
   429 
   430             if may_delete and not readonly:
   431                 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='move')) +
   432                              fmt.text(label_move) +
   433                              fmt.url(0))
   434 
   435             links.append(fmt.url(1, getAttachUrl(pagename, file, request)) +
   436                          fmt.text(label_get) +
   437                          fmt.url(0))
   438 
   439             links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
   440                          fmt.text(label_view) +
   441                          fmt.url(0))
   442 
   443             if may_write and not readonly:
   444                 edit_url = getAttachUrl(pagename, file, request, do='modify')
   445                 if edit_url:
   446                     links.append(fmt.url(1, edit_url) +
   447                                  fmt.text(label_edit) +
   448                                  fmt.url(0))
   449 
   450             try:
   451                 is_zipfile = zipfile.is_zipfile(fullpath)
   452                 if is_zipfile and not readonly:
   453                     is_package = packages.ZipPackage(request, fullpath).isPackage()
   454                     if is_package and request.user.isSuperUser():
   455                         links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='install')) +
   456                                      fmt.text(label_install) +
   457                                      fmt.url(0))
   458                     elif (not is_package and mt.minor == 'zip' and
   459                           may_read and may_write and may_delete):
   460                         links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='unzip')) +
   461                                      fmt.text(label_unzip) +
   462                                      fmt.url(0))
   463             except (RuntimeError, zipfile.BadZipfile, zipfile.LargeZipFile):
   464                 # We don't want to crash with a traceback here (an exception
   465                 # here could be caused by an uploaded defective zip file - and
   466                 # if we crash here, the user does not get a UI to remove the
   467                 # defective zip file again).
   468                 # RuntimeError is raised by zipfile stdlib module in case of
   469                 # problems (like inconsistent slash and backslash usage in the
   470                 # archive).
   471                 # BadZipfile/LargeZipFile are raised when there are some
   472                 # specific problems with the archive file.
   473                 logging.exception("An exception within zip file attachment handling occurred:")
   474 
   475             html.append(fmt.listitem(1))
   476             html.append("[%s]" % "&nbsp;| ".join(links))
   477             html.append('''<input type="checkbox" name="fn" value="%s">''' % file)
   478             html.append(" (%(fmtime)s, %(fsize)s KB) [[attachment:%(file)s]]" % parmdict)
   479             html.append(fmt.listitem(0))
   480         html.append(fmt.bullet_list(0))
   481         html.append(u"""\
   482 <input type="checkbox" onclick="checkAll(this, 'fn')">\
   483 &nbsp;%(all_files)s&nbsp;|&nbsp;%(sel_files)s
   484 <input type="radio" name="multifile" value="rm">%(delete)s</input>
   485 <input type="radio" name="multifile" value="mv">%(move)s</input>
   486 <input type="text" name="multi_dest_pagename" value="%(pagename)s">
   487 <input type="submit" value="%(submit)s">
   488 """ % dict(
   489             all_files=_('All files'),
   490             sel_files=_("Selected Files:"),
   491             delete=_("delete"),
   492             move=_("move to page"),
   493             pagename=pagename,
   494             submit=_("Do it."),
   495 ))
   496         html.append("</form>")
   497 
   498     else:
   499         if showheader:
   500             html.append(fmt.paragraph(1))
   501             html.append(fmt.text(_("No attachments stored for %(pagename)s") % {
   502                                    'pagename': pagename}))
   503             html.append(fmt.paragraph(0))
   504 
   505     return ''.join(html)
   506 
   507 
   508 def _do_multifile(pagename, request):
   509     _ = request.getText
   510     action = request.form.get('multifile')
   511     fnames = request.form.getlist('fn')
   512     if action == 'rm':
   513         if not request.user.may.delete(pagename):
   514             return _('You are not allowed to delete attachments on this page.')
   515         for fn in fnames:
   516             remove_attachment(request, pagename, fn)
   517         msg = _("Attachment '%(filename)s' deleted.") % dict(
   518                 filename=u'{%s}' % ','.join(fnames))
   519         return upload_form(pagename, request, msg=msg)
   520     if action == 'mv':
   521         if not request.user.may.delete(pagename):
   522             return _('You are not allowed to move attachments from this page.')
   523         dest_pagename = request.form.get('multi_dest_pagename')
   524         if not request.user.may.write(dest_pagename):
   525             return _('You are not allowed to attach a file to this page.')
   526         for fn in fnames:
   527             move_attachment(request, pagename, dest_pagename, fn, fn)
   528         msg = _("Attachment '%(pagename)s/%(filename)s' moved to '%(new_pagename)s/%(new_filename)s'.") % dict(
   529                 pagename=pagename,
   530                 filename=u'{%s}' % ','.join(fnames),
   531                 new_pagename=dest_pagename,
   532                 new_filename=u'*')
   533         return upload_form(pagename, request, msg=msg)
   534     return u'unsupported multifile operation'
   535 
   536 
   537 def _get_files(request, pagename):
   538     attach_dir = getAttachDir(request, pagename)
   539     if os.path.isdir(attach_dir):
   540         files = [fn.decode(config.charset) for fn in os.listdir(attach_dir)]
   541         files.sort()
   542     else:
   543         files = []
   544     return files
   545 
   546 
   547 def _get_filelist(request, pagename):
   548     return _build_filelist(request, pagename, 1, 0)
   549 
   550 
   551 def error_msg(pagename, request, msg):
   552     msg = wikiutil.escape(msg)
   553     request.theme.add_msg(msg, "error")
   554     Page(request, pagename).send_page()
   555 
   556 
   557 #############################################################################
   558 ### Create parts of the Web interface
   559 #############################################################################
   560 
   561 def send_link_rel(request, pagename):
   562     files = _get_files(request, pagename)
   563     for fname in files:
   564         url = getAttachUrl(pagename, fname, request, do='view')
   565         request.write(u'<link rel="Appendix" title="%s" href="%s">\n' % (
   566                       wikiutil.escape(fname, 1),
   567                       wikiutil.escape(url, 1)))
   568 
   569 def send_uploadform(pagename, request):
   570     """ Send the HTML code for the list of already stored attachments and
   571         the file upload form.
   572     """
   573     _ = request.getText
   574 
   575     if not request.user.may.read(pagename):
   576         request.write('<p>%s</p>' % _('You are not allowed to view this page.'))
   577         return
   578 
   579     writeable = request.user.may.write(pagename)
   580 
   581     # First send out the upload new attachment form on top of everything else.
   582     # This avoids usability issues if you have to scroll down a lot to upload
   583     # a new file when the page already has lots of attachments:
   584     if writeable:
   585         request.write('<h2>' + _("New Attachment") + '</h2>')
   586         request.write("""
   587 <form action="%(url)s" method="POST" enctype="multipart/form-data">
   588 <dl>
   589 <dt>%(upload_label_file)s</dt>
   590 <dd><input type="file" name="file" size="50"></dd>
   591 <dt>%(upload_label_target)s</dt>
   592 <dd><input type="text" name="target" size="50" value="%(target)s"></dd>
   593 <dt>%(upload_label_overwrite)s</dt>
   594 <dd><input type="checkbox" name="overwrite" value="1" %(overwrite_checked)s></dd>
   595 </dl>
   596 %(textcha)s
   597 <p>
   598 <input type="hidden" name="action" value="%(action_name)s">
   599 <input type="hidden" name="do" value="upload">
   600 <input type="hidden" name="ticket" value="%(ticket)s">
   601 <input type="submit" value="%(upload_button)s">
   602 </p>
   603 </form>
   604 """ % {
   605     'url': request.href(pagename),
   606     'action_name': action_name,
   607     'upload_label_file': _('File to upload'),
   608     'upload_label_target': _('Rename to'),
   609     'target': wikiutil.escape(request.values.get('target', ''), 1),
   610     'upload_label_overwrite': _('Overwrite existing attachment of same name'),
   611     'overwrite_checked': ('', 'checked')[request.form.get('overwrite', '0') == '1'],
   612     'upload_button': _('Upload'),
   613     'textcha': TextCha(request).render(),
   614     'ticket': wikiutil.createTicket(request),
   615 })
   616 
   617     request.write('<h2>' + _("Attached Files") + '</h2>')
   618     request.write(_get_filelist(request, pagename))
   619 
   620     if not writeable:
   621         request.write('<p>%s</p>' % _('You are not allowed to attach a file to this page.'))
   622 
   623 #############################################################################
   624 ### Web interface for file upload, viewing and deletion
   625 #############################################################################
   626 
   627 def execute(pagename, request):
   628     """ Main dispatcher for the 'AttachFile' action. """
   629     _ = request.getText
   630 
   631     do = request.values.get('do', 'upload_form')
   632     handler = globals().get('_do_%s' % do)
   633     if handler:
   634         msg = handler(pagename, request)
   635     else:
   636         msg = _('Unsupported AttachFile sub-action: %s') % do
   637     if msg:
   638         error_msg(pagename, request, msg)
   639 
   640 
   641 def _do_upload_form(pagename, request):
   642     upload_form(pagename, request)
   643 
   644 
   645 def upload_form(pagename, request, msg=''):
   646     if msg:
   647         msg = wikiutil.escape(msg)
   648     _ = request.getText
   649 
   650     # Use user interface language for this generated page
   651     request.setContentLanguage(request.lang)
   652     request.theme.add_msg(msg, "dialog")
   653     request.theme.send_title(_('Attachments for "%(pagename)s"') % {'pagename': pagename}, pagename=pagename)
   654     request.write('<div id="content">\n') # start content div
   655     send_uploadform(pagename, request)
   656     request.write('</div>\n') # end content div
   657     request.theme.send_footer(pagename)
   658     request.theme.send_closing_html()
   659 
   660 
   661 def _do_upload(pagename, request):
   662     _ = request.getText
   663 
   664     if not wikiutil.checkTicket(request, request.form.get('ticket', '')):
   665         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.upload' }
   666 
   667     # Currently we only check TextCha for upload (this is what spammers ususally do),
   668     # but it could be extended to more/all attachment write access
   669     if not TextCha(request).check_answer_from_form():
   670         return _('TextCha: Wrong answer! Go back and try again...')
   671 
   672     form = request.form
   673 
   674     file_upload = request.files.get('file')
   675     if not file_upload:
   676         # This might happen when trying to upload file names
   677         # with non-ascii characters on Safari.
   678         return _("No file content. Delete non ASCII characters from the file name and try again.")
   679 
   680     try:
   681         overwrite = int(form.get('overwrite', '0'))
   682     except:
   683         overwrite = 0
   684 
   685     if not request.user.may.write(pagename):
   686         return _('You are not allowed to attach a file to this page.')
   687 
   688     if overwrite and not request.user.may.delete(pagename):
   689         return _('You are not allowed to overwrite a file attachment of this page.')
   690 
   691     target = form.get('target', u'').strip()
   692     if not target:
   693         target = file_upload.filename or u''
   694 
   695     target = wikiutil.clean_input(target)
   696 
   697     if not target:
   698         return _("Filename of attachment not specified!")
   699 
   700     # add the attachment
   701     try:
   702         target, bytes = add_attachment(request, pagename, target, file_upload.stream, overwrite=overwrite)
   703         msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
   704                 " with %(bytes)d bytes saved.") % {
   705                 'target': target, 'filename': file_upload.filename, 'bytes': bytes}
   706     except AttachmentAlreadyExists:
   707         msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
   708             'target': target, 'filename': file_upload.filename}
   709 
   710     # return attachment list
   711     upload_form(pagename, request, msg)
   712 
   713 
   714 class ContainerItem:
   715     """ A storage container (multiple objects in 1 tarfile) """
   716 
   717     def __init__(self, request, pagename, containername):
   718         """
   719         @param pagename: a wiki page name
   720         @param containername: the filename of the tar file.
   721                               Make sure this is a simple filename, NOT containing any path components.
   722                               Use wikiutil.taintfilename() to avoid somebody giving a container
   723                               name that starts with e.g. ../../filename or you'll create a
   724                               directory traversal and code execution vulnerability.
   725         """
   726         self.request = request
   727         self.pagename = pagename
   728         self.containername = containername
   729         self.container_filename = getFilename(request, pagename, containername)
   730 
   731     def member_url(self, member):
   732         """ return URL for accessing container member
   733             (we use same URL for get (GET) and put (POST))
   734         """
   735         url = Page(self.request, self.pagename).url(self.request, {
   736             'action': 'AttachFile',
   737             'do': 'box',  # shorter to type than 'container'
   738             'target': self.containername,
   739             #'member': member,
   740         })
   741         return url + '&member=%s' % member
   742         # member needs to be last in qs because twikidraw looks for "file extension" at the end
   743 
   744     def get(self, member):
   745         """ return a file-like object with the member file data
   746         """
   747         tf = tarfile.TarFile(self.container_filename)
   748         return tf.extractfile(member)
   749 
   750     def put(self, member, content, content_length=None):
   751         """ save data into a container's member """
   752         tf = tarfile.TarFile(self.container_filename, mode='a')
   753         if isinstance(member, unicode):
   754             member = member.encode('utf-8')
   755         ti = tarfile.TarInfo(member)
   756         if isinstance(content, str):
   757             if content_length is None:
   758                 content_length = len(content)
   759             content = StringIO(content) # we need a file obj
   760         elif not hasattr(content, 'read'):
   761             logging.error("unsupported content object: %r" % content)
   762             raise
   763         assert content_length >= 0  # we don't want -1 interpreted as 4G-1
   764         ti.size = content_length
   765         tf.addfile(ti, content)
   766         tf.close()
   767 
   768     def truncate(self):
   769         f = open(self.container_filename, 'w')
   770         f.close()
   771 
   772     def exists(self):
   773         return os.path.exists(self.container_filename)
   774 
   775 def _do_del(pagename, request):
   776     _ = request.getText
   777 
   778     if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
   779         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.del' }
   780 
   781     pagename, filename, fpath = _access_file(pagename, request)
   782     if not request.user.may.delete(pagename):
   783         return _('You are not allowed to delete attachments on this page.')
   784     if not filename:
   785         return # error msg already sent in _access_file
   786 
   787     remove_attachment(request, pagename, filename)
   788 
   789     upload_form(pagename, request, msg=_("Attachment '%(filename)s' deleted.") % {'filename': filename})
   790 
   791 
   792 def move_file(request, pagename, new_pagename, attachment, new_attachment):
   793     """
   794     move a file attachment from pagename:attachment to new_pagename:new_attachment
   795 
   796     @param pagename: original pagename
   797     @param new_pagename: new pagename (may be same as original pagename)
   798     @param attachment: original attachment filename
   799                        note: attachment filename must not contain a path,
   800                              use wikiutil.taintfilename() before calling move_file
   801     @param new_attachment: new attachment filename (may be same as original filename)
   802                        note: attachment filename must not contain a path,
   803                              use wikiutil.taintfilename() before calling move_file
   804     """
   805     _ = request.getText
   806 
   807     newpage = Page(request, new_pagename)
   808     if (newpage.exists(includeDeleted=1)
   809         and
   810         request.user.may.write(new_pagename)
   811         and
   812         request.user.may.delete(pagename)):
   813         try:
   814             move_attachment(request, pagename, new_pagename,
   815                             attachment, new_attachment)
   816         except DestPathExists:
   817             msg = _("Attachment '%(new_pagename)s/%(new_filename)s' already exists.") % {
   818                     'new_pagename': new_pagename,
   819                     'new_filename': new_attachment}
   820         except SamePath:
   821             msg = _("Nothing changed")
   822         else:
   823             msg = _("Attachment '%(pagename)s/%(filename)s' moved to '%(new_pagename)s/%(new_filename)s'.") % {
   824                     'pagename': pagename,
   825                     'filename': attachment,
   826                     'new_pagename': new_pagename,
   827                     'new_filename': new_attachment}
   828     else:
   829         msg = _("Page '%(new_pagename)s' does not exist or you don't have enough rights.") % {
   830                 'new_pagename': new_pagename}
   831     upload_form(pagename, request, msg=msg)
   832 
   833 
   834 def _do_attachment_move(pagename, request):
   835     _ = request.getText
   836 
   837     if 'cancel' in request.form:
   838         return _('Move aborted!')
   839     if not wikiutil.checkTicket(request, request.form.get('ticket', '')):
   840         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.move' }
   841     if not request.user.may.delete(pagename):
   842         return _('You are not allowed to move attachments from this page.')
   843 
   844     if 'newpagename' in request.form:
   845         new_pagename = request.form.get('newpagename')
   846     else:
   847         upload_form(pagename, request, msg=_("Move aborted because new page name is empty."))
   848     if 'newattachmentname' in request.form:
   849         new_attachment = request.form.get('newattachmentname')
   850         if new_attachment != wikiutil.taintfilename(new_attachment):
   851             upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
   852                                   'filename': new_attachment})
   853             return
   854     else:
   855         upload_form(pagename, request, msg=_("Move aborted because new attachment name is empty."))
   856 
   857     attachment = request.form.get('oldattachmentname')
   858     if attachment != wikiutil.taintfilename(attachment):
   859         upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
   860                               'filename': attachment})
   861         return
   862     move_file(request, pagename, new_pagename, attachment, new_attachment)
   863 
   864 
   865 def _do_move(pagename, request):
   866     _ = request.getText
   867 
   868     pagename, filename, fpath = _access_file(pagename, request)
   869     if not request.user.may.delete(pagename):
   870         return _('You are not allowed to move attachments from this page.')
   871     if not filename:
   872         return # error msg already sent in _access_file
   873 
   874     # move file
   875     d = {'action': action_name,
   876          'url': request.href(pagename),
   877          'do': 'attachment_move',
   878          'ticket': wikiutil.createTicket(request),
   879          'pagename': wikiutil.escape(pagename, 1),
   880          'attachment_name': wikiutil.escape(filename, 1),
   881          'move': _('Move'),
   882          'cancel': _('Cancel'),
   883          'newname_label': _("New page name"),
   884          'attachment_label': _("New attachment name"),
   885         }
   886     formhtml = '''
   887 <form action="%(url)s" method="POST">
   888 <input type="hidden" name="action" value="%(action)s">
   889 <input type="hidden" name="do" value="%(do)s">
   890 <input type="hidden" name="ticket" value="%(ticket)s">
   891 <table>
   892     <tr>
   893         <td class="label"><label>%(newname_label)s</label></td>
   894         <td class="content">
   895             <input type="text" name="newpagename" value="%(pagename)s" size="80">
   896         </td>
   897     </tr>
   898     <tr>
   899         <td class="label"><label>%(attachment_label)s</label></td>
   900         <td class="content">
   901             <input type="text" name="newattachmentname" value="%(attachment_name)s" size="80">
   902         </td>
   903     </tr>
   904     <tr>
   905         <td></td>
   906         <td class="buttons">
   907             <input type="hidden" name="oldattachmentname" value="%(attachment_name)s">
   908             <input type="submit" name="move" value="%(move)s">
   909             <input type="submit" name="cancel" value="%(cancel)s">
   910         </td>
   911     </tr>
   912 </table>
   913 </form>''' % d
   914     thispage = Page(request, pagename)
   915     request.theme.add_msg(formhtml, "dialog")
   916     return thispage.send_page()
   917 
   918 
   919 def _do_box(pagename, request):
   920     _ = request.getText
   921 
   922     pagename, filename, fpath = _access_file(pagename, request)
   923     if not request.user.may.read(pagename):
   924         return _('You are not allowed to get attachments from this page.')
   925     if not filename:
   926         return # error msg already sent in _access_file
   927 
   928     timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
   929     if_modified = request.if_modified_since
   930     if if_modified and if_modified >= timestamp:
   931         request.status_code = 304
   932     else:
   933         ci = ContainerItem(request, pagename, filename)
   934         filename = wikiutil.taintfilename(request.values['member'])
   935         mt = wikiutil.MimeType(filename=filename)
   936         content_type = mt.content_type()
   937         mime_type = mt.mime_type()
   938 
   939         # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
   940         # There is no solution that is compatible to IE except stripping non-ascii chars
   941         filename_enc = filename.encode(config.charset)
   942 
   943         # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
   944         # we just let the user store them to disk ('attachment').
   945         # For safe files, we directly show them inline (this also works better for IE).
   946         dangerous = mime_type in request.cfg.mimetypes_xss_protect
   947         content_dispo = dangerous and 'attachment' or 'inline'
   948 
   949         now = time.time()
   950         request.headers['Date'] = http_date(now)
   951         request.headers['Content-Type'] = content_type
   952         request.headers['Last-Modified'] = http_date(timestamp)
   953         request.headers['Expires'] = http_date(now - 365 * 24 * 3600)
   954         #request.headers['Content-Length'] = os.path.getsize(fpath)
   955         content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
   956         request.headers['Content-Disposition'] = content_dispo_string
   957 
   958         # send data
   959         request.send_file(ci.get(filename))
   960 
   961 
   962 def _do_get(pagename, request):
   963     _ = request.getText
   964 
   965     pagename, filename, fpath = _access_file(pagename, request)
   966     if not request.user.may.read(pagename):
   967         return _('You are not allowed to get attachments from this page.')
   968     if not filename:
   969         request.status_code = 404
   970         return # error msg already sent in _access_file
   971 
   972     timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
   973     if_modified = request.if_modified_since
   974     if if_modified and if_modified >= timestamp:
   975         request.status_code = 304
   976     else:
   977         mt = wikiutil.MimeType(filename=filename)
   978         content_type = mt.content_type()
   979         mime_type = mt.mime_type()
   980 
   981         # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
   982         # There is no solution that is compatible to IE except stripping non-ascii chars
   983         filename_enc = filename.encode(config.charset)
   984 
   985         # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
   986         # we just let the user store them to disk ('attachment').
   987         # For safe files, we directly show them inline (this also works better for IE).
   988         dangerous = mime_type in request.cfg.mimetypes_xss_protect
   989         content_dispo = dangerous and 'attachment' or 'inline'
   990 
   991         now = time.time()
   992         request.headers['Date'] = http_date(now)
   993         request.headers['Content-Type'] = content_type
   994         request.headers['Last-Modified'] = http_date(timestamp)
   995         request.headers['Expires'] = http_date(now - 365 * 24 * 3600)
   996         request.headers['Content-Length'] = os.path.getsize(fpath)
   997         content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
   998         request.headers['Content-Disposition'] = content_dispo_string
   999 
  1000         # send data
  1001         request.send_file(open(fpath, 'rb'))
  1002 
  1003 
  1004 def _do_install(pagename, request):
  1005     _ = request.getText
  1006 
  1007     if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
  1008         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.install' }
  1009 
  1010     pagename, target, targetpath = _access_file(pagename, request)
  1011     if not request.user.isSuperUser():
  1012         return _('You are not allowed to install files.')
  1013     if not target:
  1014         return
  1015 
  1016     package = packages.ZipPackage(request, targetpath)
  1017 
  1018     if package.isPackage():
  1019         if package.installPackage():
  1020             msg = _("Attachment '%(filename)s' installed.") % {'filename': target}
  1021         else:
  1022             msg = _("Installation of '%(filename)s' failed.") % {'filename': target}
  1023         if package.msg:
  1024             msg += " " + package.msg
  1025     else:
  1026         msg = _('The file %s is not a MoinMoin package file.') % target
  1027 
  1028     upload_form(pagename, request, msg=msg)
  1029 
  1030 
  1031 def _do_unzip(pagename, request, overwrite=False):
  1032     _ = request.getText
  1033 
  1034     if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
  1035         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.unzip' }
  1036 
  1037     pagename, filename, fpath = _access_file(pagename, request)
  1038     if not (request.user.may.delete(pagename) and request.user.may.read(pagename) and request.user.may.write(pagename)):
  1039         return _('You are not allowed to unzip attachments of this page.')
  1040 
  1041     if not filename:
  1042         return # error msg already sent in _access_file
  1043 
  1044     try:
  1045         if not zipfile.is_zipfile(fpath):
  1046             return _('The file %(filename)s is not a .zip file.') % {'filename': filename}
  1047 
  1048         # determine how which attachment names we have and how much space each is occupying
  1049         curr_fsizes = dict([(f, size(request, pagename, f)) for f in _get_files(request, pagename)])
  1050 
  1051         # Checks for the existance of one common prefix path shared among
  1052         # all files in the zip file. If this is the case, remove the common prefix.
  1053         # We also prepare a dict of the new filenames->filesizes.
  1054         zip_path_sep = '/'  # we assume '/' is as zip standard suggests
  1055         fname_index = None
  1056         mapping = []
  1057         new_fsizes = {}
  1058         zf = zipfile.ZipFile(fpath)
  1059         for zi in zf.infolist():
  1060             name = zi.filename
  1061             if not name.endswith(zip_path_sep):  # a file (not a directory)
  1062                 if fname_index is None:
  1063                     fname_index = name.rfind(zip_path_sep) + 1
  1064                     path = name[:fname_index]
  1065                 if (name.rfind(zip_path_sep) + 1 != fname_index  # different prefix len
  1066                     or
  1067                     name[:fname_index] != path): # same len, but still different
  1068                     mapping = []  # zip is not acceptable
  1069                     break
  1070                 if zi.file_size >= request.cfg.unzip_single_file_size:  # file too big
  1071                     mapping = []  # zip is not acceptable
  1072                     break
  1073                 finalname = name[fname_index:]  # remove common path prefix
  1074                 finalname = finalname.decode(config.charset, 'replace')  # replaces trash with \uFFFD char
  1075                 mapping.append((name, finalname))
  1076                 new_fsizes[finalname] = zi.file_size
  1077 
  1078         # now we either have an empty mapping (if the zip is not acceptable),
  1079         # an identity mapping (no subdirs in zip, just all flat), or
  1080         # a mapping (origname, finalname) where origname is the zip member filename
  1081         # (including some prefix path) and finalname is a simple filename.
  1082 
  1083         # calculate resulting total file size / count after unzipping:
  1084         if overwrite:
  1085             curr_fsizes.update(new_fsizes)
  1086             total = curr_fsizes
  1087         else:
  1088             new_fsizes.update(curr_fsizes)
  1089             total = new_fsizes
  1090         total_count = len(total)
  1091         total_size = sum(total.values())
  1092 
  1093         if not mapping:
  1094             msg = _("Attachment '%(filename)s' not unzipped because some files in the zip "
  1095                     "are either not in the same directory or exceeded the single file size limit (%(maxsize_file)d kB)."
  1096                    ) % {'filename': filename,
  1097                         'maxsize_file': request.cfg.unzip_single_file_size / 1000, }
  1098         elif total_size > request.cfg.unzip_attachments_space:
  1099             msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
  1100                     "the per page attachment storage size limit (%(size)d kB).") % {
  1101                         'filename': filename,
  1102                         'size': request.cfg.unzip_attachments_space / 1000, }
  1103         elif total_count > request.cfg.unzip_attachments_count:
  1104             msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
  1105                     "the per page attachment count limit (%(count)d).") % {
  1106                         'filename': filename,
  1107                         'count': request.cfg.unzip_attachments_count, }
  1108         else:
  1109             not_overwritten = []
  1110             for origname, finalname in mapping:
  1111                 try:
  1112                     # Note: reads complete zip member file into memory. ZipFile does not offer block-wise reading:
  1113                     add_attachment(request, pagename, finalname, zf.read(origname), overwrite)
  1114                 except AttachmentAlreadyExists:
  1115                     not_overwritten.append(finalname)
  1116             if not_overwritten:
  1117                 msg = _("Attachment '%(filename)s' partially unzipped (did not overwrite: %(filelist)s).") % {
  1118                         'filename': filename,
  1119                         'filelist': ', '.join(not_overwritten), }
  1120             else:
  1121                 msg = _("Attachment '%(filename)s' unzipped.") % {'filename': filename}
  1122     except (RuntimeError, zipfile.BadZipfile, zipfile.LargeZipFile), err:
  1123         # We don't want to crash with a traceback here (an exception
  1124         # here could be caused by an uploaded defective zip file - and
  1125         # if we crash here, the user does not get a UI to remove the
  1126         # defective zip file again).
  1127         # RuntimeError is raised by zipfile stdlib module in case of
  1128         # problems (like inconsistent slash and backslash usage in the
  1129         # archive).
  1130         # BadZipfile/LargeZipFile are raised when there are some
  1131         # specific problems with the archive file.
  1132         logging.exception("An exception within zip file attachment handling occurred:")
  1133         msg = _("A severe error occurred:") + ' ' + str(err)
  1134 
  1135     upload_form(pagename, request, msg=msg)
  1136 
  1137 
  1138 def send_viewfile(pagename, request):
  1139     _ = request.getText
  1140     fmt = request.html_formatter
  1141 
  1142     pagename, filename, fpath = _access_file(pagename, request)
  1143     if not filename:
  1144         return
  1145 
  1146     request.write('<h2>' + _("Attachment '%(filename)s'") % {'filename': filename} + '</h2>')
  1147     # show a download link above the content
  1148     label = _('Download')
  1149     link = (fmt.url(1, getAttachUrl(pagename, filename, request, do='get'), css_class="download") +
  1150             fmt.text(label) +
  1151             fmt.url(0))
  1152     request.write('%s<br><br>' % link)
  1153 
  1154     if filename.endswith('.tdraw') or filename.endswith('.adraw'):
  1155         request.write(fmt.attachment_drawing(filename, ''))
  1156         return
  1157 
  1158     mt = wikiutil.MimeType(filename=filename)
  1159 
  1160     # destinguishs if browser need a plugin in place
  1161     if mt.major == 'image' and mt.minor in config.browser_supported_images:
  1162         url = getAttachUrl(pagename, filename, request)
  1163         request.write('<img src="%s" alt="%s">' % (
  1164             wikiutil.escape(url, 1),
  1165             wikiutil.escape(filename, 1)))
  1166         return
  1167     elif mt.major == 'text':
  1168         ext = os.path.splitext(filename)[1]
  1169         Parser = wikiutil.getParserForExtension(request.cfg, ext)
  1170         if Parser is not None:
  1171             try:
  1172                 content = file(fpath, 'r').read()
  1173                 content = wikiutil.decodeUnknownInput(content)
  1174                 colorizer = Parser(content, request, filename=filename)
  1175                 colorizer.format(request.formatter)
  1176                 return
  1177             except IOError:
  1178                 pass
  1179 
  1180         request.write(request.formatter.preformatted(1))
  1181         # If we have text but no colorizing parser we try to decode file contents.
  1182         content = open(fpath, 'r').read()
  1183         content = wikiutil.decodeUnknownInput(content)
  1184         content = wikiutil.escape(content)
  1185         request.write(request.formatter.text(content))
  1186         request.write(request.formatter.preformatted(0))
  1187         return
  1188 
  1189     try:
  1190         package = packages.ZipPackage(request, fpath)
  1191         if package.isPackage():
  1192             request.write("<pre><b>%s</b>\n%s</pre>" % (_("Package script:"), wikiutil.escape(package.getScript())))
  1193             return
  1194 
  1195         if zipfile.is_zipfile(fpath) and mt.minor == 'zip':
  1196             zf = zipfile.ZipFile(fpath, mode='r')
  1197             request.write("<pre>%-46s %19s %12s\n" % (_("File Name"), _("Modified")+" "*5, _("Size")))
  1198             for zinfo in zf.filelist:
  1199                 date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time
  1200                 request.write(wikiutil.escape("%-46s %s %12d\n" % (zinfo.filename, date, zinfo.file_size)))
  1201             request.write("</pre>")
  1202             return
  1203     except (RuntimeError, zipfile.BadZipfile, zipfile.LargeZipFile):
  1204         # We don't want to crash with a traceback here (an exception
  1205         # here could be caused by an uploaded defective zip file - and
  1206         # if we crash here, the user does not get a UI to remove the
  1207         # defective zip file again).
  1208         # RuntimeError is raised by zipfile stdlib module in case of
  1209         # problems (like inconsistent slash and backslash usage in the
  1210         # archive).
  1211         # BadZipfile/LargeZipFile are raised when there are some
  1212         # specific problems with the archive file.
  1213         logging.exception("An exception within zip file attachment handling occurred:")
  1214         return
  1215 
  1216     from MoinMoin import macro
  1217     from MoinMoin.parser.text import Parser
  1218 
  1219     macro.request = request
  1220     macro.formatter = request.html_formatter
  1221     p = Parser("##\n", request)
  1222     m = macro.Macro(p)
  1223 
  1224     # use EmbedObject to view valid mime types
  1225     if mt is None:
  1226         request.write('<p>' + _("Unknown file type, cannot display this attachment inline.") + '</p>')
  1227         link = (fmt.url(1, getAttachUrl(pagename, filename, request)) +
  1228                 fmt.text(filename) +
  1229                 fmt.url(0))
  1230         request.write('For using an external program follow this link %s' % link)
  1231         return
  1232     request.write(m.execute('EmbedObject', u'target="%s", pagename="%s"' % (filename, pagename)))
  1233     return
  1234 
  1235 
  1236 def _do_view(pagename, request):
  1237     _ = request.getText
  1238 
  1239     orig_pagename = pagename
  1240     pagename, filename, fpath = _access_file(pagename, request)
  1241     if not request.user.may.read(pagename):
  1242         return _('You are not allowed to view attachments of this page.')
  1243     if not filename:
  1244         request.status_code = 404
  1245         return
  1246 
  1247     request.formatter.page = Page(request, pagename)
  1248 
  1249     # send header & title
  1250     # Use user interface language for this generated page
  1251     request.setContentLanguage(request.lang)
  1252     title = _('attachment:%(filename)s of %(pagename)s') % {
  1253         'filename': filename, 'pagename': pagename}
  1254     request.theme.send_title(title, pagename=pagename)
  1255 
  1256     # send body
  1257     request.write(request.formatter.startContent())
  1258     send_viewfile(orig_pagename, request)
  1259     send_uploadform(pagename, request)
  1260     request.write(request.formatter.endContent())
  1261 
  1262     request.theme.send_footer(pagename)
  1263     request.theme.send_closing_html()
  1264 
  1265 
  1266 #############################################################################
  1267 ### File attachment administration
  1268 #############################################################################
  1269 
  1270 def do_admin_browser(request):
  1271     """ Browser for SystemAdmin macro. """
  1272     from MoinMoin.util.dataset import TupleDataset, Column
  1273     _ = request.getText
  1274 
  1275     data = TupleDataset()
  1276     data.columns = [
  1277         Column('page', label=('Page')),
  1278         Column('file', label=('Filename')),
  1279         Column('size', label=_('Size'), align='right'),
  1280     ]
  1281 
  1282     # iterate over pages that might have attachments
  1283     pages = request.rootpage.getPageList()
  1284     for pagename in pages:
  1285         # check for attachments directory
  1286         page_dir = getAttachDir(request, pagename)
  1287         if os.path.isdir(page_dir):
  1288             # iterate over files of the page
  1289             files = os.listdir(page_dir)
  1290             for filename in files:
  1291                 filepath = os.path.join(page_dir, filename)
  1292                 data.addRow((
  1293                     (Page(request, pagename).link_to(request,
  1294                                 querystr="action=AttachFile"), wikiutil.escape(pagename, 1)),
  1295                     wikiutil.escape(filename.decode(config.charset)),
  1296                     os.path.getsize(filepath),
  1297                 ))
  1298 
  1299     if data:
  1300         from MoinMoin.widget.browser import DataBrowserWidget
  1301 
  1302         browser = DataBrowserWidget(request)
  1303         browser.setData(data, sort_columns=[0, 1])
  1304         return browser.render(method="GET")
  1305 
  1306     return ''
  1307