# -*- 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 link to an attachment, use [[attachment:file.txt]],
    to embed an attachment, use {{attachment:file.png}}.

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

import os, time, zipfile, errno, datetime
from StringIO import StringIO
import tarfile

from werkzeug.http import http_date

from MoinMoin import log
logging = log.getLogger(__name__)

# keep both imports below as they are, order is important:
from MoinMoin import wikiutil
import mimetypes

from MoinMoin import config, packages
from MoinMoin.Page import Page
from MoinMoin.util import filesys, timefuncs
from MoinMoin.security.textcha import TextCha
from MoinMoin.events import FileAttachedEvent, FileRemovedEvent, send_event

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

#############################################################################
### 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. """
    return request.rootpage.getPagePath('pages')


def getAttachDir(request, pagename, create=0):
    """ Get directory where attachments for page `pagename` are stored. """
    if request.page and pagename == request.page.page_name:
        page = request.page # reusing existing page obj is faster
    else:
        page = Page(request, pagename)
    return page.getPagePath("attachments", check_create=create)


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
    """
    url = wikiutil.AbsPageName(pagename, url)
    pieces = url.split(u'/')
    if len(pieces) == 1:
        return pagename, pieces[0]
    else:
        return u"/".join(pieces[:-1]), pieces[-1]


def get_action(request, filename, do):
    generic_do_mapping = {
        # do -> action
        'get': action_name,
        'view': action_name,
        'move': action_name,
        'del': action_name,
        'unzip': action_name,
        'install': action_name,
        'upload_form': action_name,
    }
    basename, ext = os.path.splitext(filename)
    do_mapping = request.cfg.extensions_mapping.get(ext, {})
    action = do_mapping.get(do, None)
    if action is None:
        # we have no special support for this,
        # look up whether we have generic support:
        action = generic_do_mapping.get(do, None)
    return action


def getAttachUrl(pagename, filename, request, addts=0, do='get'):
    """ Get URL that points to attachment `filename` of page `pagename`.
        For upload url, call with do='upload_form'.
        Returns the URL to do the specified "do" action or None,
        if this action is not supported.
    """
    action = get_action(request, filename, do)
    if action:
        args = dict(action=action, do=do, target=filename)
        if do not in ['get', 'view', # harmless
                      'modify', # just renders the applet html, which has own ticket
                      'move', # renders rename form, which has own ticket
            ]:
            # create a ticket for the not so harmless operations
            # we need action= here because the current action (e.g. "show" page
            # with a macro AttachList) may not be the linked-to action, e.g.
            # "AttachFile". Also, AttachList can list attachments of another page,
            # thus we need to give pagename= also.
            args['ticket'] = wikiutil.createTicket(request,
                                                   pagename=pagename, action=action_name)
        url = request.href(pagename, **args)
        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 ''

    fmt = request.formatter
    attach_count = _('[%d attachments]') % len(files)
    attach_icon = request.theme.make_icon('attach', vars={'attach_count': attach_count})
    attach_link = (fmt.url(1, request.href(pagename, action=action_name), rel='nofollow') +
                   attach_icon +
                   fmt.url(0))
    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
    """
    if isinstance(filename, unicode):
        filename = filename.encode(config.charset)
    return os.path.join(getAttachDir(request, pagename, create=1), filename)


def exists(request, pagename, filename):
    """ check if page <pagename> has a file <filename> attached """
    fpath = getFilename(request, pagename, filename)
    return os.path.exists(fpath)


def size(request, pagename, filename):
    """ return file size of file attachment """
    fpath = getFilename(request, pagename, filename)
    return os.path.getsize(fpath)


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)
    link = page.url(request, {'action': action_name})
    attach_info = _('There are <a href="%(link)s">%(count)s attachment(s)</a> stored for this page.') % {
        'count': len(files),
        'link': wikiutil.escape(link)
        }
    return "\n<p>\n%s\n</p>\n" % attach_info


def _write_stream(content, stream, bufsize=8192):
    if hasattr(content, 'read'): # looks file-like
        import shutil
        shutil.copyfileobj(content, stream, bufsize)
    elif isinstance(content, str):
        stream.write(content)
    else:
        msg = "unsupported content object: %r" % content
        logging.error(msg)
        raise Exception(msg)

