view MoinMoin/action/AttachFile.py @ 1609:7f73dafb525d

move attachments: remove unneeded imports
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sun, 08 Oct 2006 21:02:03 +0200
parents 7a26b4c769f3
children 48194fc011e5
line wrap: on
line source
# -*- coding: iso-8859-1 -*-
"""
    MoinMoin - AttachFile action

    This action lets a page have multiple attachment files.
    It creates a folder <data>/pages/<pagename>/attachments
    and keeps everything in there.

    Form values: action=Attachment
    1. with no 'do' key: returns file upload form
    2. do=attach: accept file upload and saves the file in
       ../attachment/pagename/
    3. /pagename/fname?action=Attachment&do=get[&mimetype=type]:
       return contents of the attachment file with the name fname.
    4. /pathname/fname, do=view[&mimetype=type]:create a page
       to view the content of the file

    To insert an attachment into the page, use the "attachment:" pseudo
    schema.

    @copyright: 2001 by Ken Sugino (sugino@mediaone.net)
    @copyright: 2001-2004 by Jürgen Hermann <jh@web.de>
    @copyright: 2005 MoinMoin:ReimarBauer
    @copyright: 2005 MoinMoin:AlexanderSchremmer
    @copyright: 2005 DiegoOngaro at ETSZONE (diego@etszone.com)
    @copyright: 2006 MoinMoin:ReimarBauer
    @license: GNU GPL, see COPYING for details.
"""

import os, time, zipfile
from MoinMoin import config, user, util, wikiutil, packages
from MoinMoin.Page import Page
from MoinMoin.util import filesys, timefuncs

action_name = __name__.split('.')[-1]

def htdocs_access(request):
    return isinstance(request.cfg.attachments, type({}))


#############################################################################
### External interface - these are called from the core code
#############################################################################

class AttachmentAlreadyExists(Exception):
    pass

def getBasePath(request):
    """ Get base path where page dirs for attachments are stored.
    """
    if htdocs_access(request):
        return request.cfg.attachments['dir']
    else:
        return request.rootpage.getPagePath('pages')


def getAttachDir(request, pagename, create=0):
    """ Get directory where attachments for page `pagename` are stored.
    """
    if htdocs_access(request):
        # direct file access via webserver, from public htdocs area
        pagename = wikiutil.quoteWikinameFS(pagename)
        attach_dir = os.path.join(request.cfg.attachments['dir'], pagename, "attachments")
        if create and not os.path.isdir(attach_dir):
            os.makedirs(attach_dir)
    else:
        # send file via CGI, from page storage area
        attach_dir = Page(request, pagename).getPagePath("attachments", check_create=create)

    return attach_dir

def absoluteName(url, pagename):
    """ Get (pagename, filename) of an attachment: link
        @param url: PageName/filename.ext or filename.ext (unicode)
        @param pagename: name of the currently processed page (unicode)
        @rtype: tuple of unicode
        @return: PageName, filename.ext
    """
    pieces = url.split(u'/')
    if len(pieces) == 1:
        return pagename, pieces[0]
    else:
        return u"/".join(pieces[:-1]), pieces[-1]

def getAttachUrl(pagename, filename, request, addts=0, escaped=0):
    """ Get URL that points to attachment `filename` of page `pagename`.

        If 'addts' is true, a timestamp with the file's modification time
        is added, so that browsers reload a changed file.
    """
    if htdocs_access(request):
        # direct file access via webserver
        timestamp = ''
        if addts:
            try:
                timestamp = '?ts=%s' % os.path.getmtime(
                    getFilename(request, pagename, filename))
            except IOError:
                pass

        url = "%s/%s/attachments/%s%s" % (
            request.cfg.attachments['url'], wikiutil.quoteWikinameFS(pagename),
            wikiutil.url_quote(filename), timestamp)
    else:
        # send file via CGI
        url = "%s/%s?action=%s&do=get&target=%s" % (
            request.getScriptname(), wikiutil.quoteWikinameURL(pagename),
            action_name, wikiutil.url_quote_plus(filename))
    if escaped:
        url = wikiutil.escape(url)
    return url

def getIndicator(request, pagename):
    """ Get an attachment indicator for a page (linked clip image) or
        an empty string if not attachments exist.
    """
    _ = request.getText
    attach_dir = getAttachDir(request, pagename)
    if not os.path.exists(attach_dir): return ''

    files = os.listdir(attach_dir)
    if not files: return ''

    attach_count = _('[%d attachments]') % len(files)
    attach_icon = request.theme.make_icon('attach', vars={'attach_count': attach_count})
    attach_link = wikiutil.link_tag(request,
        "%s?action=AttachFile" % wikiutil.quoteWikinameURL(pagename),
        attach_icon,
        request.formatter, rel='nofollow')

    return attach_link


