view MoinMoin/action/AttachFile.py @ 116:de898247fbff

limit file count when unzipping an attachment imported from: moin--main--1.5--patch-118
author Thomas Waldmann <tw@waldmann-edv.de>
date Mon, 17 Oct 2005 23:29:32 +0000
parents 7785febd9530
children 4775d5ff5f40
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 R. Bauer
    @copyright: 2005 MoinMoin:AlexanderSchremmer
    @license: GNU GPL, see COPYING for details.
"""

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

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

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


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

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):
            filesys.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 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)

    return attach_link


def getFilename(request, pagename, name):
    return os.path.join(getAttachDir(request, pagename), name).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


#############################################################################
### 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_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,
                        '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), '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 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.name in request.cfg.superuser):
                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
            str = str + ('<li>[%(del_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 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 + "/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)
    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 request.form.has_key('filepath'):
        if request.user.may.write(pagename):
            save_drawing(pagename, request)
            request.http_headers()
            request.write("OK")
        else:
            msg = _('You are not allowed to save a drawing on this page.')
    elif not request.form.has_key('do'):
        upload_form(pagename, request)
    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] == '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.name in request.cfg.superuser:
            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.http_headers()
    # Use user interface language for this generated page
    request.setContentLanguage(request.lang)
    wikiutil.send_title(request, _('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
    wikiutil.send_footer(request, pagename, showpage=1)


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
    # 1. 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:]
        
    # 2. replace illegal chars
    target = wikiutil.taintfilename(target)

    # set mimetype from extension, or from given mimetype
    #type, encoding = mimetypes.guess_type(target)
    #if not type:
    #    ext = None
    #    if request.form.has_key('mime'):
    #        ext = mimetypes.guess_extension(request.form['mime'][0])
    #    if not ext:
    #        type, encoding = mimetypes.guess_type(filename)
    #        if type:
    #            ext = mimetypes.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):
        msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
            'target': target, 'filename': filename}
    else:
        stream = open(fpath, 'wb')
        try:
            stream.write(filecontent)
        finally:
            stream.close()
        os.chmod(fpath, 0666 & config.umask)

        bytes = len(filecontent)
        msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
                " with %(bytes)d bytes saved.") % {
                'target': target, 'filename': filename, 'bytes': bytes}
        _addLogEntry(request, 'ATTNEW', pagename, target)

    # 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 filecontent.strip()=='':
        # delete map file if it is empty
        os.unlink(savepath)
    else:
        file = open(savepath, 'wb')
        try:
            file.write(filecontent)
        finally:
            file.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)

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


def get_file(pagename, request):
    import shutil

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

    # get mimetype
    type, enc = mimetypes.guess_type(filename)
    if not type:
        type = "application/octet-stream"

    # send header
    request.http_headers([
        "Content-Type: %s" % type,
        "Content-Length: %d" % os.path.getsize(fpath),
        # 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
        "Content-Disposition: inline; filename=\"%s\"" % filename.encode(config.charset),
    ])

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

    raise MoinMoinNoFooter

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: (name.find('/') == -1) and (name.find('\\') == -1)

    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
    max_file_count = request.cfg.unzip_file_count

    files = _get_files(request, pagename)

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

        available_attachments_file_space = attachments_file_space - fsize

        if zipfile.is_zipfile(fpath):
            zf = zipfile.ZipFile(fpath)
            sum_size_over_all_valid_files = 0.0
            count_valid_files = 0
            for name in zf.namelist():
                if valid_pathname(name):
                    sum_size_over_all_valid_files += zf.getinfo(name).file_size
                    count_valid_files += 1

            if (sum_size_over_all_valid_files < available_attachments_file_space and
                count_valid_files < max_file_count):
                valid_name = False
                for name in zf.namelist():
                    if valid_pathname(name):
                        zi = zf.getinfo(name)
                        if zi.file_size < single_file_size:
                            new_file = getFilename(request, pagename, name)
                            if not os.path.exists(new_file):
                                outfile = open(new_file, 'wb')
                                outfile.write(zf.read(name))
                                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
                                    os.chmod(new_file, 0666 & config.umask)
                                    _addLogEntry(request, 'ATTNEW', pagename, new_file)

                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=_("Attachment '%(filename)s' could not be unzipped because"
                      " the resulting files would be too large (%(space)d kB"
                      " missing) or more than %(count)d files.") % {
                        'filename': filename,
                        'space': (sum_size_over_all_valid_files -
                              available_attachments_file_space) / 1000,
                        'count': max_file_count }
        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>')

    type, enc = mimetypes.guess_type(filename)
    if type:
        if type[:5] == '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 type[:4] == '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.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}
    wikiutil.send_title(request, 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

    # send footer
    wikiutil.send_footer(request, pagename)


#############################################################################
### 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 ''