def add_attachment(request, pagename, target, filecontent, overwrite=0):
    """ save <filecontent> to an attachment <target> of page <pagename>

        filecontent can be either a str (in memory file content),
        or an open file object (file content in e.g. a tempfile).
    """
    # replace illegal chars
    target = wikiutil.taintfilename(target)

    # get directory, and possibly create it
    attach_dir = getAttachDir(request, pagename, create=1)
    fpath = os.path.join(attach_dir, target).encode(config.charset)

    exists = os.path.exists(fpath)
    if exists:
        if overwrite:
            remove_attachment(request, pagename, target)
        else:
            raise AttachmentAlreadyExists

    # save file
    stream = open(fpath, 'wb')
    try:
        _write_stream(filecontent, stream)
    finally:
        stream.close()

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

    filesize = os.path.getsize(fpath)
    event = FileAttachedEvent(request, pagename, target, filesize)
    send_event(event)

    return target, filesize


def remove_attachment(request, pagename, target):
    """ remove attachment <target> of page <pagename>
    """
    # replace illegal chars
    target = wikiutil.taintfilename(target)

    # get directory, do not create it
    attach_dir = getAttachDir(request, pagename, create=0)
    # remove file
    fpath = os.path.join(attach_dir, target).encode(config.charset)
    try:
        filesize = os.path.getsize(fpath)
        os.remove(fpath)
    except:
        # either it is gone already or we have no rights - not much we can do about it
        filesize = 0
    else:
        _addLogEntry(request, 'ATTDEL', pagename, target)

        event = FileRemovedEvent(request, pagename, target, filesize)
        send_event(event)

    return target, filesize


class SamePath(Exception):
    """
    raised if an attachment move is attempted to same target path
    """

class DestPathExists(Exception):
    """
    raised if an attachment move is attempted to an existing target path
    """


def move_attachment(request, pagename, dest_pagename, target, dest_target,
                    overwrite=False):
    """ move attachment <target> of page <pagename>
        to attachment <dest_target> of page <dest_pagename>

        note: this is lowlevel code, acl permissions need to be checked before
              and also the target page should somehow exist (can be "deleted",
              but the pagedir should be there)
    """
    # replace illegal chars
    target = wikiutil.taintfilename(target)
    dest_target = wikiutil.taintfilename(dest_target)

    attachment_path = os.path.join(getAttachDir(request, pagename),
                                   target).encode(config.charset)
    dest_attachment_path = os.path.join(getAttachDir(request, dest_pagename, create=1),
                                        dest_target).encode(config.charset)
    if not overwrite and os.path.exists(dest_attachment_path):
        raise DestPathExists
    if dest_attachment_path == attachment_path:
        raise SamePath
    filesize = os.path.getsize(attachment_path)
    try:
        filesys.rename(attachment_path, dest_attachment_path)
    except Exception:
        raise
    else:
        _addLogEntry(request, 'ATTDEL', pagename, target)
        event = FileRemovedEvent(request, pagename, target, filesize)
        send_event(event)
        _addLogEntry(request, 'ATTNEW', dest_pagename, dest_target)
        event = FileAttachedEvent(request, dest_pagename, dest_target, filesize)
        send_event(event)

    return dest_target, filesize


def copy_attachment(request, pagename, dest_pagename, target, dest_target,
                    overwrite=False):
    """ copy attachment <target> of page <pagename>
        to attachment <dest_target> of page <dest_pagename>

        note: this is lowlevel code, acl permissions need to be checked before
              and also the target page should somehow exist (can be "deleted",
              but the pagedir should be there)
    """
    # replace illegal chars
    target = wikiutil.taintfilename(target)
    dest_target = wikiutil.taintfilename(dest_target)

    attachment_path = os.path.join(getAttachDir(request, pagename),
                                   target).encode(config.charset)
    dest_attachment_path = os.path.join(getAttachDir(request, dest_pagename, create=1),
                                        dest_target).encode(config.charset)
    if not overwrite and os.path.exists(dest_attachment_path):
        raise DestPathExists
    if dest_attachment_path == attachment_path:
        raise SamePath
    filesize = os.path.getsize(attachment_path)
    try:
        filesys.copy(attachment_path, dest_attachment_path)
    except Exception:
        raise
    else:
        _addLogEntry(request, 'ATTNEW', dest_pagename, dest_target)
        event = FileAttachedEvent(request, dest_pagename, dest_target, filesize)
        send_event(event)

    return dest_target, filesize


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

    # 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
        `(pagename, filename, filepath)` for an existing attachment.

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

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

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

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