def getFilename(request, pagename, filename):
    """ make complete pathfilename of file "name" attached to some page "pagename"
        @param request: request object
        @param pagename: name of page where the file is attached to (unicode)
        @param filename: filename of attached file (unicode)
        @rtype: string (in config.charset encoding)
        @return: complete path/filename of attached file
    """
    return os.path.join(getAttachDir(request, pagename), filename).encode(config.charset)


def info(pagename, request):
    """ Generate snippet with info on the attachment for page `pagename`.
    """
    _ = request.getText

    attach_dir = getAttachDir(request, pagename)
    files = []
    if os.path.isdir(attach_dir):
        files = os.listdir(attach_dir)
    page = Page(request, pagename)
    # TODO: remove escape=0 in 2.0
    link = page.url(request, {'action': 'AttachFile'}, escape=0)
    attach_info = _('There are <a href="%(link)s">%(count)s attachment(s)</a> stored for this page.', formatted=False) % {
        'count': len(files),
        'link': wikiutil.escape(link)
        }
    return "\n<p>\n%s\n</p>\n" % attach_info

def add_attachment(request, pagename, target, filecontent):
    # replace illegal chars
    target = wikiutil.taintfilename(target)

    # set mimetype from extension, or from given mimetype
    #type, encoding = wikiutil.guess_type(target)
    #if not type:
    #    ext = None
    #    if request.form.has_key('mime'):
    #        ext = wikiutil.guess_extension(request.form['mime'][0])
    #    if not ext:
    #        type, encoding = wikiutil.guess_type(filename)
    #        if type:
    #            ext = wikiutil.guess_extension(type)
    #        else:
    #            ext = ''
    #    target = target + ext

    # get directory, and possibly create it
    attach_dir = getAttachDir(request, pagename, create=1)
    # save file
    fpath = os.path.join(attach_dir, target).encode(config.charset)
    if os.path.exists(fpath):
        raise AttachmentAlreadyExists
    else:
        stream = open(fpath, 'wb')
        try:
            stream.write(filecontent)
        finally:
            stream.close()

        _addLogEntry(request, 'ATTNEW', pagename, target)

        if request.cfg.xapian_search:
            from MoinMoin.search.Xapian import Index
            index = Index(request)
            if index.exists():
                index.update_page(pagename)

        return target


#############################################################################
### Internal helpers
#############################################################################

def _addLogEntry(request, action, pagename, filename):
    """ Add an entry to the edit log on uploads and deletes.

        `action` should be "ATTNEW" or "ATTDEL"
    """
    from MoinMoin.logfile import editlog
    t = wikiutil.timestamp2version(time.time())
    fname = wikiutil.url_quote(filename, want_unicode=True)

    # TODO: for now we simply write 2 logs, maybe better use some multilog stuff
    # Write to global log
    log = editlog.EditLog(request)
    log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)

    # Write to local log
    log = editlog.EditLog(request, rootpagename=pagename)
    log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)


def _access_file(pagename, request):
    """ Check form parameter `target` and return a tuple of
        `(filename, filepath)` for an existing attachment.

        Return `(None, None)` if an error occurs.
    """
    _ = request.getText

    error = None
    if not request.form.get('target', [''])[0]:
        error = _("Filename of attachment not specified!")
    else:
        filename = wikiutil.taintfilename(request.form['target'][0])
        fpath = getFilename(request, pagename, filename)

        if os.path.isfile(fpath):
            return (filename, fpath)
        error = _("Attachment '%(filename)s' does not exist!") % {'filename': filename}

    error_msg(pagename, request, error)
    return (None, None)


