MoinMoin/action/AttachFile.py
author Roger Haase <crosseyedpenquin@yahoo.com>
Fri, 23 Jul 2010 09:23:59 -0700
changeset 5712 7a83cc907f68
parent 5591 1dff6cfdcf90
permissions -rw-r--r--
simplify auto scroll initialization; fix bug in IE init discovered when using IE7 on pages with wide tables
     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-2007 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 #############################################################################
   269 ### Internal helpers
   270 #############################################################################
   271 
   272 def _addLogEntry(request, action, pagename, filename):
   273     """ Add an entry to the edit log on uploads and deletes.
   274 
   275         `action` should be "ATTNEW" or "ATTDEL"
   276     """
   277     from MoinMoin.logfile import editlog
   278     t = wikiutil.timestamp2version(time.time())
   279     fname = wikiutil.url_quote(filename)
   280 
   281     # Write to global log
   282     log = editlog.EditLog(request)
   283     log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
   284 
   285     # Write to local log
   286     log = editlog.EditLog(request, rootpagename=pagename)
   287     log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
   288 
   289 
   290 def _access_file(pagename, request):
   291     """ Check form parameter `target` and return a tuple of
   292         `(pagename, filename, filepath)` for an existing attachment.
   293 
   294         Return `(pagename, None, None)` if an error occurs.
   295     """
   296     _ = request.getText
   297 
   298     error = None
   299     if not request.values.get('target'):
   300         error = _("Filename of attachment not specified!")
   301     else:
   302         filename = wikiutil.taintfilename(request.values['target'])
   303         fpath = getFilename(request, pagename, filename)
   304 
   305         if os.path.isfile(fpath):
   306             return (pagename, filename, fpath)
   307         error = _("Attachment '%(filename)s' does not exist!") % {'filename': filename}
   308 
   309     error_msg(pagename, request, error)
   310     return (pagename, None, None)
   311 
   312 
   313 def _build_filelist(request, pagename, showheader, readonly, mime_type='*'):
   314     _ = request.getText
   315     fmt = request.html_formatter
   316 
   317     # access directory
   318     attach_dir = getAttachDir(request, pagename)
   319     files = _get_files(request, pagename)
   320 
   321     if mime_type != '*':
   322         files = [fname for fname in files if mime_type == mimetypes.guess_type(fname)[0]]
   323 
   324     html = []
   325     if files:
   326         if showheader:
   327             html.append(fmt.rawHTML(_(
   328                 "To refer to attachments on a page, use '''{{{attachment:filename}}}''', \n"
   329                 "as shown below in the list of files. \n"
   330                 "Do '''NOT''' use the URL of the {{{[get]}}} link, \n"
   331                 "since this is subject to change and can break easily.",
   332                 wiki=True
   333             )))
   334 
   335         label_del = _("del")
   336         label_move = _("move")
   337         label_get = _("get")
   338         label_edit = _("edit")
   339         label_view = _("view")
   340         label_unzip = _("unzip")
   341         label_install = _("install")
   342 
   343         may_read = request.user.may.read(pagename)
   344         may_write = request.user.may.write(pagename)
   345         may_delete = request.user.may.delete(pagename)
   346 
   347         html.append(fmt.bullet_list(1))
   348         for file in files:
   349             mt = wikiutil.MimeType(filename=file)
   350             fullpath = os.path.join(attach_dir, file).encode(config.charset)
   351             st = os.stat(fullpath)
   352             base, ext = os.path.splitext(file)
   353             parmdict = {'file': wikiutil.escape(file),
   354                         'fsize': "%.1f" % (float(st.st_size) / 1024),
   355                         'fmtime': request.user.getFormattedDateTime(st.st_mtime),
   356                        }
   357 
   358             links = []
   359             if may_delete and not readonly:
   360                 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='del')) +
   361                              fmt.text(label_del) +
   362                              fmt.url(0))
   363 
   364             if may_delete and not readonly:
   365                 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='move')) +
   366                              fmt.text(label_move) +
   367                              fmt.url(0))
   368 
   369             links.append(fmt.url(1, getAttachUrl(pagename, file, request)) +
   370                          fmt.text(label_get) +
   371                          fmt.url(0))
   372 
   373             links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
   374                          fmt.text(label_view) +
   375                          fmt.url(0))
   376 
   377             if may_write and not readonly:
   378                 edit_url = getAttachUrl(pagename, file, request, do='modify')
   379                 if edit_url:
   380                     links.append(fmt.url(1, edit_url) +
   381                                  fmt.text(label_edit) +
   382                                  fmt.url(0))
   383 
   384             try:
   385                 is_zipfile = zipfile.is_zipfile(fullpath)
   386                 if is_zipfile and not readonly:
   387                     is_package = packages.ZipPackage(request, fullpath).isPackage()
   388                     if is_package and request.user.isSuperUser():
   389                         links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='install')) +
   390                                      fmt.text(label_install) +
   391                                      fmt.url(0))
   392                     elif (not is_package and mt.minor == 'zip' and
   393                           may_read and may_write and may_delete):
   394                         links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='unzip')) +
   395                                      fmt.text(label_unzip) +
   396                                      fmt.url(0))
   397             except RuntimeError:
   398                 # We don't want to crash with a traceback here (an exception
   399                 # here could be caused by an uploaded defective zip file - and
   400                 # if we crash here, the user does not get a UI to remove the
   401                 # defective zip file again).
   402                 # RuntimeError is raised by zipfile stdlib module in case of
   403                 # problems (like inconsistent slash and backslash usage in the
   404                 # archive).
   405                 logging.exception("An exception within zip file attachment handling occurred:")
   406 
   407             html.append(fmt.listitem(1))
   408             html.append("[%s]" % "&nbsp;| ".join(links))
   409             html.append(" (%(fmtime)s, %(fsize)s KB) [[attachment:%(file)s]]" % parmdict)
   410             html.append(fmt.listitem(0))
   411         html.append(fmt.bullet_list(0))
   412 
   413     else:
   414         if showheader:
   415             html.append(fmt.paragraph(1))
   416             html.append(fmt.text(_("No attachments stored for %(pagename)s") % {
   417                                    'pagename': pagename}))
   418             html.append(fmt.paragraph(0))
   419 
   420     return ''.join(html)
   421 
   422 
   423 def _get_files(request, pagename):
   424     attach_dir = getAttachDir(request, pagename)
   425     if os.path.isdir(attach_dir):
   426         files = [fn.decode(config.charset) for fn in os.listdir(attach_dir)]
   427         files.sort()
   428     else:
   429         files = []
   430     return files
   431 
   432 
   433 def _get_filelist(request, pagename):
   434     return _build_filelist(request, pagename, 1, 0)
   435 
   436 
   437 def error_msg(pagename, request, msg):
   438     msg = wikiutil.escape(msg)
   439     request.theme.add_msg(msg, "error")
   440     Page(request, pagename).send_page()
   441 
   442 
   443 #############################################################################
   444 ### Create parts of the Web interface
   445 #############################################################################
   446 
   447 def send_link_rel(request, pagename):
   448     files = _get_files(request, pagename)
   449     for fname in files:
   450         url = getAttachUrl(pagename, fname, request, do='view')
   451         request.write(u'<link rel="Appendix" title="%s" href="%s">\n' % (
   452                       wikiutil.escape(fname, 1),
   453                       wikiutil.escape(url, 1)))
   454 
   455 def send_uploadform(pagename, request):
   456     """ Send the HTML code for the list of already stored attachments and
   457         the file upload form.
   458     """
   459     _ = request.getText
   460 
   461     if not request.user.may.read(pagename):
   462         request.write('<p>%s</p>' % _('You are not allowed to view this page.'))
   463         return
   464 
   465     writeable = request.user.may.write(pagename)
   466 
   467     # First send out the upload new attachment form on top of everything else.
   468     # This avoids usability issues if you have to scroll down a lot to upload
   469     # a new file when the page already has lots of attachments:
   470     if writeable:
   471         request.write('<h2>' + _("New Attachment") + '</h2>')
   472         request.write("""
   473 <form action="%(url)s" method="POST" enctype="multipart/form-data">
   474 <dl>
   475 <dt>%(upload_label_file)s</dt>
   476 <dd><input type="file" name="file" size="50"></dd>
   477 <dt>%(upload_label_target)s</dt>
   478 <dd><input type="text" name="target" size="50" value="%(target)s"></dd>
   479 <dt>%(upload_label_overwrite)s</dt>
   480 <dd><input type="checkbox" name="overwrite" value="1" %(overwrite_checked)s></dd>
   481 </dl>
   482 %(textcha)s
   483 <p>
   484 <input type="hidden" name="action" value="%(action_name)s">
   485 <input type="hidden" name="do" value="upload">
   486 <input type="hidden" name="ticket" value="%(ticket)s">
   487 <input type="submit" value="%(upload_button)s">
   488 </p>
   489 </form>
   490 """ % {
   491     'url': request.href(pagename),
   492     'action_name': action_name,
   493     'upload_label_file': _('File to upload'),
   494     'upload_label_target': _('Rename to'),
   495     'target': wikiutil.escape(request.values.get('target', ''), 1),
   496     'upload_label_overwrite': _('Overwrite existing attachment of same name'),
   497     'overwrite_checked': ('', 'checked')[request.form.get('overwrite', '0') == '1'],
   498     'upload_button': _('Upload'),
   499     'textcha': TextCha(request).render(),
   500     'ticket': wikiutil.createTicket(request),
   501 })
   502 
   503     request.write('<h2>' + _("Attached Files") + '</h2>')
   504     request.write(_get_filelist(request, pagename))
   505 
   506     if not writeable:
   507         request.write('<p>%s</p>' % _('You are not allowed to attach a file to this page.'))
   508 
   509 #############################################################################
   510 ### Web interface for file upload, viewing and deletion
   511 #############################################################################
   512 
   513 def execute(pagename, request):
   514     """ Main dispatcher for the 'AttachFile' action. """
   515     _ = request.getText
   516 
   517     do = request.values.get('do', 'upload_form')
   518     handler = globals().get('_do_%s' % do)
   519     if handler:
   520         msg = handler(pagename, request)
   521     else:
   522         msg = _('Unsupported AttachFile sub-action: %s') % do
   523     if msg:
   524         error_msg(pagename, request, msg)
   525 
   526 
   527 def _do_upload_form(pagename, request):
   528     upload_form(pagename, request)
   529 
   530 
   531 def upload_form(pagename, request, msg=''):
   532     if msg:
   533         msg = wikiutil.escape(msg)
   534     _ = request.getText
   535 
   536     # Use user interface language for this generated page
   537     request.setContentLanguage(request.lang)
   538     request.theme.add_msg(msg, "dialog")
   539     request.theme.send_title(_('Attachments for "%(pagename)s"') % {'pagename': pagename}, pagename=pagename)
   540     request.write('<div id="content">\n') # start content div
   541     send_uploadform(pagename, request)
   542     request.write('</div>\n') # end content div
   543     request.theme.send_footer(pagename)
   544     request.theme.send_closing_html()
   545 
   546 
   547 def _do_upload(pagename, request):
   548     _ = request.getText
   549 
   550     if not wikiutil.checkTicket(request, request.form.get('ticket', '')):
   551         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.upload' }
   552 
   553     # Currently we only check TextCha for upload (this is what spammers ususally do),
   554     # but it could be extended to more/all attachment write access
   555     if not TextCha(request).check_answer_from_form():
   556         return _('TextCha: Wrong answer! Go back and try again...')
   557 
   558     form = request.form
   559 
   560     file_upload = request.files.get('file')
   561     if not file_upload:
   562         # This might happen when trying to upload file names
   563         # with non-ascii characters on Safari.
   564         return _("No file content. Delete non ASCII characters from the file name and try again.")
   565 
   566     try:
   567         overwrite = int(form.get('overwrite', '0'))
   568     except:
   569         overwrite = 0
   570 
   571     if not request.user.may.write(pagename):
   572         return _('You are not allowed to attach a file to this page.')
   573 
   574     if overwrite and not request.user.may.delete(pagename):
   575         return _('You are not allowed to overwrite a file attachment of this page.')
   576 
   577     target = form.get('target', u'').strip()
   578     if not target:
   579         target = file_upload.filename or u''
   580 
   581     target = wikiutil.clean_input(target)
   582 
   583     if not target:
   584         return _("Filename of attachment not specified!")
   585 
   586     # add the attachment
   587     try:
   588         target, bytes = add_attachment(request, pagename, target, file_upload.stream, overwrite=overwrite)
   589         msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
   590                 " with %(bytes)d bytes saved.") % {
   591                 'target': target, 'filename': file_upload.filename, 'bytes': bytes}
   592     except AttachmentAlreadyExists:
   593         msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
   594             'target': target, 'filename': file_upload.filename}
   595 
   596     # return attachment list
   597     upload_form(pagename, request, msg)
   598 
   599 
   600 class ContainerItem:
   601     """ A storage container (multiple objects in 1 tarfile) """
   602 
   603     def __init__(self, request, pagename, containername):
   604         self.request = request
   605         self.pagename = pagename
   606         self.containername = containername
   607         self.container_filename = getFilename(request, pagename, containername)
   608 
   609     def member_url(self, member):
   610         """ return URL for accessing container member
   611             (we use same URL for get (GET) and put (POST))
   612         """
   613         url = Page(self.request, self.pagename).url(self.request, {
   614             'action': 'AttachFile',
   615             'do': 'box',  # shorter to type than 'container'
   616             'target': self.containername,
   617             #'member': member,
   618         })
   619         return url + '&member=%s' % member
   620         # member needs to be last in qs because twikidraw looks for "file extension" at the end
   621 
   622     def get(self, member):
   623         """ return a file-like object with the member file data
   624         """
   625         tf = tarfile.TarFile(self.container_filename)
   626         return tf.extractfile(member)
   627 
   628     def put(self, member, content, content_length=None):
   629         """ save data into a container's member """
   630         tf = tarfile.TarFile(self.container_filename, mode='a')
   631         if isinstance(member, unicode):
   632             member = member.encode('utf-8')
   633         ti = tarfile.TarInfo(member)
   634         if isinstance(content, str):
   635             if content_length is None:
   636                 content_length = len(content)
   637             content = StringIO(content) # we need a file obj
   638         elif not hasattr(content, 'read'):
   639             logging.error("unsupported content object: %r" % content)
   640             raise
   641         assert content_length >= 0  # we don't want -1 interpreted as 4G-1
   642         ti.size = content_length
   643         tf.addfile(ti, content)
   644         tf.close()
   645 
   646     def truncate(self):
   647         f = open(self.container_filename, 'w')
   648         f.close()
   649 
   650     def exists(self):
   651         return os.path.exists(self.container_filename)
   652 
   653 def _do_del(pagename, request):
   654     _ = request.getText
   655 
   656     if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
   657         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.del' }
   658 
   659     pagename, filename, fpath = _access_file(pagename, request)
   660     if not request.user.may.delete(pagename):
   661         return _('You are not allowed to delete attachments on this page.')
   662     if not filename:
   663         return # error msg already sent in _access_file
   664 
   665     remove_attachment(request, pagename, filename)
   666 
   667     upload_form(pagename, request, msg=_("Attachment '%(filename)s' deleted.") % {'filename': filename})
   668 
   669 
   670 def move_file(request, pagename, new_pagename, attachment, new_attachment):
   671     _ = request.getText
   672 
   673     newpage = Page(request, new_pagename)
   674     if newpage.exists(includeDeleted=1) and request.user.may.write(new_pagename) and request.user.may.delete(pagename):
   675         new_attachment_path = os.path.join(getAttachDir(request, new_pagename,
   676                               create=1), new_attachment).encode(config.charset)
   677         attachment_path = os.path.join(getAttachDir(request, pagename),
   678                           attachment).encode(config.charset)
   679 
   680         if os.path.exists(new_attachment_path):
   681             upload_form(pagename, request,
   682                 msg=_("Attachment '%(new_pagename)s/%(new_filename)s' already exists.") % {
   683                     'new_pagename': new_pagename,
   684                     'new_filename': new_attachment})
   685             return
   686 
   687         if new_attachment_path != attachment_path:
   688             filesize = os.path.getsize(attachment_path)
   689             filesys.rename(attachment_path, new_attachment_path)
   690             _addLogEntry(request, 'ATTDEL', pagename, attachment)
   691             event = FileRemovedEvent(request, pagename, attachment, filesize)
   692             send_event(event)
   693             _addLogEntry(request, 'ATTNEW', new_pagename, new_attachment)
   694             event = FileAttachedEvent(request, new_pagename, new_attachment, filesize)
   695             send_event(event)
   696             upload_form(pagename, request,
   697                         msg=_("Attachment '%(pagename)s/%(filename)s' moved to '%(new_pagename)s/%(new_filename)s'.") % {
   698                             'pagename': pagename,
   699                             'filename': attachment,
   700                             'new_pagename': new_pagename,
   701                             'new_filename': new_attachment})
   702         else:
   703             upload_form(pagename, request, msg=_("Nothing changed"))
   704     else:
   705         upload_form(pagename, request, msg=_("Page '%(new_pagename)s' does not exist or you don't have enough rights.") % {
   706             'new_pagename': new_pagename})
   707 
   708 
   709 def _do_attachment_move(pagename, request):
   710     _ = request.getText
   711 
   712     if 'cancel' in request.form:
   713         return _('Move aborted!')
   714     if not wikiutil.checkTicket(request, request.form.get('ticket', '')):
   715         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.move' }
   716     if not request.user.may.delete(pagename):
   717         return _('You are not allowed to move attachments from this page.')
   718 
   719     if 'newpagename' in request.form:
   720         new_pagename = request.form.get('newpagename')
   721     else:
   722         upload_form(pagename, request, msg=_("Move aborted because new page name is empty."))
   723     if 'newattachmentname' in request.form:
   724         new_attachment = request.form.get('newattachmentname')
   725         if new_attachment != wikiutil.taintfilename(new_attachment):
   726             upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
   727                                   'filename': new_attachment})
   728             return
   729     else:
   730         upload_form(pagename, request, msg=_("Move aborted because new attachment name is empty."))
   731 
   732     attachment = request.form.get('oldattachmentname')
   733     move_file(request, pagename, new_pagename, attachment, new_attachment)
   734 
   735 
   736 def _do_move(pagename, request):
   737     _ = request.getText
   738 
   739     pagename, filename, fpath = _access_file(pagename, request)
   740     if not request.user.may.delete(pagename):
   741         return _('You are not allowed to move attachments from this page.')
   742     if not filename:
   743         return # error msg already sent in _access_file
   744 
   745     # move file
   746     d = {'action': action_name,
   747          'url': request.href(pagename),
   748          'do': 'attachment_move',
   749          'ticket': wikiutil.createTicket(request),
   750          'pagename': wikiutil.escape(pagename, 1),
   751          'attachment_name': wikiutil.escape(filename, 1),
   752          'move': _('Move'),
   753          'cancel': _('Cancel'),
   754          'newname_label': _("New page name"),
   755          'attachment_label': _("New attachment name"),
   756         }
   757     formhtml = '''
   758 <form action="%(url)s" method="POST">
   759 <input type="hidden" name="action" value="%(action)s">
   760 <input type="hidden" name="do" value="%(do)s">
   761 <input type="hidden" name="ticket" value="%(ticket)s">
   762 <table>
   763     <tr>
   764         <td class="label"><label>%(newname_label)s</label></td>
   765         <td class="content">
   766             <input type="text" name="newpagename" value="%(pagename)s" size="80">
   767         </td>
   768     </tr>
   769     <tr>
   770         <td class="label"><label>%(attachment_label)s</label></td>
   771         <td class="content">
   772             <input type="text" name="newattachmentname" value="%(attachment_name)s" size="80">
   773         </td>
   774     </tr>
   775     <tr>
   776         <td></td>
   777         <td class="buttons">
   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">
   781         </td>
   782     </tr>
   783 </table>
   784 </form>''' % d
   785     thispage = Page(request, pagename)
   786     request.theme.add_msg(formhtml, "dialog")
   787     return thispage.send_page()
   788 
   789 
   790 def _do_box(pagename, request):
   791     _ = request.getText
   792 
   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.')
   796     if not filename:
   797         return # error msg already sent in _access_file
   798 
   799     timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
   800     if_modified = request.if_modified_since
   801     if if_modified and if_modified >= timestamp:
   802         request.status_code = 304
   803     else:
   804         ci = ContainerItem(request, pagename, filename)
   805         filename = wikiutil.taintfilename(request.values['member'])
   806         mt = wikiutil.MimeType(filename=filename)
   807         content_type = mt.content_type()
   808         mime_type = mt.mime_type()
   809 
   810         # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
   811         # There is no solution that is compatible to IE except stripping non-ascii chars
   812         filename_enc = filename.encode(config.charset)
   813 
   814         # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
   815         # we just let the user store them to disk ('attachment').
   816         # For safe files, we directly show them inline (this also works better for IE).
   817         dangerous = mime_type in request.cfg.mimetypes_xss_protect
   818         content_dispo = dangerous and 'attachment' or 'inline'
   819 
   820         now = time.time()
   821         request.headers['Date'] = http_date(now)
   822         request.headers['Content-Type'] = content_type
   823         request.headers['Last-Modified'] = http_date(timestamp)
   824         request.headers['Expires'] = http_date(now - 365 * 24 * 3600)
   825         #request.headers['Content-Length'] = os.path.getsize(fpath)
   826         content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
   827         request.headers['Content-Disposition'] = content_dispo_string
   828 
   829         # send data
   830         request.send_file(ci.get(filename))
   831 
   832 
   833 def _do_get(pagename, request):
   834     _ = request.getText
   835 
   836     pagename, filename, fpath = _access_file(pagename, request)
   837     if not request.user.may.read(pagename):
   838         return _('You are not allowed to get attachments from this page.')
   839     if not filename:
   840         return # error msg already sent in _access_file
   841 
   842     timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
   843     if_modified = request.if_modified_since
   844     if if_modified and if_modified >= timestamp:
   845         request.status_code = 304
   846     else:
   847         mt = wikiutil.MimeType(filename=filename)
   848         content_type = mt.content_type()
   849         mime_type = mt.mime_type()
   850 
   851         # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
   852         # There is no solution that is compatible to IE except stripping non-ascii chars
   853         filename_enc = filename.encode(config.charset)
   854 
   855         # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
   856         # we just let the user store them to disk ('attachment').
   857         # For safe files, we directly show them inline (this also works better for IE).
   858         dangerous = mime_type in request.cfg.mimetypes_xss_protect
   859         content_dispo = dangerous and 'attachment' or 'inline'
   860 
   861         now = time.time()
   862         request.headers['Date'] = http_date(now)
   863         request.headers['Content-Type'] = content_type
   864         request.headers['Last-Modified'] = http_date(timestamp)
   865         request.headers['Expires'] = http_date(now - 365 * 24 * 3600)
   866         request.headers['Content-Length'] = os.path.getsize(fpath)
   867         content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
   868         request.headers['Content-Disposition'] = content_dispo_string
   869 
   870         # send data
   871         request.send_file(open(fpath, 'rb'))
   872 
   873 
   874 def _do_install(pagename, request):
   875     _ = request.getText
   876 
   877     if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
   878         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.install' }
   879 
   880     pagename, target, targetpath = _access_file(pagename, request)
   881     if not request.user.isSuperUser():
   882         return _('You are not allowed to install files.')
   883     if not target:
   884         return
   885 
   886     package = packages.ZipPackage(request, targetpath)
   887 
   888     if package.isPackage():
   889         if package.installPackage():
   890             msg = _("Attachment '%(filename)s' installed.") % {'filename': target}
   891         else:
   892             msg = _("Installation of '%(filename)s' failed.") % {'filename': target}
   893         if package.msg:
   894             msg += " " + package.msg
   895     else:
   896         msg = _('The file %s is not a MoinMoin package file.') % target
   897 
   898     upload_form(pagename, request, msg=msg)
   899 
   900 
   901 def _do_unzip(pagename, request, overwrite=False):
   902     _ = request.getText
   903 
   904     if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
   905         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.unzip' }
   906 
   907     pagename, filename, fpath = _access_file(pagename, request)
   908     if not (request.user.may.delete(pagename) and request.user.may.read(pagename) and request.user.may.write(pagename)):
   909         return _('You are not allowed to unzip attachments of this page.')
   910 
   911     if not filename:
   912         return # error msg already sent in _access_file
   913 
   914     try:
   915         if not zipfile.is_zipfile(fpath):
   916             return _('The file %(filename)s is not a .zip file.') % {'filename': filename}
   917 
   918         # determine how which attachment names we have and how much space each is occupying
   919         curr_fsizes = dict([(f, size(request, pagename, f)) for f in _get_files(request, pagename)])
   920 
   921         # Checks for the existance of one common prefix path shared among
   922         # all files in the zip file. If this is the case, remove the common prefix.
   923         # We also prepare a dict of the new filenames->filesizes.
   924         zip_path_sep = '/'  # we assume '/' is as zip standard suggests
   925         fname_index = None
   926         mapping = []
   927         new_fsizes = {}
   928         zf = zipfile.ZipFile(fpath)
   929         for zi in zf.infolist():
   930             name = zi.filename
   931             if not name.endswith(zip_path_sep):  # a file (not a directory)
   932                 if fname_index is None:
   933                     fname_index = name.rfind(zip_path_sep) + 1
   934                     path = name[:fname_index]
   935                 if (name.rfind(zip_path_sep) + 1 != fname_index  # different prefix len
   936                     or
   937                     name[:fname_index] != path): # same len, but still different
   938                     mapping = []  # zip is not acceptable
   939                     break
   940                 if zi.file_size >= request.cfg.unzip_single_file_size:  # file too big
   941                     mapping = []  # zip is not acceptable
   942                     break
   943                 finalname = name[fname_index:]  # remove common path prefix
   944                 finalname = finalname.decode(config.charset, 'replace')  # replaces trash with \uFFFD char
   945                 mapping.append((name, finalname))
   946                 new_fsizes[finalname] = zi.file_size
   947 
   948         # now we either have an empty mapping (if the zip is not acceptable),
   949         # an identity mapping (no subdirs in zip, just all flat), or
   950         # a mapping (origname, finalname) where origname is the zip member filename
   951         # (including some prefix path) and finalname is a simple filename.
   952 
   953         # calculate resulting total file size / count after unzipping:
   954         if overwrite:
   955             curr_fsizes.update(new_fsizes)
   956             total = curr_fsizes
   957         else:
   958             new_fsizes.update(curr_fsizes)
   959             total = new_fsizes
   960         total_count = len(total)
   961         total_size = sum(total.values())
   962 
   963         if not mapping:
   964             msg = _("Attachment '%(filename)s' not unzipped because some files in the zip "
   965                     "are either not in the same directory or exceeded the single file size limit (%(maxsize_file)d kB)."
   966                    ) % {'filename': filename,
   967                         'maxsize_file': request.cfg.unzip_single_file_size / 1000, }
   968         elif total_size > request.cfg.unzip_attachments_space:
   969             msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
   970                     "the per page attachment storage size limit (%(size)d kB).") % {
   971                         'filename': filename,
   972                         'size': request.cfg.unzip_attachments_space / 1000, }
   973         elif total_count > request.cfg.unzip_attachments_count:
   974             msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
   975                     "the per page attachment count limit (%(count)d).") % {
   976                         'filename': filename,
   977                         'count': request.cfg.unzip_attachments_count, }
   978         else:
   979             not_overwritten = []
   980             for origname, finalname in mapping:
   981                 try:
   982                     # Note: reads complete zip member file into memory. ZipFile does not offer block-wise reading:
   983                     add_attachment(request, pagename, finalname, zf.read(origname), overwrite)
   984                 except AttachmentAlreadyExists:
   985                     not_overwritten.append(finalname)
   986             if not_overwritten:
   987                 msg = _("Attachment '%(filename)s' partially unzipped (did not overwrite: %(filelist)s).") % {
   988                         'filename': filename,
   989                         'filelist': ', '.join(not_overwritten), }
   990             else:
   991                 msg = _("Attachment '%(filename)s' unzipped.") % {'filename': filename}
   992     except RuntimeError, err:
   993         # We don't want to crash with a traceback here (an exception
   994         # here could be caused by an uploaded defective zip file - and
   995         # if we crash here, the user does not get a UI to remove the
   996         # defective zip file again).
   997         # RuntimeError is raised by zipfile stdlib module in case of
   998         # problems (like inconsistent slash and backslash usage in the
   999         # archive).
  1000         logging.exception("An exception within zip file attachment handling occurred:")
  1001         msg = _("A severe error occurred:") + ' ' + str(err)
  1002 
  1003     upload_form(pagename, request, msg=msg)
  1004 
  1005 
  1006 def send_viewfile(pagename, request):
  1007     _ = request.getText
  1008     fmt = request.html_formatter
  1009 
  1010     pagename, filename, fpath = _access_file(pagename, request)
  1011     if not filename:
  1012         return
  1013 
  1014     request.write('<h2>' + _("Attachment '%(filename)s'") % {'filename': filename} + '</h2>')
  1015     # show a download link above the content
  1016     label = _('Download')
  1017     link = (fmt.url(1, getAttachUrl(pagename, filename, request, do='get'), css_class="download") +
  1018             fmt.text(label) +
  1019             fmt.url(0))
  1020     request.write('%s<br><br>' % link)
  1021 
  1022     if filename.endswith('.tdraw') or filename.endswith('.adraw'):
  1023         request.write(fmt.attachment_drawing(filename, ''))
  1024         return
  1025 
  1026     mt = wikiutil.MimeType(filename=filename)
  1027 
  1028     # destinguishs if browser need a plugin in place
  1029     if mt.major == 'image' and mt.minor in config.browser_supported_images:
  1030         url = getAttachUrl(pagename, filename, request)
  1031         request.write('<img src="%s" alt="%s">' % (
  1032             wikiutil.escape(url, 1),
  1033             wikiutil.escape(filename, 1)))
  1034         return
  1035     elif mt.major == 'text':
  1036         ext = os.path.splitext(filename)[1]
  1037         Parser = wikiutil.getParserForExtension(request.cfg, ext)
  1038         if Parser is not None:
  1039             try:
  1040                 content = file(fpath, 'r').read()
  1041                 content = wikiutil.decodeUnknownInput(content)
  1042                 colorizer = Parser(content, request, filename=filename)
  1043                 colorizer.format(request.formatter)
  1044                 return
  1045             except IOError:
  1046                 pass
  1047 
  1048         request.write(request.formatter.preformatted(1))
  1049         # If we have text but no colorizing parser we try to decode file contents.
  1050         content = open(fpath, 'r').read()
  1051         content = wikiutil.decodeUnknownInput(content)
  1052         content = wikiutil.escape(content)
  1053         request.write(request.formatter.text(content))
  1054         request.write(request.formatter.preformatted(0))
  1055         return
  1056 
  1057     try:
  1058         package = packages.ZipPackage(request, fpath)
  1059         if package.isPackage():
  1060             request.write("<pre><b>%s</b>\n%s</pre>" % (_("Package script:"), wikiutil.escape(package.getScript())))
  1061             return
  1062 
  1063         if zipfile.is_zipfile(fpath) and mt.minor == 'zip':
  1064             zf = zipfile.ZipFile(fpath, mode='r')
  1065             request.write("<pre>%-46s %19s %12s\n" % (_("File Name"), _("Modified")+" "*5, _("Size")))
  1066             for zinfo in zf.filelist:
  1067                 date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time
  1068                 request.write(wikiutil.escape("%-46s %s %12d\n" % (zinfo.filename, date, zinfo.file_size)))
  1069             request.write("</pre>")
  1070             return
  1071     except RuntimeError:
  1072         # We don't want to crash with a traceback here (an exception
  1073         # here could be caused by an uploaded defective zip file - and
  1074         # if we crash here, the user does not get a UI to remove the
  1075         # defective zip file again).
  1076         # RuntimeError is raised by zipfile stdlib module in case of
  1077         # problems (like inconsistent slash and backslash usage in the
  1078         # archive).
  1079         logging.exception("An exception within zip file attachment handling occurred:")
  1080         return
  1081 
  1082     from MoinMoin import macro
  1083     from MoinMoin.parser.text import Parser
  1084 
  1085     macro.request = request
  1086     macro.formatter = request.html_formatter
  1087     p = Parser("##\n", request)
  1088     m = macro.Macro(p)
  1089 
  1090     # use EmbedObject to view valid mime types
  1091     if mt is None:
  1092         request.write('<p>' + _("Unknown file type, cannot display this attachment inline.") + '</p>')
  1093         link = (fmt.url(1, getAttachUrl(pagename, filename, request)) +
  1094                 fmt.text(filename) +
  1095                 fmt.url(0))
  1096         request.write('For using an external program follow this link %s' % link)
  1097         return
  1098     request.write(m.execute('EmbedObject', u'target="%s", pagename="%s"' % (filename, pagename)))
  1099     return
  1100 
  1101 
  1102 def _do_view(pagename, request):
  1103     _ = request.getText
  1104 
  1105     orig_pagename = pagename
  1106     pagename, filename, fpath = _access_file(pagename, request)
  1107     if not request.user.may.read(pagename):
  1108         return _('You are not allowed to view attachments of this page.')
  1109     if not filename:
  1110         return
  1111 
  1112     request.formatter.page = Page(request, pagename)
  1113 
  1114     # send header & title
  1115     # Use user interface language for this generated page
  1116     request.setContentLanguage(request.lang)
  1117     title = _('attachment:%(filename)s of %(pagename)s') % {
  1118         'filename': filename, 'pagename': pagename}
  1119     request.theme.send_title(title, pagename=pagename)
  1120 
  1121     # send body
  1122     request.write(request.formatter.startContent())
  1123     send_viewfile(orig_pagename, request)
  1124     send_uploadform(pagename, request)
  1125     request.write(request.formatter.endContent())
  1126 
  1127     request.theme.send_footer(pagename)
  1128     request.theme.send_closing_html()
  1129 
  1130 
  1131 #############################################################################
  1132 ### File attachment administration
  1133 #############################################################################
  1134 
  1135 def do_admin_browser(request):
  1136     """ Browser for SystemAdmin macro. """
  1137     from MoinMoin.util.dataset import TupleDataset, Column
  1138     _ = request.getText
  1139 
  1140     data = TupleDataset()
  1141     data.columns = [
  1142         Column('page', label=('Page')),
  1143         Column('file', label=('Filename')),
  1144         Column('size', label=_('Size'), align='right'),
  1145     ]
  1146 
  1147     # iterate over pages that might have attachments
  1148     pages = request.rootpage.getPageList()
  1149     for pagename in pages:
  1150         # check for attachments directory
  1151         page_dir = getAttachDir(request, pagename)
  1152         if os.path.isdir(page_dir):
  1153             # iterate over files of the page
  1154             files = os.listdir(page_dir)
  1155             for filename in files:
  1156                 filepath = os.path.join(page_dir, filename)
  1157                 data.addRow((
  1158                     (Page(request, pagename).link_to(request,
  1159                                 querystr="action=AttachFile"), wikiutil.escape(pagename, 1)),
  1160                     wikiutil.escape(filename.decode(config.charset)),
  1161                     os.path.getsize(filepath),
  1162                 ))
  1163 
  1164     if data:
  1165         from MoinMoin.widget.browser import DataBrowserWidget
  1166 
  1167         browser = DataBrowserWidget(request)
  1168         browser.setData(data, sort_columns=[0, 1])
  1169         return browser.render(method="GET")
  1170 
  1171     return ''
  1172