def _build_filelist(request, pagename, showheader, readonly, mime_type='*', filterfn=None):
    _ = request.getText
    fmt = request.html_formatter

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

    if mime_type != '*':
        files = [fname for fname in files if mime_type == mimetypes.guess_type(fname)[0]]
    if filterfn is not None:
        files = [fname for fname in files if filterfn(fname)]

    html = []
    if files:
        if showheader:
            html.append(fmt.rawHTML(_(
                "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.",
                wiki=True
            )))

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

        may_read = request.user.may.read(pagename)
        may_write = request.user.may.write(pagename)
        may_delete = request.user.may.delete(pagename)

        html.append(u"""\
<script>
function checkAll(bx, targets_name) {
  var cbs = document.getElementsByTagName('input');
  for(var i=0; i < cbs.length; i++) {
    if(cbs[i].type == 'checkbox' && cbs[i].name == targets_name) {
      cbs[i].checked = bx.checked;
    }
  }
}
</script>
<form method="POST">
<input type="hidden" name="action" value="AttachFile">
<input type="hidden" name="do" value="multifile">
""")

        html.append(fmt.bullet_list(1))
        for file in files:
            mt = wikiutil.MimeType(filename=file)
            fullpath = os.path.join(attach_dir, file).encode(config.charset)
            st = os.stat(fullpath)
            base, ext = os.path.splitext(file)
            parmdict = {'file': wikiutil.escape(file),
                        'fsize': "%.1f" % (float(st.st_size) / 1024),
                        'fmtime': request.user.getFormattedDateTime(st.st_mtime),
                       }

            links = []
            if may_delete and not readonly:
                links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='del')) +
                             fmt.text(label_del) +
                             fmt.url(0))

            if may_delete and not readonly:
                links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='move')) +
                             fmt.text(label_move) +
                             fmt.url(0))

            links.append(fmt.url(1, getAttachUrl(pagename, file, request)) +
                         fmt.text(label_get) +
                         fmt.url(0))

            links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
                         fmt.text(label_view) +
                         fmt.url(0))

            if may_write and not readonly:
                edit_url = getAttachUrl(pagename, file, request, do='modify')
                if edit_url:
                    links.append(fmt.url(1, edit_url) +
                                 fmt.text(label_edit) +
                                 fmt.url(0))

            try:
                is_zipfile = zipfile.is_zipfile(fullpath)
                if is_zipfile and not readonly:
                    is_package = packages.ZipPackage(request, fullpath).isPackage()
                    if is_package and request.user.isSuperUser():
                        links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='install')) +
                                     fmt.text(label_install) +
                                     fmt.url(0))
                    elif (not is_package and mt.minor == 'zip' and
                          may_read and may_write and may_delete):
                        links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='unzip')) +
                                     fmt.text(label_unzip) +
                                     fmt.url(0))
            except (IOError, RuntimeError, zipfile.BadZipfile, zipfile.LargeZipFile):
                # We don't want to crash with a traceback here (an exception
                # here could be caused by an uploaded defective zip file - and
                # if we crash here, the user does not get a UI to remove the
                # defective zip file again).
                # RuntimeError is raised by zipfile stdlib module in case of
                # problems (like inconsistent slash and backslash usage in the
                # archive).
                # BadZipfile/LargeZipFile are raised when there are some
                # specific problems with the archive file.
                logging.exception("An exception within zip file attachment [%r:%r] handling occurred:" % (
                     pagename, file))

            html.append(fmt.listitem(1))
            html.append("[%s]" % "&nbsp;| ".join(links))
            html.append('''<input type="checkbox" name="fn" value="%s">''' % file)
            html.append(" (%(fmtime)s, %(fsize)s KB) [[attachment:%(file)s]]" % parmdict)
            html.append(fmt.listitem(0))
        html.append(fmt.bullet_list(0))
        html.append(u"""\
<input type="checkbox" onclick="checkAll(this, 'fn')">\
&nbsp;%(all_files)s&nbsp;|&nbsp;%(sel_files)s
<input type="radio" name="multifile" value="rm">%(delete)s</input>
<input type="radio" name="multifile" value="mv">%(move)s</input>
<input type="radio" name="multifile" value="cp">%(copy)s</input>
<input type="text" name="multi_dest_pagename" value="%(pagename)s">
<input type="submit" value="%(submit)s">
""" % dict(
            all_files=_('All files'),
            sel_files=_("Selected Files:"),
            delete=_("delete"),
            move=_("move to page"),
            copy=_("copy to page"),
            pagename=wikiutil.escape(pagename),
            submit=_("Do it."),
))
        html.append("</form>")

    else:
        if showheader:
            html.append(fmt.paragraph(1))
            html.append(fmt.text(_("No attachments stored for %(pagename)s") % {
                                   'pagename': pagename}))
            html.append(fmt.paragraph(0))

    return ''.join(html)