def _build_filelist(request, pagename, showheader, readonly):
    _ = request.getText

    # access directory
    attach_dir = getAttachDir(request, pagename)
    files = _get_files(request, pagename)

    str = ""
    if files:
        if showheader:
            str = str + _(
                "To refer to attachments on a page, use '''{{{attachment:filename}}}''', \n"
                "as shown below in the list of files. \n"
                "Do '''NOT''' use the URL of the {{{[get]}}} link, \n"
                "since this is subject to change and can break easily."
            )
        str = str + "<ul>"

        label_del = _("del")
        label_move = _("move")
        label_get = _("get")
        label_edit = _("edit")
        label_view = _("view")
        label_unzip = _("unzip")
        label_install = _("install")

        for file in files:
            fsize = float(os.stat(os.path.join(attach_dir, file).encode(config.charset))[6]) # in byte
            fsize = "%.1f" % (fsize / 1024)
            baseurl = request.getScriptname()
            action = action_name
            urlpagename = wikiutil.quoteWikinameURL(pagename)
            urlfile = wikiutil.url_quote_plus(file)

            base, ext = os.path.splitext(file)
            get_url = getAttachUrl(pagename, file, request, escaped=1)
            parmdict = {'baseurl': baseurl, 'urlpagename': urlpagename, 'action': action,
                        'urlfile': urlfile, 'label_del': label_del,
                        'label_move': label_move,
                        'base': base, 'label_edit': label_edit,
                        'label_view': label_view,
                        'label_unzip': label_unzip,
                        'label_install': label_install,
                        'get_url': get_url, 'label_get': label_get,
                        'file': wikiutil.escape(file).replace(' ', '%20'),
                        'fsize': fsize,
                        'pagename': pagename}

            del_link = ''
            if request.user.may.delete(pagename) and not readonly:
                del_link = '<a href="%(baseurl)s/%(urlpagename)s' \
                    '?action=%(action)s&amp;do=del&amp;target=%(urlfile)s">%(label_del)s</a>&nbsp;| ' % parmdict
            if request.user.may.delete(pagename) and not readonly:
                move_link = '<a href="%(baseurl)s/%(urlpagename)s' \
                    '?action=%(action)s&amp;do=move&amp;target=%(urlfile)s">%(label_move)s</a>&nbsp;| ' % parmdict
            else:
                move_link = ''
            if ext == '.draw':
                viewlink = '<a href="%(baseurl)s/%(urlpagename)s?action=%(action)s&amp;drawing=%(base)s">%(label_edit)s</a>' % parmdict
            else:
                viewlink = '<a href="%(baseurl)s/%(urlpagename)s?action=%(action)s&amp;do=view&amp;target=%(urlfile)s">%(label_view)s</a>' % parmdict

            if (packages.ZipPackage(request, os.path.join(attach_dir, file).encode(config.charset)).isPackage() and
                request.user.isSuperUser()):
                viewlink += ' | <a href="%(baseurl)s/%(urlpagename)s?action=%(action)s&amp;do=install&amp;target=%(urlfile)s">%(label_install)s</a>' % parmdict
            elif (zipfile.is_zipfile(os.path.join(attach_dir, file).encode(config.charset)) and
                request.user.may.read(pagename) and request.user.may.delete(pagename)
                and request.user.may.write(pagename)):
                viewlink += ' | <a href="%(baseurl)s/%(urlpagename)s?action=%(action)s&amp;do=unzip&amp;target=%(urlfile)s">%(label_unzip)s</a>' % parmdict


            parmdict['viewlink'] = viewlink
            parmdict['del_link'] = del_link
            parmdict['move_link'] = move_link
            str = str + ('<li>[%(del_link)s%(move_link)s'
                '<a href="%(get_url)s">%(label_get)s</a>&nbsp;| %(viewlink)s]'
                ' (%(fsize)s KB) attachment:<strong>%(file)s</strong></li>') % parmdict
        str = str + "</ul>"
    else:
        if showheader:
            str = '%s<p>%s</p>' % (str, _("No attachments stored for %(pagename)s") % {'pagename': pagename})

    return str


def _get_files(request, pagename):
    attach_dir = getAttachDir(request, pagename)
    if os.path.isdir(attach_dir):
        files = map(lambda a: a.decode(config.charset), os.listdir(attach_dir))
        files.sort()
        return files
    return []


def _get_filelist(request, pagename):
    return _build_filelist(request, pagename, 1, 0)

def _subdir_exception(zf):
    """
    Checks for the existance of one common subdirectory shared among
    all files in the zip file. If this is the case, returns a dict of
    original names to modified names so that such files can be unpacked
    as the user would expect.
    """

    b = zf.namelist()
    if not '/' in b[0]:
        return False #No directory
    slashoffset = b[0].index('/')
    directory = b[0][:slashoffset]
    for origname in b:
        if origname.rfind('/') != slashoffset or origname[:slashoffset] != directory:
            return False #Multiple directories or different directory
    names = {}
    for origname in b:
        names[origname] = origname[slashoffset+1:]
    return names #Returns dict of {origname: safename}

def error_msg(pagename, request, msg):
    Page(request, pagename).send_page(request, msg=msg)


#############################################################################
### Create parts of the Web interface
#############################################################################

def send_link_rel(request, pagename):
    files = _get_files(request, pagename)
    if len(files) > 0 and not htdocs_access(request):
        scriptName = request.getScriptname()
        pagename_quoted = wikiutil.quoteWikinameURL(pagename)

        for file in files:
            url = "%s/%s?action=%s&do=view&target=%s" % (
                scriptName, pagename_quoted,
                action_name, wikiutil.url_quote_plus(file))

            request.write(u'<link rel="Appendix" title="%s" href="%s">\n' % (
                wikiutil.escape(file), wikiutil.escape(url)))