def _do_multifile(pagename, request):
    _ = request.getText
    action = request.form.get('multifile')
    fnames = request.form.getlist('fn')
    fails = []
    if action == 'rm':
        if not request.user.may.delete(pagename):
            return _('You are not allowed to delete attachments on this page.')
        for fn in fnames:
            remove_attachment(request, pagename, fn)
        msg = _("Attachment '%(filename)s' deleted.") % dict(
                filename=u'{%s}' % ','.join(fnames))
        return upload_form(pagename, request, msg=msg)
    if action == 'mv':
        if not request.user.may.delete(pagename):
            return _('You are not allowed to move attachments from this page.')
        dest_pagename = request.form.get('multi_dest_pagename')
        if not request.user.may.write(dest_pagename):
            return _('You are not allowed to attach a file to this page.')
        for fn in fnames:
            try:
                move_attachment(request, pagename, dest_pagename, fn, fn)
            except (DestPathExists, SamePath):
                fails.append(fn)
        msg = _("Attachment '%(pagename)s/%(filename)s' moved to '%(new_pagename)s/%(new_filename)s'.") % dict(
                pagename=pagename,
                filename=u'{%s}' % ','.join(fnames),
                new_pagename=dest_pagename,
                new_filename=u'*')
        if fails:
            msg += " " + _("Failed: %s") % ", ".join(fails)
        return upload_form(pagename, request, msg=msg)
    if action == 'cp':
        dest_pagename = request.form.get('multi_dest_pagename')
        if not request.user.may.write(dest_pagename):
            return _('You are not allowed to attach a file to this page.')
        for fn in fnames:
            try:
                copy_attachment(request, pagename, dest_pagename, fn, fn)
            except (DestPathExists, SamePath):
                fails.append(fn)
        msg = _("Attachment '%(pagename)s/%(filename)s' copied to '%(new_pagename)s/%(new_filename)s'.") % dict(
                pagename=pagename,
                filename=u'{%s}' % ','.join(fnames),
                new_pagename=dest_pagename,
                new_filename=u'*')
        if fails:
            msg += " " + _("Failed: %s") % ", ".join(fails)
        return upload_form(pagename, request, msg=msg)
    return u'unsupported multifile operation'


def _get_files(request, pagename):
    attach_dir = getAttachDir(request, pagename)
    if os.path.isdir(attach_dir):
        files = [fn.decode(config.charset) for fn in os.listdir(attach_dir)]
        files.sort()
    else:
        files = []
    return files


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


def error_msg(pagename, request, msg):
    msg = wikiutil.escape(msg)
    request.theme.add_msg(msg, "error")
    Page(request, pagename).send_page()


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

def send_link_rel(request, pagename):
    files = _get_files(request, pagename)
    for fname in files:
        url = getAttachUrl(pagename, fname, request, do='view')
        request.write(u'<link rel="Appendix" title="%s" href="%s">\n' % (
                      wikiutil.escape(fname, 1),
                      wikiutil.escape(url, 1)))

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

    writeable = request.user.may.write(pagename)

    # First send out the upload new attachment form on top of everything else.
    # This avoids usability issues if you have to scroll down a lot to upload
    # a new file when the page already has lots of attachments:
    if writeable:
        request.write('<h2>' + _("New Attachment") + '</h2>')
        request.write("""
<form action="%(url)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_target)s</dt>
<dd><input type="text" name="target" size="50" value="%(target)s"></dd>
<dt>%(upload_label_overwrite)s</dt>
<dd><input type="checkbox" name="overwrite" value="1" %(overwrite_checked)s></dd>
</dl>
%(textcha)s
<p>
<input type="hidden" name="action" value="%(action_name)s">
<input type="hidden" name="do" value="upload">
<input type="hidden" name="ticket" value="%(ticket)s">
<input type="submit" value="%(upload_button)s">
</p>
</form>
""" % {
    'url': request.href(pagename),
    'action_name': action_name,
    'upload_label_file': _('File to upload'),
    'upload_label_target': _('Rename to'),
    'target': wikiutil.escape(request.values.get('target', ''), 1),
    'upload_label_overwrite': _('Overwrite existing attachment of same name'),
    'overwrite_checked': ('', 'checked')[request.form.get('overwrite', '0') == '1'],
    'upload_button': _('Upload'),
    'textcha': TextCha(request).render(),
    'ticket': wikiutil.createTicket(request),
})

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

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

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

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

    do = request.values.get('do', 'upload_form')
    handler = globals().get('_do_%s' % do)
    if handler:
        msg = handler(pagename, request)
    else:
        msg = _('Unsupported AttachFile sub-action: %s') % do
    if msg:
        error_msg(pagename, request, msg)