def send_hotdraw(pagename, request):
    _ = request.getText

    now = time.time()
    pubpath = request.cfg.url_prefix_static + "/applets/TWikiDrawPlugin"
    basename = request.form['drawing'][0]
    drawpath = getAttachUrl(pagename, basename + '.draw', request, escaped=1)
    pngpath = getAttachUrl(pagename, basename + '.png', request, escaped=1)
    querystr = {'action': 'AttachFile', 'ts': now}
    querystr = wikiutil.escape(wikiutil.makeQueryString(querystr))
    pagelink = '%s/%s?%s' % (request.getScriptname(), wikiutil.quoteWikinameURL(pagename), querystr)
    helplink = Page(request, "HelpOnActions/AttachFile").url(request)
    querystr = {'action': 'AttachFile', 'do': 'savedrawing'}
    querystr = wikiutil.escape(wikiutil.makeQueryString(querystr))
    savelink = '%s/%s?%s' % (request.getScriptname(), wikiutil.quoteWikinameURL(pagename), querystr)
    #savelink = Page(request, pagename).url(request) # XXX include target filename param here for twisted
                                           # request, {'savename': request.form['drawing'][0]+'.draw'}
    #savelink = '/cgi-bin/dumpform.bat'

    if htdocs_access(request):
        timestamp = '?ts=%s' % now
    else:
        timestamp = '&amp;ts=%s' % now

    request.write('<h2>' + _("Edit drawing") + '</h2>')
    request.write("""
<p>
<img src="%(pngpath)s%(timestamp)s">
<applet code="CH.ifa.draw.twiki.TWikiDraw.class"
        archive="%(pubpath)s/twikidraw.jar" width="640" height="480">
<param name="drawpath" value="%(drawpath)s">
<param name="pngpath"  value="%(pngpath)s">
<param name="savepath" value="%(savelink)s">
<param name="basename" value="%(basename)s">
<param name="viewpath" value="%(pagelink)s">
<param name="helppath" value="%(helplink)s">
<strong>NOTE:</strong> You need a Java enabled browser to edit the drawing example.
</applet>
</p>""" % {
    'pngpath': pngpath, 'timestamp': timestamp,
    'pubpath': pubpath, 'drawpath': drawpath,
    'savelink': savelink, 'pagelink': pagelink, 'helplink': helplink,
    'basename': basename
})


def send_uploadform(pagename, request):
    """ Send the HTML code for the list of already stored attachments and
        the file upload form.
    """
    _ = request.getText

    if not request.user.may.read(pagename):
        request.write('<p>%s</p>' % _('You are not allowed to view this page.'))
        return

    request.write('<h2>' + _("Attached Files") + '</h2>')
    request.write(_get_filelist(request, pagename))

    if not request.user.may.write(pagename):
        request.write('<p>%s</p>' % _('You are not allowed to attach a file to this page.'))
        return

    if request.form.get('drawing', [None])[0]:
        send_hotdraw(pagename, request)
        return

    request.write('<h2>' + _("New Attachment") + '</h2><p>' +
_("""An upload will never overwrite an existing file. If there is a name
conflict, you have to rename the file that you want to upload.
Otherwise, if "Rename to" is left blank, the original filename will be used.""") + '</p>')
    request.write("""
<form action="%(baseurl)s/%(pagename)s" method="POST" enctype="multipart/form-data">
<dl>
<dt>%(upload_label_file)s</dt>
<dd><input type="file" name="file" size="50"></dd>
<dt>%(upload_label_rename)s</dt>
<dd><input type="text" name="rename" size="50" value="%(rename)s"></dd>
</dl>
<p>
<input type="hidden" name="action" value="%(action_name)s">
<input type="hidden" name="do" value="upload">
<input type="submit" value="%(upload_button)s">
</p>
</form>
""" % {
    'baseurl': request.getScriptname(),
    'pagename': wikiutil.quoteWikinameURL(pagename),
    'action_name': action_name,
    'upload_label_file': _('File to upload'),
    'upload_label_rename': _('Rename to'),
    'rename': request.form.get('rename', [''])[0],
    'upload_button': _('Upload'),
})

#<dt>%(upload_label_mime)s</dt>
#<dd><input type="text" name="mime" size="50"></dd>
#    'upload_label_mime': _('MIME Type (optional)'),


#############################################################################
### Web interface for file upload, viewing and deletion
#############################################################################

def execute(pagename, request):
    """ Main dispatcher for the 'AttachFile' action.
    """
    _ = request.getText

    msg = None
    if action_name in request.cfg.actions_excluded:
        msg = _('File attachments are not allowed in this wiki!')
    elif not request.form.has_key('do'):
        upload_form(pagename, request)
    elif request.form['do'][0] == 'savedrawing':
        if request.user.may.write(pagename):
            save_drawing(pagename, request)
            request.emit_http_headers()
            request.write("OK")
        else:
            msg = _('You are not allowed to save a drawing on this page.')
    elif request.form['do'][0] == 'upload':
        if request.user.may.write(pagename):
            if request.form.has_key('file'):
                do_upload(pagename, request)
            else:
                # This might happen when trying to upload file names
                # with non-ascii characters on Safari.
                msg = _("No file content. Delete non ASCII characters from the file name and try again.")
        else:
            msg = _('You are not allowed to attach a file to this page.')
    elif request.form['do'][0] == 'del':
        if request.user.may.delete(pagename):
            del_file(pagename, request)
        else:
            msg = _('You are not allowed to delete attachments on this page.')
    elif request.form['do'][0] == 'move':
        if request.user.may.delete(pagename):
            send_moveform(pagename, request)
        else:
            msg = _('You are not allowed to move attachments from this page.')
    elif request.form['do'][0] == 'attachment_move':
        if request.form.has_key('cancel'):
            msg = _('Move aborted!')
            error_msg(pagename, request, msg)
            return
        if not wikiutil.checkTicket(request.form['ticket'][0]):
            msg = _('Please use the interactive user interface to move attachments!')
            error_msg(pagename, request, msg)
            return
        if request.user.may.delete(pagename):
            attachment_move(pagename, request)
        else:
            msg = _('You are not allowed to move attachments from this page.')
    elif request.form['do'][0] == 'get':
        if request.user.may.read(pagename):
            get_file(pagename, request)
        else:
            msg = _('You are not allowed to get attachments from this page.')
    elif request.form['do'][0] == 'unzip':
         if request.user.may.delete(pagename) and request.user.may.read(pagename) and request.user.may.write(pagename):
            unzip_file(pagename, request)
         else:
            msg = _('You are not allowed to unzip attachments of this page.')
    elif request.form['do'][0] == 'install':
         if request.user.isSuperUser():
            install_package(pagename, request)
         else:
            msg = _('You are not allowed to install files.')
    elif request.form['do'][0] == 'view':
        if request.user.may.read(pagename):
            view_file(pagename, request)
        else:
            msg = _('You are not allowed to view attachments of this page.')
    else:
        msg = _('Unsupported upload action: %s') % (request.form['do'][0],)

    if msg:
        error_msg(pagename, request, msg)

def upload_form(pagename, request, msg=''):
    _ = request.getText

    request.emit_http_headers()
    # Use user interface language for this generated page
    request.setContentLanguage(request.lang)
    request.theme.send_title(_('Attachments for "%(pagename)s"') % {'pagename': pagename}, pagename=pagename, msg=msg)
    request.write('<div id="content">\n') # start content div
    send_uploadform(pagename, request)
    request.write('</div>\n') # end content div
    request.theme.send_footer(pagename)
    request.theme.send_closing_html()

def do_upload(pagename, request):
    _ = request.getText

    # make filename
    filename = None
    if request.form.has_key('file__filename__'):
        filename = request.form['file__filename__']
    rename = None
    if request.form.has_key('rename'):
        rename = request.form['rename'][0].strip()

    # if we use twisted, "rename" field is NOT optional, because we
    # can't access the client filename
    if rename:
        target = rename
    elif filename:
        target = filename
    else:
        error_msg(pagename, request, _("Filename of attachment not specified!"))
        return

    # get file content
    filecontent = request.form['file'][0]

    # preprocess the filename
    # strip leading drive and path (IE misbehaviour)
    if len(target) > 1 and (target[1] == ':' or target[0] == '\\'): # C:.... or \path... or \\server\...
        bsindex = target.rfind('\\')
        if bsindex >= 0:
            target = target[bsindex+1:]

    # add the attachment
    try:
        add_attachment(request, pagename, target, filecontent)

        bytes = len(filecontent)
        msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
                " with %(bytes)d bytes saved.") % {
                'target': target, 'filename': filename, 'bytes': bytes}
    except AttachmentAlreadyExists:
        msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
            'target': target, 'filename': filename}

    # return attachment list
    upload_form(pagename, request, msg)