def _do_upload_form(pagename, request):
    upload_form(pagename, request)


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

    # Use user interface language for this generated page
    request.setContentLanguage(request.lang)
    request.theme.add_msg(msg, "dialog")
    request.theme.send_title(_('Attachments for "%(pagename)s"') % {'pagename': pagename}, pagename=pagename)
    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

    if not wikiutil.checkTicket(request, request.form.get('ticket', '')):
        return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.upload' }

    # Currently we only check TextCha for upload (this is what spammers ususally do),
    # but it could be extended to more/all attachment write access
    if not TextCha(request).check_answer_from_form():
        return _('TextCha: Wrong answer! Go back and try again...')

    form = request.form

    file_upload = request.files.get('file')
    if not file_upload:
        # This might happen when trying to upload file names
        # with non-ascii characters on Safari.
        return _("No file content. Delete non ASCII characters from the file name and try again.")

    try:
        overwrite = int(form.get('overwrite', '0'))
    except:
        overwrite = 0

    if not request.user.may.write(pagename):
        return _('You are not allowed to attach a file to this page.')

    if overwrite and not request.user.may.delete(pagename):
        return _('You are not allowed to overwrite a file attachment of this page.')

    target = form.get('target', u'').strip()
    if not target:
        target = file_upload.filename or u''

    target = wikiutil.clean_input(target)

    if not target:
        return _("Filename of attachment not specified!")

    # add the attachment
    try:
        target, bytes = add_attachment(request, pagename, target, file_upload.stream, overwrite=overwrite)
        msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
                " with %(bytes)d bytes saved.") % {
                'target': target, 'filename': file_upload.filename, 'bytes': bytes}
    except AttachmentAlreadyExists:
        msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
            'target': target, 'filename': file_upload.filename}

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


class ContainerItem:
    """ A storage container (multiple objects in 1 tarfile) """

    def __init__(self, request, pagename, containername):
        """
        @param pagename: a wiki page name
        @param containername: the filename of the tar file.
                              Make sure this is a simple filename, NOT containing any path components.
                              Use wikiutil.taintfilename() to avoid somebody giving a container
                              name that starts with e.g. ../../filename or you'll create a
                              directory traversal and code execution vulnerability.
        """
        self.request = request
        self.pagename = pagename
        self.containername = containername
        self.container_filename = getFilename(request, pagename, containername)

    def member_url(self, member):
        """ return URL for accessing container member
            (we use same URL for get (GET) and put (POST))
        """
        url = Page(self.request, self.pagename).url(self.request, {
            'action': 'AttachFile',
            'do': 'box',  # shorter to type than 'container'
            'target': self.containername,
            #'member': member,
        })
        return url + '&member=%s' % member
        # member needs to be last in qs because twikidraw looks for "file extension" at the end

    def get(self, member):
        """ return a file-like object with the member file data
        """
        tf = tarfile.TarFile(self.container_filename)
        return tf.extractfile(member)

    def put(self, member, content, content_length=None):
        """ save data into a container's member """
        tf = tarfile.TarFile(self.container_filename, mode='a')
        if isinstance(member, unicode):
            member = member.encode('utf-8')
        ti = tarfile.TarInfo(member)
        if isinstance(content, str):
            if content_length is None:
                content_length = len(content)
            content = StringIO(content) # we need a file obj
        elif not hasattr(content, 'read'):
            msg = "unsupported content object: %r" % content
            logging.error(msg)
            raise Exception(msg)
        assert content_length >= 0  # we don't want -1 interpreted as 4G-1
        ti.size = content_length
        tf.addfile(ti, content)
        tf.close()

    def truncate(self):
        f = open(self.container_filename, 'w')
        f.close()

    def exists(self):
        return os.path.exists(self.container_filename)

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

    if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
        return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.del' }

    pagename, filename, fpath = _access_file(pagename, request)
    if not request.user.may.delete(pagename):
        return _('You are not allowed to delete attachments on this page.')
    if not filename:
        return # error msg already sent in _access_file

    remove_attachment(request, pagename, filename)

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