def save_drawing(pagename, request):

    filename = request.form['filename'][0]
    filecontent = request.form['filepath'][0]

    # there should be no difference in filename parsing with or without
    # htdocs_access, cause the filename param is used
    basepath, basename = os.path.split(filename)
    basename, ext = os.path.splitext(basename)

    # get directory, and possibly create it
    attach_dir = getAttachDir(request, pagename, create=1)

    if ext == '.draw':
        _addLogEntry(request, 'ATTDRW', pagename, basename + ext)
        filecontent = filecontent.replace("\r", "")

    savepath = os.path.join(getAttachDir(request, pagename), basename + ext)
    if ext == '.map' and not filecontent.strip():
        # delete map file if it is empty
        os.unlink(savepath)
    else:
        stream = open(savepath, 'wb')
        try:
            stream.write(filecontent)
        finally:
            stream.close()

    # touch attachment directory to invalidate cache if new map is saved
    if ext == '.map':
        os.utime(getAttachDir(request, pagename), None)

def del_file(pagename, request):
    _ = request.getText

    filename, fpath = _access_file(pagename, request)
    if not filename: return # error msg already sent in _access_file

    # delete file
    os.remove(fpath)
    _addLogEntry(request, 'ATTDEL', pagename, filename)

    if request.cfg.xapian_search:
        from MoinMoin.search.Xapian import Index
        index = Index(request)
        if index.exists:
            index.remove_item(pagename, filename)

    upload_form(pagename, request, msg=_("Attachment '%(filename)s' deleted.") % {'filename': filename})

def move_file(request, pagename, new_pagename, attachment, new_attachment):
    _ = request.getText

    newpage = Page(request, new_pagename)
    if newpage.exists(includeDeleted=1) and request.user.may.write(new_pagename) and request.user.may.delete(pagename):
        new_attachment_path = os.path.join(getAttachDir(request, new_pagename,
                              create=1), new_attachment).encode(config.charset)
        attachment_path = os.path.join(getAttachDir(request, pagename),
                          attachment).encode(config.charset)

        if os.path.exists(new_attachment_path):
            upload_form(pagename, request, msg=_("Attachment '%(filename)s' already exists.") % {
                                   'filename': new_attachment})
            return

        if new_attachment_path != attachment_path:
        # move file  
            filesys.rename(attachment_path, new_attachment_path)
            _addLogEntry(request, 'ATTDEL', pagename, attachment)
            _addLogEntry(request, 'ATTNEW', new_pagename, new_attachment)
            upload_form(pagename, request, msg=_("Attachment '%(filename)s' moved to %(page)s.") % {
                                                 'filename': new_attachment,
                                                 'page': new_pagename})
        else:
            upload_form(pagename, request, msg=_("Nothing changed"))
    else:
         upload_form(pagename, request, msg=_("Page %(newpagename)s does not exists or you don't have enough rights.") % {
             'newpagename': new_pagename})

def attachment_move(pagename, request):
    _ = request.getText
    if request.form.has_key('newpagename'):
        new_pagename = request.form.get('newpagename')[0]
    else:
        upload_form(pagename, request, msg=_("Move aborted because empty page name"))
    if request.form.has_key('newattachmentname'):
        new_attachment = request.form.get('newattachmentname')[0]
        if new_attachment != wikiutil.taintfilename(new_attachment):
            upload_form(pagename, request, msg=_("Please use proper signs in attachment '%(filename)s'.") % {
                                  'filename': new_attachment})
            return
    else:
        upload_form(pagename, request, msg=_("Move aborted because empty attachment name"))

    attachment = request.form.get('oldattachmentname')[0]
    move_file(request, pagename, new_pagename, attachment, new_attachment)

def send_moveform(pagename, request):
    _ = request.getText

    filename, fpath = _access_file(pagename, request)
    if not filename: return # error msg already sent in _access_file

    # move file
    d = {'action': 'AttachFile',
         'do': 'attachment_move',
         'ticket': wikiutil.createTicket(),
         'pagename': pagename,
         'attachment_name': filename,
         'move': _('Move'),
         'cancel': _('Cancel'),
         'newname_label': _("New page name"),
         'attachment_label': _("New attachment name"),
        }
    formhtml = '''
<form method="post" action="">
<input type="hidden" name="action" value="%(action)s">
<input type="hidden" name="do" value="%(do)s">
<input type="hidden" name="ticket" value="%(ticket)s">
<table>
    <tr>
        <td class="label"><label>%(newname_label)s</label></td>
        <td class="content">
            <input type="text" name="newpagename" value="%(pagename)s">
        </td>
    </tr>
    <tr>
        <td class="label"><label>%(attachment_label)s</label></td>
        <td class="content">
            <input type="text" name="newattachmentname" value="%(attachment_name)s">
        </td>
    </tr>
    <tr>
        <td></td>
        <td class="buttons">
            <input type="hidden" name="oldattachmentname" value="%(attachment_name)s">
            <input type="submit" name="move" value="%(move)s">
            <input type="submit" name="cancel" value="%(cancel)s">
        </td>
    </tr>
</table>
</form>''' % d
    thispage = Page(request, pagename)
    return thispage.send_page(request, msg=formhtml)