def move_file(request, pagename, new_pagename, attachment, new_attachment):
    """
    move a file attachment from pagename:attachment to new_pagename:new_attachment

    @param pagename: original pagename
    @param new_pagename: new pagename (may be same as original pagename)
    @param attachment: original attachment filename
                       note: attachment filename must not contain a path,
                             use wikiutil.taintfilename() before calling move_file
    @param new_attachment: new attachment filename (may be same as original filename)
                       note: attachment filename must not contain a path,
                             use wikiutil.taintfilename() before calling move_file
    """
    _ = 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)):
        try:
            move_attachment(request, pagename, new_pagename,
                            attachment, new_attachment)
        except DestPathExists:
            msg = _("Attachment '%(new_pagename)s/%(new_filename)s' already exists.") % {
                    'new_pagename': new_pagename,
                    'new_filename': new_attachment}
        except SamePath:
            msg = _("Nothing changed")
        else:
            msg = _("Attachment '%(pagename)s/%(filename)s' moved to '%(new_pagename)s/%(new_filename)s'.") % {
                    'pagename': pagename,
                    'filename': attachment,
                    'new_pagename': new_pagename,
                    'new_filename': new_attachment}
    else:
        msg = _("Page '%(new_pagename)s' does not exist or you don't have enough rights.") % {
                'new_pagename': new_pagename}
    upload_form(pagename, request, msg=msg)


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

    if 'cancel' in request.form:
        return _('Move aborted!')
    if not wikiutil.checkTicket(request, request.form.get('ticket', '')):
        return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.move' }
    if not request.user.may.delete(pagename):
        return _('You are not allowed to move attachments from this page.')

    if 'newpagename' in request.form:
        new_pagename = request.form.get('newpagename')
    else:
        upload_form(pagename, request, msg=_("Move aborted because new page name is empty."))
    if 'newattachmentname' in request.form:
        new_attachment = request.form.get('newattachmentname')
        if new_attachment != wikiutil.taintfilename(new_attachment):
            upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
                                  'filename': new_attachment})
            return
    else:
        upload_form(pagename, request, msg=_("Move aborted because new attachment name is empty."))

    attachment = request.form.get('oldattachmentname')
    if attachment != wikiutil.taintfilename(attachment):
        upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
                              'filename': attachment})
        return
    move_file(request, pagename, new_pagename, attachment, new_attachment)


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

    pagename, filename, fpath = _access_file(pagename, request)
    if not request.user.may.delete(pagename):
        return _('You are not allowed to move attachments from this page.')
    if not filename:
        return # error msg already sent in _access_file

    # move file
    d = {'action': action_name,
         'url': request.href(pagename),
         'do': 'attachment_move',
         'ticket': wikiutil.createTicket(request),
         'pagename': wikiutil.escape(pagename, 1),
         'attachment_name': wikiutil.escape(filename, 1),
         'move': _('Move'),
         'cancel': _('Cancel'),
         'newname_label': _("New page name"),
         'attachment_label': _("New attachment name"),
        }
    formhtml = '''
<form action="%(url)s" method="POST">
<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" size="80">
        </td>
    </tr>
    <tr>
        <td class="label"><label>%(attachment_label)s</label></td>
        <td class="content">
            <input type="text" name="newattachmentname" value="%(attachment_name)s" size="80">
        </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)
    request.theme.add_msg(formhtml, "dialog")
    return thispage.send_page()


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

    pagename, filename, fpath = _access_file(pagename, request)
    if not request.user.may.read(pagename):
        return _('You are not allowed to get attachments from this page.')
    if not filename:
        return # error msg already sent in _access_file

    timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
    if_modified = request.if_modified_since
    if if_modified and if_modified >= timestamp:
        request.status_code = 304
    else:
        ci = ContainerItem(request, pagename, filename)
        filename = wikiutil.taintfilename(request.values['member'])
        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'

        now = time.time()
        request.headers['Date'] = http_date(now)
        request.headers['Content-Type'] = content_type
        request.headers['Last-Modified'] = http_date(timestamp)
        request.headers['Expires'] = http_date(now - 365 * 24 * 3600)
        #request.headers['Content-Length'] = os.path.getsize(fpath)
        content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
        request.headers['Content-Disposition'] = content_dispo_string

        # send data
        request.send_file(ci.get(filename))


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

    pagename, filename, fpath = _access_file(pagename, request)
    if not request.user.may.read(pagename):
        return _('You are not allowed to get attachments from this page.')
    if not filename:
        request.status_code = 404
        return # error msg already sent in _access_file

    timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
    if_modified = request.if_modified_since
    if if_modified and if_modified >= timestamp:
        request.status_code = 304
    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'

        now = time.time()
        request.headers['Date'] = http_date(now)
        request.headers['Content-Type'] = content_type
        request.headers['Last-Modified'] = http_date(timestamp)
        request.headers['Expires'] = http_date(now - 365 * 24 * 3600)
        request.headers['Content-Length'] = os.path.getsize(fpath)
        content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
        request.headers['Content-Disposition'] = content_dispo_string

        # send data
        request.send_file(open(fpath, 'rb'))


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

    if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
        return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.install' }

    pagename, target, targetpath = _access_file(pagename, request)
    if not request.user.isSuperUser():
        return _('You are not allowed to install files.')
    if not target:
        return

    package = packages.ZipPackage(request, targetpath)

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

    upload_form(pagename, request, msg=msg)


def _do_unzip(pagename, request, overwrite=False):
    _ = request.getText

    if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
        return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.unzip' }

    pagename, filename, fpath = _access_file(pagename, request)
    if not (request.user.may.delete(pagename) and request.user.may.read(pagename) and request.user.may.write(pagename)):
        return _('You are not allowed to unzip attachments of this page.')

    if not filename:
        return # error msg already sent in _access_file

    try:
        if not zipfile.is_zipfile(fpath):
            return _('The file %(filename)s is not a .zip file.') % {'filename': filename}

        # determine how which attachment names we have and how much space each is occupying
        curr_fsizes = dict([(f, size(request, pagename, f)) for f in _get_files(request, pagename)])

        # Checks for the existance of one common prefix path shared among
        # all files in the zip file. If this is the case, remove the common prefix.
        # We also prepare a dict of the new filenames->filesizes.
        zip_path_sep = '/'  # we assume '/' is as zip standard suggests
        fname_index = None
        mapping = []
        new_fsizes = {}
        zf = zipfile.ZipFile(fpath)
        for zi in zf.infolist():
            name = zi.filename
            if not name.endswith(zip_path_sep):  # a file (not a directory)
                if fname_index is None:
                    fname_index = name.rfind(zip_path_sep) + 1
                    path = name[:fname_index]
                if (name.rfind(zip_path_sep) + 1 != fname_index  # different prefix len
                    or
                    name[:fname_index] != path): # same len, but still different
                    mapping = []  # zip is not acceptable
                    break
                if zi.file_size >= request.cfg.unzip_single_file_size:  # file too big
                    mapping = []  # zip is not acceptable
                    break
                finalname = name[fname_index:]  # remove common path prefix
                finalname = finalname.decode(config.charset, 'replace')  # replaces trash with \uFFFD char
                mapping.append((name, finalname))
                new_fsizes[finalname] = zi.file_size

        # now we either have an empty mapping (if the zip is not acceptable),
        # an identity mapping (no subdirs in zip, just all flat), or
        # a mapping (origname, finalname) where origname is the zip member filename
        # (including some prefix path) and finalname is a simple filename.

        # calculate resulting total file size / count after unzipping:
        if overwrite:
            curr_fsizes.update(new_fsizes)
            total = curr_fsizes
        else:
            new_fsizes.update(curr_fsizes)
            total = new_fsizes
        total_count = len(total)
        total_size = sum(total.values())

        if not mapping:
            msg = _("Attachment '%(filename)s' not unzipped because some files in the zip "
                    "are either not in the same directory or exceeded the single file size limit (%(maxsize_file)d kB)."
                   ) % {'filename': filename,
                        'maxsize_file': request.cfg.unzip_single_file_size / 1000, }
        elif total_size > request.cfg.unzip_attachments_space:
            msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
                    "the per page attachment storage size limit (%(size)d kB).") % {
                        'filename': filename,
                        'size': request.cfg.unzip_attachments_space / 1000, }
        elif total_count > request.cfg.unzip_attachments_count:
            msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
                    "the per page attachment count limit (%(count)d).") % {
                        'filename': filename,
                        'count': request.cfg.unzip_attachments_count, }
        else:
            not_overwritten = []
            for origname, finalname in mapping:
                try:
                    # Note: reads complete zip member file into memory. ZipFile does not offer block-wise reading:
                    add_attachment(request, pagename, finalname, zf.read(origname), overwrite)
                except AttachmentAlreadyExists:
                    not_overwritten.append(finalname)
            if not_overwritten:
                msg = _("Attachment '%(filename)s' partially unzipped (did not overwrite: %(filelist)s).") % {
                        'filename': filename,
                        'filelist': ', '.join(not_overwritten), }
            else:
                msg = _("Attachment '%(filename)s' unzipped.") % {'filename': filename}
    except (IOError, RuntimeError, zipfile.BadZipfile, zipfile.LargeZipFile), err:
        # We don't want to crash with a traceback here (an exception
        # here could be caused by an uploaded defective zip file - and
        # if we crash here, the user does not get a UI to remove the
        # defective zip file again).
        # RuntimeError is raised by zipfile stdlib module in case of
        # problems (like inconsistent slash and backslash usage in the
        # archive).
        # BadZipfile/LargeZipFile are raised when there are some
        # specific problems with the archive file.
        logging.exception("An exception within zip file attachment [%r:%r] handling occurred:" % (
            pagename, filename))
        msg = _("A severe error occurred:") + ' ' + str(err)

    upload_form(pagename, request, msg=msg)


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

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

    request.write('<h2>' + _("Attachment '%(filename)s'") % {'filename': filename} + '</h2>')
    # show a download link above the content
    label = _('Download')
    link = (fmt.url(1, getAttachUrl(pagename, filename, request, do='get'), css_class="download") +
            fmt.text(label) +
            fmt.url(0))
    request.write('%s<br><br>' % link)

    if filename.endswith('.tdraw') or filename.endswith('.adraw'):
        request.write(fmt.attachment_drawing(filename, ''))
        return

    mt = wikiutil.MimeType(filename=filename)

    if mt.major == 'image' and mt.minor in config.browser_supported_images:
        url = getAttachUrl(pagename, filename, request)
        request.write('<img src="%s" alt="%s">' % (
            wikiutil.escape(url, 1),
            wikiutil.escape(filename, 1)))
        return
    elif mt.major == 'text':
        ext = os.path.splitext(filename)[1]
        Parser = wikiutil.getParserForExtension(request.cfg, ext)
        if Parser is not None:
            try:
                # rU: universal newline support so that even a \r is considered a valid line separator.
                # CSV exported by office (on Mac?) has \r line separators.
                content = file(fpath, 'rU').read()
                content = wikiutil.decodeUnknownInput(content)
                colorizer = Parser(content, request, filename=filename)
                colorizer.format(request.formatter)
                return
            except IOError:
                pass

        request.write(request.formatter.preformatted(1))
        # If we have text but no colorizing parser we try to decode file contents.
        content = open(fpath, 'r').read()
        content = wikiutil.decodeUnknownInput(content)
        content = wikiutil.escape(content)
        request.write(request.formatter.text(content))
        request.write(request.formatter.preformatted(0))
        return

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

        if zipfile.is_zipfile(fpath) and mt.minor == 'zip':
            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
    except (IOError, RuntimeError, zipfile.BadZipfile, zipfile.LargeZipFile):
        # We don't want to crash with a traceback here (an exception
        # here could be caused by an uploaded defective zip file - and
        # if we crash here, the user does not get a UI to remove the
        # defective zip file again).
        # RuntimeError is raised by zipfile stdlib module in case of
        # problems (like inconsistent slash and backslash usage in the
        # archive).
        # BadZipfile/LargeZipFile are raised when there are some
        # specific problems with the archive file.
        logging.exception("An exception within zip file attachment [%r:%r] handling occurred:" % (
            pagename, filename))
        return

    from MoinMoin import macro
    from MoinMoin.parser.text import Parser

    macro.request = request
    macro.formatter = request.html_formatter
    p = Parser("##\n", request)
    m = macro.Macro(p)

    # use EmbedObject to view valid mime types
    if mt is None:
        request.write('<p>' + _("Unknown file type, cannot display this attachment inline.") + '</p>')
        link = (fmt.url(1, getAttachUrl(pagename, filename, request)) +
                fmt.text(filename) +
                fmt.url(0))
        request.write('For using an external program follow this link %s' % link)
        return
    request.write(m.execute('EmbedObject', u'target="%s", pagename="%s"' % (filename, pagename)))
    return


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

    orig_pagename = pagename
    pagename, filename, fpath = _access_file(pagename, request)
    if not request.user.may.read(pagename):
        return _('You are not allowed to view attachments of this page.')
    if not filename:
        request.status_code = 404
        return

    request.formatter.page = Page(request, pagename)

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

    # send body
    request.write(request.formatter.startContent())
    send_viewfile(orig_pagename, request)
    send_uploadform(pagename, request)
    request.write(request.formatter.endContent())

    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'),
    ]

    # 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(pagename, 1)),
                    wikiutil.escape(filename.decode(config.charset)),
                    os.path.getsize(filepath),
                ))

    if data:
        from MoinMoin.widget.browser import DataBrowserWidget

        browser = DataBrowserWidget(request)
        browser.setData(data, sort_columns=[0, 1])
        return browser.render(method="GET")

    return ''