def get_file(pagename, request):
    import shutil

    filename, fpath = _access_file(pagename, request)
    if not filename:
        return # error msg already sent in _access_file

    timestamp = timefuncs.formathttpdate(int(os.path.getmtime(fpath)))
    if request.if_modified_since == timestamp:
        request.emit_http_headers(["Status: 304 Not modified"])
    else:
        mt = wikiutil.MimeType(filename=filename)
        content_type = mt.content_type()
        mime_type = mt.mime_type()

        # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
        # There is no solution that is compatible to IE except stripping non-ascii chars
        filename_enc = filename.encode(config.charset)

        # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
        # we just let the user store them to disk ('attachment').
        # For safe files, we directly show them inline (this also works better for IE).
        dangerous = mime_type in request.cfg.mimetypes_xss_protect
        content_dispo = dangerous and 'attachment' or 'inline'

        request.emit_http_headers([
            'Content-Type: %s' % content_type,
            'Last-Modified: %s' % timestamp, # TODO maybe add a short Expires: header here?
            'Content-Length: %d' % os.path.getsize(fpath),
            'Content-Disposition: %s; filename="%s"' % (content_dispo, filename_enc),
        ])

        # send data
        shutil.copyfileobj(open(fpath, 'rb'), request, 8192)

def install_package(pagename, request):
    _ = request.getText

    target, targetpath = _access_file(pagename, request)
    if not target:
        return

    package = packages.ZipPackage(request, targetpath)

    if package.isPackage():
        if package.installPackage():
            msg = _("Attachment '%(filename)s' installed.") % {'filename': wikiutil.escape(target)}
        else:
            msg = _("Installation of '%(filename)s' failed.") % {'filename': wikiutil.escape(target)}
        if package.msg != "":
            msg += "<br><pre>" + wikiutil.escape(package.msg) + "</pre>"
    else:
        msg = _('The file %s is not a MoinMoin package file.' % wikiutil.escape(target))

    upload_form(pagename, request, msg=msg)

def unzip_file(pagename, request):
    _ = request.getText
    valid_pathname = lambda name: ('/' not in name) and ('\\' not in name)

    filename, fpath = _access_file(pagename, request)
    if not filename:
        return # error msg already sent in _access_file

    attachment_path = getAttachDir(request, pagename)
    single_file_size = request.cfg.unzip_single_file_size
    attachments_file_space = request.cfg.unzip_attachments_space
    attachments_file_count = request.cfg.unzip_attachments_count

    files = _get_files(request, pagename)

    msg = ""
    if files:
        fsize = 0.0
        fcount = 0
        for file in files:
            fsize += float(os.stat(getFilename(request, pagename, file))[6]) # in byte
            fcount += 1

        available_attachments_file_space = attachments_file_space - fsize
        available_attachments_file_count = attachments_file_count - fcount

        if zipfile.is_zipfile(fpath):
            zf = zipfile.ZipFile(fpath)
            sum_size_over_all_valid_files = 0.0
            count_valid_files = 0
            namelist = _subdir_exception(zf)
            if not namelist: #if it's not handled by _subdir_exception()
                #Convert normal zf.namelist() to {origname:finalname} dict
                namelist = {}
                for name in zf.namelist():
                    namelist[name] = name
            for (origname, finalname) in namelist.iteritems():
                if valid_pathname(finalname):
                    sum_size_over_all_valid_files += zf.getinfo(origname).file_size
                    count_valid_files += 1

            if sum_size_over_all_valid_files > available_attachments_file_space:
                msg = _("Attachment '%(filename)s' could not be unzipped because"
                        " the resulting files would be too large (%(space)d kB"
                        " missing).") % {
                            'filename': filename,
                            'space': (sum_size_over_all_valid_files -
                                available_attachments_file_space) / 1000 }
            elif count_valid_files > available_attachments_file_count:
                msg = _("Attachment '%(filename)s' could not be unzipped because"
                        " the resulting files would be too many (%(count)d "
                        "missing).") % {
                            'filename': filename,
                            'count': (count_valid_files -
                                available_attachments_file_count) }
            else:
                valid_name = False
                for (origname, finalname) in namelist.iteritems():
                    if valid_pathname(finalname):
                        zi = zf.getinfo(origname)
                        if zi.file_size < single_file_size:
                            new_file = getFilename(request, pagename, finalname)
                            if not os.path.exists(new_file):
                                outfile = open(new_file, 'wb')
                                outfile.write(zf.read(origname))
                                outfile.close()
                                # it's not allowed to zip a zip file so it is dropped
                                if zipfile.is_zipfile(new_file):
                                    os.unlink(new_file)
                                else:
                                    valid_name = True
                                    _addLogEntry(request, 'ATTNEW', pagename, finalname)

                if valid_name:
                    msg = _("Attachment '%(filename)s' unzipped.") % {'filename': filename}
                else:
                    msg = _("Attachment '%(filename)s' not unzipped because the "
                            "files are too big, .zip files only, exist already or "
                            "reside in folders.") % {'filename': filename}
        else:
            msg = _('The file %(target)s is not a .zip file.' % target)

    upload_form(pagename, request, msg=wikiutil.escape(msg))

def send_viewfile(pagename, request):
    _ = request.getText

    filename, fpath = _access_file(pagename, request)
    if not filename: return

    request.write('<h2>' + _("Attachment '%(filename)s'") % {'filename': filename} + '</h2>')

    mt = wikiutil.MimeType(filename=filename)
    if mt.major == 'image':
        timestamp = htdocs_access(request) and "?%s" % time.time() or ''
        request.write('<img src="%s%s" alt="%s">' % (
            getAttachUrl(pagename, filename, request, escaped=1), timestamp, wikiutil.escape(filename, 1)))
        return
    elif mt.major == 'text':
        # TODO: should use formatter here!
        request.write("<pre>")
        # Try to decode file contents. It may return junk, but we
        # don't have enough information on attachments.
        content = open(fpath, 'r').read()
        content = wikiutil.decodeUnknownInput(content)
        content = wikiutil.escape(content)
        request.write(content)
        request.write("</pre>")
        return

    package = packages.ZipPackage(request, fpath)
    if package.isPackage():
        request.write("<pre><b>%s</b>\n%s</pre>" % (_("Package script:"), wikiutil.escape(package.getScript())))
        return

    import zipfile
    if zipfile.is_zipfile(fpath):
        zf = zipfile.ZipFile(fpath, mode='r')
        request.write("<pre>%-46s %19s %12s\n" % (_("File Name"), _("Modified")+" "*5, _("Size")))
        for zinfo in zf.filelist:
            date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time
            request.write(wikiutil.escape("%-46s %s %12d\n" % (zinfo.filename, date, zinfo.file_size)))
        request.write("</pre>")
        return

    request.write('<p>' + _("Unknown file type, cannot display this attachment inline.") + '</p>')
    request.write('<a href="%s">%s</a>' % (
        getAttachUrl(pagename, filename, request, escaped=1), wikiutil.escape(filename)))


def view_file(pagename, request):
    _ = request.getText

    filename, fpath = _access_file(pagename, request)
    if not filename: return

    # send header & title
    request.emit_http_headers()
    # Use user interface language for this generated page
    request.setContentLanguage(request.lang)
    title = _('attachment:%(filename)s of %(pagename)s', formatted=True) % {
        'filename': filename, 'pagename': pagename}
    request.theme.send_title(title, pagename=pagename)

    # send body
    # TODO: use formatter startContent?
    request.write('<div id="content">\n') # start content div
    send_viewfile(pagename, request)
    send_uploadform(pagename, request)
    request.write('</div>\n') # end content div

    request.theme.send_footer(pagename)
    request.theme.send_closing_html()

#############################################################################
### File attachment administration
#############################################################################

def do_admin_browser(request):
    """ Browser for SystemAdmin macro.
    """
    from MoinMoin.util.dataset import TupleDataset, Column
    _ = request.getText

    data = TupleDataset()
    data.columns = [
        Column('page', label=('Page')),
        Column('file', label=('Filename')),
        Column('size', label=_('Size'), align='right'),
        #Column('action', label=_('Action')),
    ]

    # iterate over pages that might have attachments
    pages = request.rootpage.getPageList()
    for pagename in pages:
        # check for attachments directory
        page_dir = getAttachDir(request, pagename)
        if os.path.isdir(page_dir):
            # iterate over files of the page
            files = os.listdir(page_dir)
            for filename in files:
                filepath = os.path.join(page_dir, filename)
                data.addRow((
                    Page(request, pagename).link_to(request, querystr="action=AttachFile"),
                    wikiutil.escape(filename.decode(config.charset)),
                    os.path.getsize(filepath),
                    # '',
                ))

    if data:
        from MoinMoin.widget.browser import DataBrowserWidget

        browser = DataBrowserWidget(request)
        browser.setData(data)
        return browser.toHTML()

    return ''