MoinMoin/action/AttachFile.py
author Thomas Waldmann <tw AT waldmann-edv DOT de>
Wed, 11 Feb 2009 02:34:33 +0100
changeset 4569 3caaa8c74c41
parent 4549 173c67aff3d6
child 4599 9275fb7a2bc2
permissions -rw-r--r--
wikiutil: replace moin's cgi/urllib wrappers by calls to werkzeug.utils code
tw-public@0
     1
# -*- coding: iso-8859-1 -*-
tw-public@0
     2
"""
tw-public@0
     3
    MoinMoin - AttachFile action
tw-public@0
     4
tw-public@0
     5
    This action lets a page have multiple attachment files.
tw-public@0
     6
    It creates a folder <data>/pages/<pagename>/attachments
tw-public@0
     7
    and keeps everything in there.
tw-public@0
     8
tw-public@0
     9
    Form values: action=Attachment
tw-public@0
    10
    1. with no 'do' key: returns file upload form
tw-public@0
    11
    2. do=attach: accept file upload and saves the file in
tw-public@0
    12
       ../attachment/pagename/
tw-public@0
    13
    3. /pagename/fname?action=Attachment&do=get[&mimetype=type]:
tw-public@0
    14
       return contents of the attachment file with the name fname.
tw-public@0
    15
    4. /pathname/fname, do=view[&mimetype=type]:create a page
tw-public@0
    16
       to view the content of the file
tw-public@0
    17
tw@3073
    18
    To link to an attachment, use [[attachment:file.txt]],
tw@3073
    19
    to embed an attachment, use {{attachment:file.png}}.
tw-public@0
    20
tw@1911
    21
    @copyright: 2001 by Ken Sugino (sugino@mediaone.net),
tw@1918
    22
                2001-2004 by Juergen Hermann <jh@web.de>,
tw@1911
    23
                2005 MoinMoin:AlexanderSchremmer,
tw@1911
    24
                2005 DiegoOngaro at ETSZONE (diego@etszone.com),
rb@1952
    25
                2005-2007 MoinMoin:ReimarBauer,
tw@3149
    26
                2007-2008 MoinMoin:ThomasWaldmann
tw-public@0
    27
    @license: GNU GPL, see COPYING for details.
tw-public@0
    28
"""
tw-public@0
    29
florian@4226
    30
import os, time, zipfile, mimetypes, errno, datetime
tw@4439
    31
from StringIO import StringIO
grzywacz@2490
    32
tw@3591
    33
from MoinMoin import log
tw@3591
    34
logging = log.getLogger(__name__)
tw@3591
    35
tw@1791
    36
from MoinMoin import config, wikiutil, packages
tw-public@0
    37
from MoinMoin.Page import Page
tw@1115
    38
from MoinMoin.util import filesys, timefuncs
tw@2983
    39
from MoinMoin.security.textcha import TextCha
grzywacz@2106
    40
from MoinMoin.events import FileAttachedEvent, send_event
tw@4439
    41
from MoinMoin.support import tarfile
tw-public@0
    42
tw-public@0
    43
action_name = __name__.split('.')[-1]
tw-public@0
    44
tw-public@0
    45
#############################################################################
tw-public@0
    46
### External interface - these are called from the core code
tw-public@0
    47
#############################################################################
tw-public@0
    48
alex@691
    49
class AttachmentAlreadyExists(Exception):
alex@691
    50
    pass
alex@691
    51
tw@3150
    52
tw-public@0
    53
def getBasePath(request):
tw@3150
    54
    """ Get base path where page dirs for attachments are stored. """
tw@3149
    55
    return request.rootpage.getPagePath('pages')
tw-public@0
    56
tw-public@0
    57
tw-public@0
    58
def getAttachDir(request, pagename, create=0):
tw@3150
    59
    """ Get directory where attachments for page `pagename` are stored. """
tw@3149
    60
    if request.page and pagename == request.page.page_name:
tw@3149
    61
        page = request.page # reusing existing page obj is faster
tw-public@0
    62
    else:
tw@3149
    63
        page = Page(request, pagename)
tw@3149
    64
    return page.getPagePath("attachments", check_create=create)
tw-public@0
    65
tw@3150
    66
MoinMoinBugs/BrokenAttachmentPaths">Florian@143
    67
def absoluteName(url, pagename):
tw@281
    68
    """ Get (pagename, filename) of an attachment: link
tw@281
    69
        @param url: PageName/filename.ext or filename.ext (unicode)
tw@281
    70
        @param pagename: name of the currently processed page (unicode)
tw@281
    71
        @rtype: tuple of unicode
tw@281
    72
        @return: PageName, filename.ext
MoinMoinBugs/BrokenAttachmentPaths">Florian@143
    73
    """
tw@3084
    74
    url = wikiutil.AbsPageName(pagename, url)
tw@281
    75
    pieces = url.split(u'/')
tw@281
    76
    if len(pieces) == 1:
MoinMoinBugs/BrokenAttachmentPaths">Florian@143
    77
        return pagename, pieces[0]
MoinMoinBugs/BrokenAttachmentPaths">Florian@143
    78
    else:
Florian@145
    79
        return u"/".join(pieces[:-1]), pieces[-1]
tw-public@0
    80
tw@3149
    81
def getAttachUrl(pagename, filename, request, addts=0, escaped=0, do='get', drawing='', upload=False):
tw@3149
    82
    """ Get URL that points to attachment `filename` of page `pagename`. """
tw@3149
    83
    if upload:
tw@3149
    84
        if not drawing:
florian@4231
    85
            url = request.href(pagename, rename=wikiutil.taintfilename(filename),
florian@4231
    86
                               action=action_name)
tw@3149
    87
        else:
florian@4231
    88
            url = request.href(pagename, rename=wikiutil.taintfilename(filename),
florian@4231
    89
                               drawing=drawing, action=action_name)
tw@3149
    90
    else:
tw@3149
    91
        if not drawing:
florian@4231
    92
            url = request.href(pagename, target=filename, action=action_name, do=do)
tw@3149
    93
        else:
florian@4231
    94
            url = request.href(pagename, drawing=drawing, action=action_name)
tw-public@0
    95
    return url
tw-public@0
    96
tw@2701
    97
tw-public@0
    98
def getIndicator(request, pagename):
tw-public@0
    99
    """ Get an attachment indicator for a page (linked clip image) or
tw-public@0
   100
        an empty string if not attachments exist.
tw-public@0
   101
    """
tw-public@0
   102
    _ = request.getText
tw-public@0
   103
    attach_dir = getAttachDir(request, pagename)
tw@1920
   104
    if not os.path.exists(attach_dir):
tw@1920
   105
        return ''
tw-public@0
   106
tw-public@0
   107
    files = os.listdir(attach_dir)
tw@1920
   108
    if not files:
tw@1920
   109
        return ''
tw-public@0
   110
tw@3149
   111
    fmt = request.formatter
tw-public@0
   112
    attach_count = _('[%d attachments]') % len(files)
tw@889
   113
    attach_icon = request.theme.make_icon('attach', vars={'attach_count': attach_count})
florian@4231
   114
    attach_link = (fmt.url(1, request.href(pagename, action=action_name), rel='nofollow') +
tw@3149
   115
                   attach_icon +
tw@3149
   116
                   fmt.url(0))
tw-public@0
   117
    return attach_link
tw-public@0
   118
tw-public@0
   119
tw@281
   120
def getFilename(request, pagename, filename):
tw@281
   121
    """ make complete pathfilename of file "name" attached to some page "pagename"
tw@281
   122
        @param request: request object
tw@281
   123
        @param pagename: name of page where the file is attached to (unicode)
tw@281
   124
        @param filename: filename of attached file (unicode)
tw@281
   125
        @rtype: string (in config.charset encoding)
tw@281
   126
        @return: complete path/filename of attached file
tw@281
   127
    """
tw@1911
   128
    if isinstance(filename, unicode):
tw@1911
   129
        filename = filename.encode(config.charset)
rb@2885
   130
    return os.path.join(getAttachDir(request, pagename, create=1), filename)
tw-public@0
   131
tw@3150
   132
tw@1910
   133
def exists(request, pagename, filename):
tw@1910
   134
    """ check if page <pagename> has a file <filename> attached """
tw@1910
   135
    fpath = getFilename(request, pagename, filename)
tw@1910
   136
    return os.path.exists(fpath)
tw-public@0
   137
tw@3150
   138
tw@1911
   139
def size(request, pagename, filename):
tw@1911
   140
    """ return file size of file attachment """
tw@1911
   141
    fpath = getFilename(request, pagename, filename)
tw@1911
   142
    return os.path.getsize(fpath)
tw@1911
   143
tw@3150
   144
tw-public@0
   145
def info(pagename, request):
tw@3150
   146
    """ Generate snippet with info on the attachment for page `pagename`. """
tw-public@0
   147
    _ = request.getText
tw-public@0
   148
tw-public@0
   149
    attach_dir = getAttachDir(request, pagename)
tw-public@0
   150
    files = []
tw-public@0
   151
    if os.path.isdir(attach_dir):
tw-public@0
   152
        files = os.listdir(attach_dir)
tw-public@0
   153
    page = Page(request, pagename)
tw@3149
   154
    link = page.url(request, {'action': action_name})
tw@3122
   155
    attach_info = _('There are <a href="%(link)s">%(count)s attachment(s)</a> stored for this page.') % {
tw-public@0
   156
        'count': len(files),
tw-public@0
   157
        'link': wikiutil.escape(link)
tw-public@0
   158
        }
tw-public@0
   159
    return "\n<p>\n%s\n</p>\n" % attach_info
tw-public@0
   160
tw@3150
   161
tw@3568
   162
def _write_stream(content, stream, bufsize=8192):
tw@3603
   163
    if hasattr(content, 'read'): # looks file-like
tw@3568
   164
        import shutil
tw@3568
   165
        shutil.copyfileobj(content, stream, bufsize)
tw@3603
   166
    elif isinstance(content, str):
tw@3603
   167
        stream.write(content)
tw@3603
   168
    else:
tw@3603
   169
        logging.error("unsupported content object: %r" % content)
tw@3603
   170
        raise
tw@3568
   171
tw@1791
   172
def add_attachment(request, pagename, target, filecontent, overwrite=0):
tw@3568
   173
    """ save <filecontent> to an attachment <target> of page <pagename>
tw@3568
   174
tw@3568
   175
        filecontent can be either a str (in memory file content),
tw@3568
   176
        or an open file object (file content in e.g. a tempfile).
tw@3568
   177
    """
rb@1797
   178
    _ = request.getText
rb@1797
   179
tw@3150
   180
    # replace illegal chars
alex@691
   181
    target = wikiutil.taintfilename(target)
tw-public@0
   182
alex@691
   183
    # get directory, and possibly create it
alex@691
   184
    attach_dir = getAttachDir(request, pagename, create=1)
alex@691
   185
    # save file
alex@691
   186
    fpath = os.path.join(attach_dir, target).encode(config.charset)
tw@1765
   187
    exists = os.path.exists(fpath)
tw@1765
   188
    if exists and not overwrite:
tw@3591
   189
        raise AttachmentAlreadyExists
alex@691
   190
    else:
tw@1765
   191
        if exists:
tw@1765
   192
            try:
tw@1765
   193
                os.remove(fpath)
tw@1765
   194
            except:
tw@1765
   195
                pass
alex@691
   196
        stream = open(fpath, 'wb')
alex@691
   197
        try:
tw@3568
   198
            _write_stream(filecontent, stream)
alex@691
   199
        finally:
alex@691
   200
            stream.close()
alex@691
   201
alex@691
   202
        _addLogEntry(request, 'ATTNEW', pagename, target)
tw@2286
   203
tw@3568
   204
        filesize = os.path.getsize(fpath)
tw@3568
   205
        event = FileAttachedEvent(request, pagename, target, filesize)
grzywacz@2490
   206
        send_event(event)
fpletz@1473
   207
tw@3568
   208
    return target, filesize
tw@802
   209
alex@520
   210
tw-public@0
   211
#############################################################################
tw-public@0
   212
### Internal helpers
tw-public@0
   213
#############################################################################
tw-public@0
   214
tw-public@0
   215
def _addLogEntry(request, action, pagename, filename):
tw-public@0
   216
    """ Add an entry to the edit log on uploads and deletes.
tw-public@0
   217
tw-public@0
   218
        `action` should be "ATTNEW" or "ATTDEL"
tw-public@0
   219
    """
tw-public@0
   220
    from MoinMoin.logfile import editlog
tw-public@0
   221
    t = wikiutil.timestamp2version(time.time())
tw@4569
   222
    fname = wikiutil.url_quote(filename)
tw-public@0
   223
tw-public@0
   224
    # Write to global log
tw-public@0
   225
    log = editlog.EditLog(request)
tw-public@0
   226
    log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
tw-public@0
   227
tw-public@0
   228
    # Write to local log
tw-public@0
   229
    log = editlog.EditLog(request, rootpagename=pagename)
tw-public@0
   230
    log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
tw-public@0
   231
tw-public@0
   232
tw-public@0
   233
def _access_file(pagename, request):
tw-public@0
   234
    """ Check form parameter `target` and return a tuple of
tw@3169
   235
        `(pagename, filename, filepath)` for an existing attachment.
tw-public@0
   236
tw@3169
   237
        Return `(pagename, None, None)` if an error occurs.
tw-public@0
   238
    """
tw-public@0
   239
    _ = request.getText
tw-public@0
   240
tw-public@0
   241
    error = None
florian@4222
   242
    if not request.values.get('target'):
tw-public@0
   243
        error = _("Filename of attachment not specified!")
tw-public@0
   244
    else:
florian@4222
   245
        filename = wikiutil.taintfilename(request.values['target'])
tw-public@0
   246
        fpath = getFilename(request, pagename, filename)
tw-public@0
   247
tw-public@0
   248
        if os.path.isfile(fpath):
tw@3169
   249
            return (pagename, filename, fpath)
tw-public@0
   250
        error = _("Attachment '%(filename)s' does not exist!") % {'filename': filename}
tw-public@0
   251
tw-public@0
   252
    error_msg(pagename, request, error)
tw@3169
   253
    return (pagename, None, None)
tw-public@0
   254
tw-public@0
   255
tw@1653
   256
def _build_filelist(request, pagename, showheader, readonly, mime_type='*'):
tw-public@0
   257
    _ = request.getText
tw@3149
   258
    fmt = request.html_formatter
tw-public@0
   259
tw-public@0
   260
    # access directory
tw-public@0
   261
    attach_dir = getAttachDir(request, pagename)
tw-public@0
   262
    files = _get_files(request, pagename)
tw-public@0
   263
tw@1653
   264
    if mime_type != '*':
tw@1653
   265
        files = [fname for fname in files if mime_type == mimetypes.guess_type(fname)[0]]
tw@1653
   266
tw@3151
   267
    html = []
tw-public@0
   268
    if files:
tw-public@0
   269
        if showheader:
tw@3151
   270
            html.append(fmt.rawHTML(_(
tw@3151
   271
                "To refer to attachments on a page, use '''{{{attachment:filename}}}''', \n"
tw-public@0
   272
                "as shown below in the list of files. \n"
tw-public@0
   273
                "Do '''NOT''' use the URL of the {{{[get]}}} link, \n"
tw@3122
   274
                "since this is subject to change and can break easily.",
tw@3122
   275
                wiki=True
tw@3151
   276
            )))
tw-public@0
   277
tw-public@0
   278
        label_del = _("del")
R@1608
   279
        label_move = _("move")
tw-public@0
   280
        label_get = _("get")
tw-public@0
   281
        label_edit = _("edit")
tw-public@0
   282
        label_view = _("view")
MoinMoin:PackageInstaller and zip support.">alex@80
   283
        label_unzip = _("unzip")
MoinMoin:PackageInstaller and zip support.">alex@80
   284
        label_install = _("install")
tw-public@0
   285
tw@3151
   286
        html.append(fmt.bullet_list(1))
tw-public@0
   287
        for file in files:
rb@1985
   288
            mt = wikiutil.MimeType(filename=file)
tw@3151
   289
            fullpath = os.path.join(attach_dir, file).encode(config.charset)
tw@3151
   290
            st = os.stat(fullpath)
tw-public@0
   291
            base, ext = os.path.splitext(file)
tw@3151
   292
            parmdict = {'file': wikiutil.escape(file),
tw@3151
   293
                        'fsize': "%.1f" % (float(st.st_size) / 1024),
tw@3151
   294
                        'fmtime': request.user.getFormattedDateTime(st.st_mtime),
tw@3151
   295
                       }
tw-public@0
   296
tw@3151
   297
            links = []
tw@3151
   298
            may_delete = request.user.may.delete(pagename)
tw@3151
   299
            if may_delete and not readonly:
tw@3151
   300
                links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='del')) +
tw@3151
   301
                             fmt.text(label_del) +
tw@3151
   302
                             fmt.url(0))
tw@3149
   303
tw@3151
   304
            if may_delete and not readonly:
tw@3151
   305
                links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='move')) +
tw@3149
   306
                             fmt.text(label_move) +
tw@3151
   307
                             fmt.url(0))
tw@3151
   308
tw@3151
   309
            links.append(fmt.url(1, getAttachUrl(pagename, file, request)) +
tw@3151
   310
                         fmt.text(label_get) +
tw@3151
   311
                         fmt.url(0))
tw@3149
   312
tw@4439
   313
            if ext == '.tdraw':
tw@3151
   314
                links.append(fmt.url(1, getAttachUrl(pagename, file, request, drawing=base)) +
tw@3149
   315
                             fmt.text(label_edit) +
tw@3149
   316
                             fmt.url(0))
tw-public@0
   317
            else:
tw@3151
   318
                links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
tw@3149
   319
                             fmt.text(label_view) +
tw@3149
   320
                             fmt.url(0))
tw-public@0
   321
tw@3705
   322
            try:
tw@3705
   323
                is_zipfile = zipfile.is_zipfile(fullpath)
tw@3705
   324
                if is_zipfile:
tw@3705
   325
                    is_package = packages.ZipPackage(request, fullpath).isPackage()
tw@3705
   326
                    if is_package and request.user.isSuperUser():
tw@3705
   327
                        links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='install')) +
tw@3705
   328
                                     fmt.text(label_install) +
tw@3705
   329
                                     fmt.url(0))
tw@3705
   330
                    elif (not is_package and mt.minor == 'zip' and
tw@3705
   331
                          may_delete and
tw@3705
   332
                          request.user.may.read(pagename) and
tw@3705
   333
                          request.user.may.write(pagename)):
tw@3705
   334
                        links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='unzip')) +
tw@3705
   335
                                     fmt.text(label_unzip) +
tw@3705
   336
                                     fmt.url(0))
tw@3705
   337
            except RuntimeError:
tw@3705
   338
                # We don't want to crash with a traceback here (an exception
tw@3705
   339
                # here could be caused by an uploaded defective zip file - and
tw@3705
   340
                # if we crash here, the user does not get a UI to remove the
tw@3705
   341
                # defective zip file again).
tw@3705
   342
                # RuntimeError is raised by zipfile stdlib module in case of
tw@3705
   343
                # problems (like inconsistent slash and backslash usage in the
tw@3705
   344
                # archive).
tw@3705
   345
                logging.exception("An exception within zip file attachment handling occurred:")
MoinMoin:PackageInstaller and zip support.">alex@80
   346
tw@3151
   347
            html.append(fmt.listitem(1))
tw@3151
   348
            html.append("[%s]" % "&nbsp;| ".join(links))
tw@3151
   349
            html.append(" (%(fmtime)s, %(fsize)s KB) [[attachment:%(file)s]]" % parmdict)
tw@3151
   350
            html.append(fmt.listitem(0))
tw@3151
   351
        html.append(fmt.bullet_list(0))
tw@3149
   352
tw-public@0
   353
    else:
tw-public@0
   354
        if showheader:
tw@3151
   355
            html.append(fmt.paragraph(1))
tw@3151
   356
            html.append(fmt.text(_("No attachments stored for %(pagename)s") % {
tw@3270
   357
                                   'pagename': pagename}))
tw@3151
   358
            html.append(fmt.paragraph(0))
tw-public@0
   359
tw@3151
   360
    return ''.join(html)
tw-public@0
   361
tw-public@0
   362
tw-public@0
   363
def _get_files(request, pagename):
tw-public@0
   364
    attach_dir = getAttachDir(request, pagename)
tw-public@0
   365
    if os.path.isdir(attach_dir):
tw@1866
   366
        files = [fn.decode(config.charset) for fn in os.listdir(attach_dir)]
tw-public@0
   367
        files.sort()
tw@3150
   368
    else:
tw@3150
   369
        files = []
tw@3150
   370
    return files
tw-public@0
   371
tw-public@0
   372
tw-public@0
   373
def _get_filelist(request, pagename):
tw-public@0
   374
    return _build_filelist(request, pagename, 1, 0)
tw-public@0
   375
tw@3150
   376
tw-public@0
   377
def error_msg(pagename, request, msg):
alex@2966
   378
    request.theme.add_msg(msg, "error")
alex@2966
   379
    Page(request, pagename).send_page()
tw-public@0
   380
tw-public@0
   381
tw-public@0
   382
#############################################################################
tw-public@0
   383
### Create parts of the Web interface
tw-public@0
   384
#############################################################################
tw-public@0
   385
tw-public@0
   386
def send_link_rel(request, pagename):
tw-public@0
   387
    files = _get_files(request, pagename)
tw@3149
   388
    for fname in files:
tw@3149
   389
        url = getAttachUrl(pagename, fname, request, do='view', escaped=1)
tw@3149
   390
        request.write(u'<link rel="Appendix" title="%s" href="%s">\n' % (
tw@4539
   391
                      wikiutil.escape(fname, 1), url))
tw-public@0
   392
tw-public@0
   393
tw-public@0
   394
def send_hotdraw(pagename, request):
tw-public@0
   395
    _ = request.getText
tw-public@0
   396
tw-public@0
   397
    now = time.time()
tw@1318
   398
    pubpath = request.cfg.url_prefix_static + "/applets/TWikiDrawPlugin"
tw@4426
   399
    basename = request.values['drawing']
tw@4439
   400
    ci = ContainerItem(request, pagename, basename + '.tdraw')
tw@4439
   401
    drawpath = ci.member_url(basename + '.draw')
tw@4439
   402
    pngpath = ci.member_url(basename + '.png')
florian@4231
   403
    pagelink = request.href(pagename, action=action_name, ts=now)
tw-public@0
   404
    helplink = Page(request, "HelpOnActions/AttachFile").url(request)
florian@4231
   405
    savelink = request.href(pagename, action=action_name, do='savedrawing')
tw@623
   406
    #savelink = Page(request, pagename).url(request) # XXX include target filename param here for twisted
tw@4426
   407
                                           # request, {'savename': request.values['drawing']+'.draw'}
tw-public@0
   408
    #savelink = '/cgi-bin/dumpform.bat'
tw-public@0
   409
tw@3149
   410
    timestamp = '&amp;ts=%s' % now
tw-public@0
   411
tw-public@0
   412
    request.write('<h2>' + _("Edit drawing") + '</h2>')
tw-public@0
   413
    request.write("""
tw-public@0
   414
<p>
tw-public@0
   415
<img src="%(pngpath)s%(timestamp)s">
tw-public@0
   416
<applet code="CH.ifa.draw.twiki.TWikiDraw.class"
tw-public@0
   417
        archive="%(pubpath)s/twikidraw.jar" width="640" height="480">
tw-public@0
   418
<param name="drawpath" value="%(drawpath)s">
tw-public@0
   419
<param name="pngpath"  value="%(pngpath)s">
tw-public@0
   420
<param name="savepath" value="%(savelink)s">
tw-public@0
   421
<param name="basename" value="%(basename)s">
tw-public@0
   422
<param name="viewpath" value="%(pagelink)s">
tw-public@0
   423
<param name="helppath" value="%(helplink)s">
tw-public@0
   424
<strong>NOTE:</strong> You need a Java enabled browser to edit the drawing example.
tw-public@0
   425
</applet>
tw-public@0
   426
</p>""" % {
tw-public@0
   427
    'pngpath': pngpath, 'timestamp': timestamp,
tw-public@0
   428
    'pubpath': pubpath, 'drawpath': drawpath,
tw-public@0
   429
    'savelink': savelink, 'pagelink': pagelink, 'helplink': helplink,
tw@4539
   430
    'basename': wikiutil.escape(basename, 1),
tw-public@0
   431
})
tw-public@0
   432
tw-public@0
   433
tw-public@0
   434
def send_uploadform(pagename, request):
tw-public@0
   435
    """ Send the HTML code for the list of already stored attachments and
tw-public@0
   436
        the file upload form.
tw-public@0
   437
    """
tw-public@0
   438
    _ = request.getText
tw-public@0
   439
tw-public@0
   440
    if not request.user.may.read(pagename):
tw-public@0
   441
        request.write('<p>%s</p>' % _('You are not allowed to view this page.'))
tw-public@0
   442
        return
tw-public@0
   443
tw@1635
   444
    writeable = request.user.may.write(pagename)
tw-public@0
   445
tw@1635
   446
    # First send out the upload new attachment form on top of everything else.
tw@1635
   447
    # This avoids usability issues if you have to scroll down a lot to upload
tw@1635
   448
    # a new file when the page already has lots of attachments:
tw@1635
   449
    if writeable:
tw@3586
   450
        request.write('<h2>' + _("New Attachment") + '</h2>')
tw@1635
   451
        request.write("""
florian@4231
   452
<form action="%(url)s" method="POST" enctype="multipart/form-data">
tw-public@0
   453
<dl>
tw-public@0
   454
<dt>%(upload_label_file)s</dt>
tw-public@0
   455
<dd><input type="file" name="file" size="50"></dd>
tw-public@0
   456
<dt>%(upload_label_rename)s</dt>
tw-public@0
   457
<dd><input type="text" name="rename" size="50" value="%(rename)s"></dd>
tw@1765
   458
<dt>%(upload_label_overwrite)s</dt>
tw@1765
   459
<dd><input type="checkbox" name="overwrite" value="1" %(overwrite_checked)s></dd>
tw-public@0
   460
</dl>
tw@2983
   461
%(textcha)s
tw-public@0
   462
<p>
tw-public@0
   463
<input type="hidden" name="action" value="%(action_name)s">
tw-public@0
   464
<input type="hidden" name="do" value="upload">
tw-public@0
   465
<input type="submit" value="%(upload_button)s">
tw-public@0
   466
</p>
tw-public@0
   467
</form>
tw-public@0
   468
""" % {
florian@4231
   469
    'url': request.href(pagename),
tw-public@0
   470
    'action_name': action_name,
tw-public@0
   471
    'upload_label_file': _('File to upload'),
tw-public@0
   472
    'upload_label_rename': _('Rename to'),
tw@4498
   473
    'rename': wikiutil.escape(request.form.get('rename', ''), 1),
tw@1765
   474
    'upload_label_overwrite': _('Overwrite existing attachment of same name'),
florian@4201
   475
    'overwrite_checked': ('', 'checked')[request.form.get('overwrite', '0') == '1'],
tw-public@0
   476
    'upload_button': _('Upload'),
tw@2983
   477
    'textcha': TextCha(request).render(),
tw-public@0
   478
})
tw-public@0
   479
tw@1635
   480
    request.write('<h2>' + _("Attached Files") + '</h2>')
tw@1635
   481
    request.write(_get_filelist(request, pagename))
tw@1635
   482
tw@1635
   483
    if not writeable:
tw@1635
   484
        request.write('<p>%s</p>' % _('You are not allowed to attach a file to this page.'))
tw@1635
   485
tw@4426
   486
    if writeable and request.values.get('drawing'):
tw@1635
   487
        send_hotdraw(pagename, request)
tw@1635
   488
tw-public@0
   489
tw-public@0
   490
#############################################################################
tw-public@0
   491
### Web interface for file upload, viewing and deletion
tw-public@0
   492
#############################################################################
tw-public@0
   493
tw-public@0
   494
def execute(pagename, request):
tw@3150
   495
    """ Main dispatcher for the 'AttachFile' action. """
tw-public@0
   496
    _ = request.getText
tw-public@0
   497
florian@4222
   498
    do = request.values.get('do', 'upload_form')
florian@4201
   499
    handler = globals().get('_do_%s' % do)
tw@3152
   500
    if handler:
tw@3152
   501
        msg = handler(pagename, request)
tw-public@0
   502
    else:
florian@4222
   503
        msg = _('Unsupported AttachFile sub-action: %s') % wikiutil.escape(do)
tw-public@0
   504
    if msg:
tw-public@0
   505
        error_msg(pagename, request, msg)
tw-public@0
   506
tw@3150
   507
tw@3152
   508
def _do_upload_form(pagename, request):
tw@3152
   509
    upload_form(pagename, request)
tw@3152
   510
tw@3152
   511
tw-public@0
   512
def upload_form(pagename, request, msg=''):
tw-public@0
   513
    _ = request.getText
tw-public@0
   514
tw-public@0
   515
    # Use user interface language for this generated page
tw-public@0
   516
    request.setContentLanguage(request.lang)
alex@2966
   517
    request.theme.add_msg(msg, "dialog")
alex@2966
   518
    request.theme.send_title(_('Attachments for "%(pagename)s"') % {'pagename': pagename}, pagename=pagename)
tw-public@0
   519
    request.write('<div id="content">\n') # start content div
tw-public@0
   520
    send_uploadform(pagename, request)
tw-public@0
   521
    request.write('</div>\n') # end content div
tw@616
   522
    request.theme.send_footer(pagename)
tw@617
   523
    request.theme.send_closing_html()
tw-public@0
   524
tw@3150
   525
tw@3591
   526
def preprocess_filename(filename):
tw@3591
   527
    """ preprocess the filename we got from upload form,
tw@3591
   528
        strip leading drive and path (IE misbehaviour)
tw@3591
   529
    """
tw@3591
   530
    if filename and len(filename) > 1 and (filename[1] == ':' or filename[0] == '\\'): # C:.... or \path... or \\server\...
tw@3591
   531
        bsindex = filename.rfind('\\')
tw@3591
   532
        if bsindex >= 0:
tw@3591
   533
            filename = filename[bsindex+1:]
tw@3591
   534
    return filename
tw@3591
   535
tw@3591
   536
tw@3152
   537
def _do_upload(pagename, request):
tw-public@0
   538
    _ = request.getText
tw@3152
   539
    # Currently we only check TextCha for upload (this is what spammers ususally do),
tw@3152
   540
    # but it could be extended to more/all attachment write access
tw@3152
   541
    if not TextCha(request).check_answer_from_form():
tw@3152
   542
        return _('TextCha: Wrong answer! Go back and try again...')
tw@3152
   543
tw@3591
   544
    form = request.form
florian@4222
   545
florian@4222
   546
    file_upload = request.files.get('file')
florian@4222
   547
    if not file_upload:
florian@4222
   548
        # This might happen when trying to upload file names
florian@4222
   549
        # with non-ascii characters on Safari.
florian@4222
   550
        return _("No file content. Delete non ASCII characters from the file name and try again.")
florian@4222
   551
tw@3152
   552
    try:
florian@4222
   553
        overwrite = int(form.get('overwrite', '0'))
tw@3152
   554
    except:
tw@3152
   555
        overwrite = 0
tw@3152
   556
tw@3595
   557
    if not request.user.may.write(pagename):
tw@3152
   558
        return _('You are not allowed to attach a file to this page.')
tw@3152
   559
tw@3595
   560
    if overwrite and not request.user.may.delete(pagename):
tw@3595
   561
        return _('You are not allowed to overwrite a file attachment of this page.')
tw@3595
   562
florian@4201
   563
    rename = form.get('rename', u'').strip()
tw@3591
   564
    if rename:
tw@3591
   565
        target = rename
tw@3591
   566
    else:
florian@4222
   567
        target = file_upload.filename
tw@3591
   568
tw@3591
   569
    target = wikiutil.clean_input(target)
tw@3591
   570
tw@3591
   571
    if not target:
tw@3591
   572
        return _("Filename of attachment not specified!")
tw@3591
   573
alex@691
   574
    # add the attachment
alex@691
   575
    try:
florian@4222
   576
        target, bytes = add_attachment(request, pagename, target, file_upload.stream, overwrite=overwrite)
tw-public@0
   577
        msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
tw-public@0
   578
                " with %(bytes)d bytes saved.") % {
florian@4222
   579
                'target': target, 'filename': file_upload.filename, 'bytes': bytes}
alex@691
   580
    except AttachmentAlreadyExists:
alex@691
   581
        msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
florian@4222
   582
            'target': target, 'filename': file_upload.filename}
tw-public@0
   583
tw-public@0
   584
    # return attachment list
tw-public@0
   585
    upload_form(pagename, request, msg)
tw-public@0
   586
tw-public@0
   587
tw@4439
   588
class ContainerItem:
tw@4439
   589
    """ A storage container (multiple objects in 1 tarfile) """
tw@4439
   590
tw@4439
   591
    def __init__(self, request, pagename, containername):
tw@4439
   592
        self.request = request
tw@4439
   593
        self.pagename = pagename
tw@4439
   594
        self.containername = containername
tw@4439
   595
        self.container_filename = getFilename(request, pagename, containername)
tw@4439
   596
tw@4439
   597
    def member_url(self, member):
tw@4439
   598
        """ return URL for accessing container member
tw@4439
   599
            (we use same URL for get (GET) and put (POST))
tw@4439
   600
        """
tw@4439
   601
        url = Page(self.request, self.pagename).url(self.request, {
tw@4439
   602
            'action': 'AttachFile',
tw@4439
   603
            'do': 'box',  # shorter to type than 'container'
tw@4439
   604
            'target': self.containername,
tw@4439
   605
            #'member': member,
tw@4439
   606
        })
tw@4439
   607
        return url + '&member=%s' % member
tw@4439
   608
        # member needs to be last in qs because twikidraw looks for "file extension" at the end
tw@4439
   609
tw@4439
   610
    def get(self, member):
tw@4439
   611
        """ return a file-like object with the member file data
tw@4439
   612
        """
tw@4439
   613
        tf = tarfile.TarFile(self.container_filename)
tw@4439
   614
        return tf.extractfile(member)
tw@4439
   615
tw@4439
   616
    def put(self, member, content, content_length=None):
tw@4439
   617
        """ save data into a container's member """
tw@4439
   618
        tf = tarfile.TarFile(self.container_filename, mode='a')
tw@4439
   619
        if isinstance(member, unicode):
tw@4439
   620
            member = member.encode('utf-8')
tw@4439
   621
        ti = tarfile.TarInfo(member)
tw@4439
   622
        if isinstance(content, str):
tw@4439
   623
            if content_length is None:
tw@4439
   624
                content_length = len(content)
tw@4439
   625
            content = StringIO(content) # we need a file obj
tw@4439
   626
        elif not hasattr(content, 'read'):
tw@4439
   627
            logging.error("unsupported content object: %r" % content)
tw@4439
   628
            raise
tw@4439
   629
        assert content_length >= 0  # we don't want -1 interpreted as 4G-1
tw@4439
   630
        ti.size = content_length
tw@4439
   631
        tf.addfile(ti, content)
tw@4439
   632
        tf.close()
tw@4439
   633
tw@4439
   634
    def truncate(self):
tw@4439
   635
        f = open(self.container_filename, 'w')
tw@4439
   636
        f.close()
tw@4439
   637
tw@4439
   638
    def exists(self):
tw@4439
   639
        return os.path.exists(self.container_filename)
tw@4439
   640
tw@3152
   641
def _do_savedrawing(pagename, request):
tw@3180
   642
    _ = request.getText
tw@3180
   643
tw@3152
   644
    if not request.user.may.write(pagename):
tw@3152
   645
        return _('You are not allowed to save a drawing on this page.')
tw-public@0
   646
tw@4432
   647
    file_upload = request.files.get('filepath')
tw@4432
   648
    if not file_upload:
tw@4432
   649
        # This might happen when trying to upload file names
tw@4432
   650
        # with non-ascii characters on Safari.
tw@4432
   651
        return _("No file content. Delete non ASCII characters from the file name and try again.")
tw-public@0
   652
florian@4201
   653
    filename = request.form['filename']
tw-public@0
   654
    basepath, basename = os.path.split(filename)
tw-public@0
   655
    basename, ext = os.path.splitext(basename)
tw-public@0
   656
tw@4439
   657
    ci = ContainerItem(request, pagename, basename + '.tdraw')
tw@4439
   658
    filecontent = file_upload.stream
tw@4439
   659
    content_length = None
tw@4439
   660
    if ext == '.draw': # TWikiDraw POSTs this first
tw@4439
   661
        _addLogEntry(request, 'ATTDRW', pagename, basename + '.tdraw')
tw@4439
   662
        ci.truncate()
tw@3603
   663
        filecontent = filecontent.read() # read file completely into memory
tw@889
   664
        filecontent = filecontent.replace("\r", "")
tw@3572
   665
    elif ext == '.map':
tw@4439
   666
        # touch attachment directory to invalidate cache if new map is saved
tw@4439
   667
        attach_dir = getAttachDir(request, pagename)
tw@4439
   668
        os.utime(attach_dir, None)
tw@3603
   669
        filecontent = filecontent.read() # read file completely into memory
tw@3572
   670
        filecontent = filecontent.strip()
tw@4439
   671
    else:
tw@4439
   672
        #content_length = file_upload.content_length
tw@4439
   673
        # XXX gives -1 for wsgiref :( If this is fixed, we could use the file obj,
tw@4439
   674
        # without reading it into memory completely:
tw@4439
   675
        filecontent = filecontent.read()
tw-public@0
   676
tw@4439
   677
    ci.put(basename + ext, filecontent, content_length)
tw-public@0
   678
tw@3152
   679
    request.write("OK")
tw@3150
   680
tw@3152
   681
tw@3152
   682
def _do_del(pagename, request):
tw-public@0
   683
    _ = request.getText
tw-public@0
   684
tw@3152
   685
    pagename, filename, fpath = _access_file(pagename, request)
tw@3152
   686
    if not request.user.may.delete(pagename):
tw@3152
   687
        return _('You are not allowed to delete attachments on this page.')
tw@1920
   688
    if not filename:
tw@1920
   689
        return # error msg already sent in _access_file
tw-public@0
   690
tw-public@0
   691
    # delete file
tw-public@0
   692
    os.remove(fpath)
tw-public@0
   693
    _addLogEntry(request, 'ATTDEL', pagename, filename)
tw-public@0
   694
fpletz@1479
   695
    if request.cfg.xapian_search:
fpletz@1479
   696
        from MoinMoin.search.Xapian import Index
fpletz@1479
   697
        index = Index(request)
fpletz@1479
   698
        if index.exists:
fpletz@1479
   699
            index.remove_item(pagename, filename)
fpletz@1479
   700
tw-public@0
   701
    upload_form(pagename, request, msg=_("Attachment '%(filename)s' deleted.") % {'filename': filename})
tw-public@0
   702
tw@3150
   703
R@1608
   704
def move_file(request, pagename, new_pagename, attachment, new_attachment):
R@1608
   705
    _ = request.getText
R@1608
   706
tw@1609
   707
    newpage = Page(request, new_pagename)
R@1608
   708
    if newpage.exists(includeDeleted=1) and request.user.may.write(new_pagename) and request.user.may.delete(pagename):
tw@1609
   709
        new_attachment_path = os.path.join(getAttachDir(request, new_pagename,
R@1608
   710
                              create=1), new_attachment).encode(config.charset)
tw@1609
   711
        attachment_path = os.path.join(getAttachDir(request, pagename),
R@1608
   712
                          attachment).encode(config.charset)
R@1608
   713
R@1608
   714
        if os.path.exists(new_attachment_path):
tw@3171
   715
            upload_form(pagename, request,
tw@3171
   716
                msg=_("Attachment '%(new_pagename)s/%(new_filename)s' already exists.") % {
tw@3171
   717
                    'new_pagename': new_pagename,
tw@3171
   718
                    'new_filename': new_attachment})
R@1608
   719
            return
R@1608
   720
R@1608
   721
        if new_attachment_path != attachment_path:
tw@3171
   722
            # move file
R@1608
   723
            filesys.rename(attachment_path, new_attachment_path)
R@1608
   724
            _addLogEntry(request, 'ATTDEL', pagename, attachment)
R@1608
   725
            _addLogEntry(request, 'ATTNEW', new_pagename, new_attachment)
tw@3171
   726
            upload_form(pagename, request,
tw@3171
   727
                        msg=_("Attachment '%(pagename)s/%(filename)s' moved to '%(new_pagename)s/%(new_filename)s'.") % {
tw@3171
   728
                            'pagename': pagename,
tw@3171
   729
                            'filename': attachment,
tw@3171
   730
                            'new_pagename': new_pagename,
tw@3171
   731
                            'new_filename': new_attachment})
R@1608
   732
        else:
R@1608
   733
            upload_form(pagename, request, msg=_("Nothing changed"))
R@1608
   734
    else:
tw@3171
   735
        upload_form(pagename, request, msg=_("Page '%(new_pagename)s' does not exist or you don't have enough rights.") % {
tw@3171
   736
            'new_pagename': new_pagename})
R@1608
   737
tw@3150
   738
tw@3152
   739
def _do_attachment_move(pagename, request):
R@1608
   740
    _ = request.getText
tw@3152
   741
tw@3152
   742
    if 'cancel' in request.form:
tw@3152
   743
        return _('Move aborted!')
florian@4201
   744
    if not wikiutil.checkTicket(request, request.form['ticket']):
tw@3152
   745
        return _('Please use the interactive user interface to move attachments!')
tw@3152
   746
    if not request.user.may.delete(pagename):
tw@3152
   747
        return _('You are not allowed to move attachments from this page.')
tw@3152
   748
tw@1868
   749
    if 'newpagename' in request.form:
florian@4201
   750
        new_pagename = request.form.get('newpagename')
R@1608
   751
    else:
tw@3171
   752
        upload_form(pagename, request, msg=_("Move aborted because new page name is empty."))
tw@1868
   753
    if 'newattachmentname' in request.form:
florian@4201
   754
        new_attachment = request.form.get('newattachmentname')
R@1608
   755
        if new_attachment != wikiutil.taintfilename(new_attachment):
tw@1662
   756
            upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
R@1608
   757
                                  'filename': new_attachment})
R@1608
   758
            return
R@1608
   759
    else:
tw@3171
   760
        upload_form(pagename, request, msg=_("Move aborted because new attachment name is empty."))
R@1608
   761
florian@4201
   762
    attachment = request.form.get('oldattachmentname')
R@1608
   763
    move_file(request, pagename, new_pagename, attachment, new_attachment)
R@1608
   764
tw@3150
   765
tw@3152
   766
def _do_move(pagename, request):
R@1608
   767
    _ = request.getText
R@1608
   768
tw@3152
   769
    pagename, filename, fpath = _access_file(pagename, request)
tw@3152
   770
    if not request.user.may.delete(pagename):
tw@3152
   771
        return _('You are not allowed to move attachments from this page.')
tw@1920
   772
    if not filename:
tw@1920
   773
        return # error msg already sent in _access_file
R@1608
   774
R@1608
   775
    # move file
tw@3149
   776
    d = {'action': action_name,
florian@4235
   777
         'url': request.href(pagename),
R@1608
   778
         'do': 'attachment_move',
tw@1610
   779
         'ticket': wikiutil.createTicket(request),
R@1608
   780
         'pagename': pagename,
R@1608
   781
         'attachment_name': filename,
R@1608
   782
         'move': _('Move'),
R@1608
   783
         'cancel': _('Cancel'),
R@1608
   784
         'newname_label': _("New page name"),
R@1608
   785
         'attachment_label': _("New attachment name"),
R@1608
   786
        }
R@1608
   787
    formhtml = '''
florian@4235
   788
<form action="%(url)s" method="POST">
R@1608
   789
<input type="hidden" name="action" value="%(action)s">
R@1608
   790
<input type="hidden" name="do" value="%(do)s">
R@1608
   791
<input type="hidden" name="ticket" value="%(ticket)s">
R@1608
   792
<table>
R@1608
   793
    <tr>
R@1608
   794
        <td class="label"><label>%(newname_label)s</label></td>
R@1608
   795
        <td class="content">
rb@2708
   796
            <input type="text" name="newpagename" value="%(pagename)s" size="80">
R@1608
   797
        </td>
R@1608
   798
    </tr>
R@1608
   799
    <tr>
R@1608
   800
        <td class="label"><label>%(attachment_label)s</label></td>
R@1608
   801
        <td class="content">
rb@2708
   802
            <input type="text" name="newattachmentname" value="%(attachment_name)s" size="80">
R@1608
   803
        </td>
R@1608
   804
    </tr>
R@1608
   805
    <tr>
R@1608
   806
        <td></td>
R@1608
   807
        <td class="buttons">
R@1608
   808
            <input type="hidden" name="oldattachmentname" value="%(attachment_name)s">
R@1608
   809
            <input type="submit" name="move" value="%(move)s">
R@1608
   810
            <input type="submit" name="cancel" value="%(cancel)s">
R@1608
   811
        </td>
R@1608
   812
    </tr>
R@1608
   813
</table>
R@1608
   814
</form>''' % d
R@1608
   815
    thispage = Page(request, pagename)
alex@2966
   816
    request.theme.add_msg(formhtml, "dialog")
alex@2966
   817
    return thispage.send_page()
tw-public@0
   818
tw@3150
   819
tw@4439
   820
def _do_box(pagename, request):
tw@4439
   821
    _ = request.getText
tw@4439
   822
tw@4439
   823
    pagename, filename, fpath = _access_file(pagename, request)
tw@4439
   824
    if not request.user.may.read(pagename):
tw@4439
   825
        return _('You are not allowed to get attachments from this page.')
tw@4439
   826
    if not filename:
tw@4439
   827
        return # error msg already sent in _access_file
tw@4439
   828
tw@4439
   829
    timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
tw@4439
   830
    if_modified = request.if_modified_since
tw@4439
   831
    if if_modified and if_modified >= timestamp:
tw@4439
   832
        request.status_code = 304
tw@4439
   833
    else:
tw@4439
   834
        ci = ContainerItem(request, pagename, filename)
tw@4439
   835
        filename = wikiutil.taintfilename(request.values['member'])
tw@4439
   836
        mt = wikiutil.MimeType(filename=filename)
tw@4439
   837
        content_type = mt.content_type()
tw@4439
   838
        mime_type = mt.mime_type()
tw@4439
   839
tw@4439
   840
        # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
tw@4439
   841
        # There is no solution that is compatible to IE except stripping non-ascii chars
tw@4439
   842
        filename_enc = filename.encode(config.charset)
tw@4439
   843
tw@4439
   844
        # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
tw@4439
   845
        # we just let the user store them to disk ('attachment').
tw@4439
   846
        # For safe files, we directly show them inline (this also works better for IE).
tw@4439
   847
        dangerous = mime_type in request.cfg.mimetypes_xss_protect
tw@4439
   848
        content_dispo = dangerous and 'attachment' or 'inline'
tw@4439
   849
tw@4439
   850
        request.content_type = content_type
tw@4439
   851
        request.last_modified = timestamp
tw@4439
   852
        #request.content_length = os.path.getsize(fpath)
tw@4439
   853
        content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
tw@4439
   854
        request.headers.add('Content-Disposition', content_dispo_string)
tw@4439
   855
tw@4439
   856
        # send data
tw@4439
   857
        request.send_file(ci.get(filename))
tw@4439
   858
tw@4439
   859
tw@3152
   860
def _do_get(pagename, request):
tw@3180
   861
    _ = request.getText
tw@3180
   862
tw@3152
   863
    pagename, filename, fpath = _access_file(pagename, request)
tw@3152
   864
    if not request.user.may.read(pagename):
tw@3152
   865
        return _('You are not allowed to get attachments from this page.')
tw@801
   866
    if not filename:
tw@801
   867
        return # error msg already sent in _access_file
tw-public@0
   868
florian@4226
   869
    timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
florian@4226
   870
    if_modified = request.if_modified_since
florian@4226
   871
    if if_modified and if_modified >= timestamp:
florian@4183
   872
        request.status_code = 304
tw@1115
   873
    else:
tw@1115
   874
        mt = wikiutil.MimeType(filename=filename)
tw@1548
   875
        content_type = mt.content_type()
tw@1548
   876
        mime_type = mt.mime_type()
tw@1548
   877
tw@1548
   878
        # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
tw@1548
   879
        # There is no solution that is compatible to IE except stripping non-ascii chars
tw@1548
   880
        filename_enc = filename.encode(config.charset)
tw@1548
   881
tw@1548
   882
        # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
tw@1548
   883
        # we just let the user store them to disk ('attachment').
tw@1548
   884
        # For safe files, we directly show them inline (this also works better for IE).
tw@1548
   885
        dangerous = mime_type in request.cfg.mimetypes_xss_protect
tw@1548
   886
        content_dispo = dangerous and 'attachment' or 'inline'
tw@1548
   887
florian@4183
   888
        request.content_type = content_type
florian@4183
   889
        request.last_modified = timestamp
florian@4183
   890
        request.content_length = os.path.getsize(fpath)
florian@4176
   891
        content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
florian@4183
   892
        request.headers.add('Content-Disposition', content_dispo_string)
tw-public@0
   893
tw@1115
   894
        # send data
tw@3553
   895
        request.send_file(open(fpath, 'rb'))
tw-public@0
   896
tw@3150
   897
tw@3152
   898
def _do_install(pagename, request):
MoinMoin:PackageInstaller and zip support.">alex@80
   899
    _ = request.getText
MoinMoin:PackageInstaller and zip support.">alex@80
   900
tw@3152
   901
    pagename, target, targetpath = _access_file(pagename, request)
tw@3152
   902
    if not request.user.isSuperUser():
tw@3152
   903
        return _('You are not allowed to install files.')
MoinMoin:PackageInstaller and zip support.">alex@80
   904
    if not target:
MoinMoin:PackageInstaller and zip support.">alex@80
   905
        return
MoinMoin:PackageInstaller and zip support.">alex@80
   906
MoinMoin:PackageInstaller and zip support.">alex@80
   907
    package = packages.ZipPackage(request, targetpath)
MoinMoin:PackageInstaller and zip support.">alex@80
   908
MoinMoin:PackageInstaller and zip support.">alex@80
   909
    if package.isPackage():
MoinMoin:PackageInstaller and zip support.">alex@80
   910
        if package.installPackage():
tw@889
   911
            msg = _("Attachment '%(filename)s' installed.") % {'filename': wikiutil.escape(target)}
MoinMoin:PackageInstaller and zip support.">alex@80
   912
        else:
tw@889
   913
            msg = _("Installation of '%(filename)s' failed.") % {'filename': wikiutil.escape(target)}
tw@3149
   914
        if package.msg:
tw@3149
   915
            msg += "<br><pre>%s</pre>" % wikiutil.escape(package.msg)
MoinMoin:PackageInstaller and zip support.">alex@80
   916
    else:
johannes@2293
   917
        msg = _('The file %s is not a MoinMoin package file.') % wikiutil.escape(target)
MoinMoin:PackageInstaller and zip support.">alex@80
   918
MoinMoin:PackageInstaller and zip support.">alex@80
   919
    upload_form(pagename, request, msg=msg)
MoinMoin:PackageInstaller and zip support.">alex@80
   920
tw@3150
   921
tw@3700
   922
def _do_unzip(pagename, request, overwrite=False):
MoinMoin:PackageInstaller and zip support.">alex@80
   923
    _ = request.getText
tw@3700
   924
    pagename, filename, fpath = _access_file(pagename, request)
MoinMoin:PackageInstaller and zip support.">alex@80
   925
tw@3152
   926
    if not (request.user.may.delete(pagename) and request.user.may.read(pagename) and request.user.may.write(pagename)):
tw@3152
   927
        return _('You are not allowed to unzip attachments of this page.')
tw@3700
   928
tw@116
   929
    if not filename:
tw@116
   930
        return # error msg already sent in _access_file
MoinMoin:PackageInstaller and zip support.">alex@80
   931
tw@3712
   932
    try:
tw@3712
   933
        if not zipfile.is_zipfile(fpath):
tw@3712
   934
            return _('The file %(filename)s is not a .zip file.') % {'filename': filename}
tw@116
   935
tw@3712
   936
        # determine how which attachment names we have and how much space each is occupying
tw@3712
   937
        curr_fsizes = dict([(f, size(request, pagename, f)) for f in _get_files(request, pagename)])
MoinMoin:PackageInstaller and zip support.">alex@80
   938
tw@3712
   939
        # Checks for the existance of one common prefix path shared among
tw@3712
   940
        # all files in the zip file. If this is the case, remove the common prefix.
tw@3712
   941
        # We also prepare a dict of the new filenames->filesizes.
tw@3712
   942
        zip_path_sep = '/'  # we assume '/' is as zip standard suggests
tw@3712
   943
        fname_index = None
tw@3712
   944
        mapping = []
tw@3712
   945
        new_fsizes = {}
tw@3712
   946
        zf = zipfile.ZipFile(fpath)
tw@3712
   947
        for zi in zf.infolist():
tw@3712
   948
            name = zi.filename
tw@3712
   949
            if not name.endswith(zip_path_sep):  # a file (not a directory)
tw@3712
   950
                if fname_index is None:
tw@3712
   951
                    fname_index = name.rfind(zip_path_sep) + 1
tw@3712
   952
                    path = name[:fname_index]
tw@3712
   953
                if (name.rfind(zip_path_sep) + 1 != fname_index  # different prefix len
tw@3712
   954
                    or
tw@3712
   955
                    name[:fname_index] != path): # same len, but still different
tw@3712
   956
                    mapping = []  # zip is not acceptable
tw@3712
   957
                    break
tw@3712
   958
                if zi.file_size >= request.cfg.unzip_single_file_size:  # file too big
tw@3712
   959
                    mapping = []  # zip is not acceptable
tw@3712
   960
                    break
tw@3712
   961
                finalname = name[fname_index:]  # remove common path prefix
tw@3712
   962
                finalname = finalname.decode(config.charset, 'replace')  # replaces trash with \uFFFD char
tw@3712
   963
                mapping.append((name, finalname))
tw@3712
   964
                new_fsizes[finalname] = zi.file_size
MoinMoin:PackageInstaller and zip support.">alex@80
   965
tw@3712
   966
        # now we either have an empty mapping (if the zip is not acceptable),
tw@3712
   967
        # an identity mapping (no subdirs in zip, just all flat), or
tw@3712
   968
        # a mapping (origname, finalname) where origname is the zip member filename
tw@3712
   969
        # (including some prefix path) and finalname is a simple filename.
tw@889
   970
tw@3712
   971
        # calculate resulting total file size / count after unzipping:
tw@3712
   972
        if overwrite:
tw@3712
   973
            curr_fsizes.update(new_fsizes)
tw@3712
   974
            total = curr_fsizes
tw@3712
   975
        else:
tw@3712
   976
            new_fsizes.update(curr_fsizes)
tw@3712
   977
            total = new_fsizes
tw@3712
   978
        total_count = len(total)
tw@3712
   979
        total_size = sum(total.values())
MoinMoin:PackageInstaller and zip support.">alex@80
   980
tw@3712
   981
        if not mapping:
tw@3712
   982
            msg = _("Attachment '%(filename)s' not unzipped because some files in the zip "
tw@3712
   983
                    "are either not in the same directory or exceeded the single file size limit (%(maxsize_file)d kB)."
tw@3712
   984
                   ) % {'filename': filename,
tw@3712
   985
                        'maxsize_file': request.cfg.unzip_single_file_size / 1000, }
tw@3712
   986
        elif total_size > request.cfg.unzip_attachments_space:
tw@3712
   987
            msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
tw@3712
   988
                    "the per page attachment storage size limit (%(size)d kB).") % {
tw@3712
   989
                        'filename': filename,
tw@3712
   990
                        'size': request.cfg.unzip_attachments_space / 1000, }
tw@3712
   991
        elif total_count > request.cfg.unzip_attachments_count:
tw@3712
   992
            msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
tw@3712
   993
                    "the per page attachment count limit (%(count)d).") % {
tw@3712
   994
                        'filename': filename,
tw@3712
   995
                        'count': request.cfg.unzip_attachments_count, }
MoinMoin:PackageInstaller and zip support.">alex@80
   996
        else:
tw@3712
   997
            not_overwritten = []
tw@3712
   998
            for origname, finalname in mapping:
tw@3712
   999
                try:
tw@3712
  1000
                    # Note: reads complete zip member file into memory. ZipFile does not offer block-wise reading:
tw@3712
  1001
                    add_attachment(request, pagename, finalname, zf.read(origname), overwrite)
tw@3712
  1002
                except AttachmentAlreadyExists:
tw@3712
  1003
                    not_overwritten.append(finalname)
tw@3712
  1004
            if not_overwritten:
tw@3712
  1005
                msg = _("Attachment '%(filename)s' partially unzipped (did not overwrite: %(filelist)s).") % {
tw@3712
  1006
                        'filename': filename,
tw@3712
  1007
                        'filelist': ', '.join(not_overwritten), }
tw@3712
  1008
            else:
tw@3712
  1009
                msg = _("Attachment '%(filename)s' unzipped.") % {'filename': filename}
tw@3712
  1010
    except RuntimeError, err:
tw@3712
  1011
        # We don't want to crash with a traceback here (an exception
tw@3712
  1012
        # here could be caused by an uploaded defective zip file - and
tw@3712
  1013
        # if we crash here, the user does not get a UI to remove the
tw@3712
  1014
        # defective zip file again).
tw@3712
  1015
        # RuntimeError is raised by zipfile stdlib module in case of
tw@3712
  1016
        # problems (like inconsistent slash and backslash usage in the
tw@3712
  1017
        # archive).
tw@3712
  1018
        logging.exception("An exception within zip file attachment handling occurred:")
tw@3712
  1019
        msg = _("A severe error occurred:") + ' ' + str(err)
MoinMoin:PackageInstaller and zip support.">alex@80
  1020
MoinMoin:PackageInstaller and zip support.">alex@80
  1021
    upload_form(pagename, request, msg=wikiutil.escape(msg))
MoinMoin:PackageInstaller and zip support.">alex@80
  1022
tw@3150
  1023
tw-public@0
  1024
def send_viewfile(pagename, request):
tw-public@0
  1025
    _ = request.getText
tw@3149
  1026
    fmt = request.html_formatter
tw-public@0
  1027
tw@3169
  1028
    pagename, filename, fpath = _access_file(pagename, request)
tw@1920
  1029
    if not filename:
tw@1920
  1030
        return
tw-public@0
  1031
tw-public@0
  1032
    request.write('<h2>' + _("Attachment '%(filename)s'") % {'filename': filename} + '</h2>')
tw@2736
  1033
    # show a download link above the content
tw@2736
  1034
    label = _('Download')
moindev@3293
  1035
    link = (fmt.url(1, getAttachUrl(pagename, filename, request, do='get'), css_class="download") +
tw@3149
  1036
            fmt.text(label) +
tw@3149
  1037
            fmt.url(0))
tw@3149
  1038
    request.write('%s<br><br>' % link)
tw-public@0
  1039
tw@801
  1040
    mt = wikiutil.MimeType(filename=filename)
rb@3307
  1041
rb@3338
  1042
    # destinguishs if browser need a plugin in place
rb@3338
  1043
    if mt.major == 'image' and mt.minor in config.browser_supported_images:
tw@3149
  1044
        request.write('<img src="%s" alt="%s">' % (
tw@3149
  1045
            getAttachUrl(pagename, filename, request, escaped=1),
tw@3149
  1046
            wikiutil.escape(filename, 1)))
tw@801
  1047
        return
tw@801
  1048
    elif mt.major == 'text':
rb@1989
  1049
        ext = os.path.splitext(filename)[1]
rb@1989
  1050
        Parser = wikiutil.getParserForExtension(request.cfg, ext)
rb@1989
  1051
        if Parser is not None:
rb@1989
  1052
            try:
rb@1989
  1053
                content = file(fpath, 'r').read()
rb@1989
  1054
                content = wikiutil.decodeUnknownInput(content)
rb@1989
  1055
                colorizer = Parser(content, request, filename=filename)
rb@1989
  1056
                colorizer.format(request.formatter)
rb@1989
  1057
                return
rb@1989
  1058
            except IOError:
rb@1989
  1059
                pass
rb@1989
  1060
tw@1833
  1061
        request.write(request.formatter.preformatted(1))
tw@2286
  1062
        # If we have text but no colorizing parser we try to decode file contents.
tw@801
  1063
        content = open(fpath, 'r').read()
tw@801
  1064
        content = wikiutil.decodeUnknownInput(content)
tw@801
  1065
        content = wikiutil.escape(content)
tw@1833
  1066
        request.write(request.formatter.text(content))
tw@1833
  1067
        request.write(request.formatter.preformatted(0))
tw@801
  1068
        return
tw-public@0
  1069
tw@3706
  1070
    try:
tw@3706
  1071
        package = packages.ZipPackage(request, fpath)
tw@3706
  1072
        if package.isPackage():
tw@3706
  1073
            request.write("<pre><b>%s</b>\n%s</pre>" % (_("Package script:"), wikiutil.escape(package.getScript())))
tw@3706
  1074
            return
MoinMoin:PackageInstaller and zip support.">alex@80
  1075
tw@3706
  1076
        if zipfile.is_zipfile(fpath) and mt.minor == 'zip':
tw@3706
  1077
            zf = zipfile.ZipFile(fpath, mode='r')
tw@3706
  1078
            request.write("<pre>%-46s %19s %12s\n" % (_("File Name"), _("Modified")+" "*5, _("Size")))
tw@3706
  1079
            for zinfo in zf.filelist:
tw@3706
  1080
                date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time
tw@3706
  1081
                request.write(wikiutil.escape("%-46s %s %12d\n" % (zinfo.filename, date, zinfo.file_size)))
tw@3706
  1082
            request.write("</pre>")
tw@3706
  1083
            return
tw@3706
  1084
    except RuntimeError:
tw@3706
  1085
        # We don't want to crash with a traceback here (an exception
tw@3706
  1086
        # here could be caused by an uploaded defective zip file - and
tw@3706
  1087
        # if we crash here, the user does not get a UI to remove the
tw@3706
  1088
        # defective zip file again).
tw@3706
  1089
        # RuntimeError is raised by zipfile stdlib module in case of
tw@3706
  1090
        # problems (like inconsistent slash and backslash usage in the
tw@3706
  1091
        # archive).
tw@3706
  1092
        logging.exception("An exception within zip file attachment handling occurred:")
MoinMoin:PackageInstaller and zip support.">alex@80
  1093
        return
MoinMoin:PackageInstaller and zip support.">alex@80
  1094
rb@3266
  1095
    from MoinMoin import macro
rb@3266
  1096
    from MoinMoin.parser.text import Parser
rb@3266
  1097
rb@1952
  1098
    macro.request = request
rb@1952
  1099
    macro.formatter = request.html_formatter
rb@3266
  1100
    p = Parser("##\n", request)
rb@3266
  1101
    m = macro.Macro(p)
rb@1952
  1102
tw@2286
  1103
    # use EmbedObject to view valid mime types
rb@1967
  1104
    if mt is None:
rb@1952
  1105
        request.write('<p>' + _("Unknown file type, cannot display this attachment inline.") + '</p>')
tw@3149
  1106
        link = (fmt.url(1, getAttachUrl(pagename, filename, request)) +
tw@3149
  1107
                fmt.text(filename) +
tw@3149
  1108
                fmt.url(0))
tw@3149
  1109
        request.write('For using an external program follow this link %s' % link)
rb@1952
  1110
        return
rb@3266
  1111
    request.write(m.execute('EmbedObject', u'target=%s, pagename=%s' % (filename, pagename)))
rb@1952
  1112
    return
tw-public@0
  1113
tw-public@0
  1114
tw@3152
  1115
def _do_view(pagename, request):
tw-public@0
  1116
    _ = request.getText
tw-public@0
  1117
tw@3152
  1118
    orig_pagename = pagename
tw@3152
  1119
    pagename, filename, fpath = _access_file(pagename, request)
tw@3152
  1120
    if not request.user.may.read(pagename):
tw@3152
  1121
        return _('You are not allowed to view attachments of this page.')
tw@1920
  1122
    if not filename:
tw@1920
  1123
        return
tw-public@0
  1124
tw-public@0
  1125
    # send header & title
tw-public@0
  1126
    # Use user interface language for this generated page
tw-public@0
  1127
    request.setContentLanguage(request.lang)
tw@3122
  1128
    title = _('attachment:%(filename)s of %(pagename)s') % {
tw-public@0
  1129
        'filename': filename, 'pagename': pagename}
tw@616
  1130
    request.theme.send_title(title, pagename=pagename)
tw-public@0
  1131
tw-public@0
  1132
    # send body
tw@1833
  1133
    request.write(request.formatter.startContent())
tw@3169
  1134
    send_viewfile(orig_pagename, request)
tw-public@0
  1135
    send_uploadform(pagename, request)
tw@1833
  1136
    request.write(request.formatter.endContent())
tw-public@0
  1137
tw@616
  1138
    request.theme.send_footer(pagename)
tw@617
  1139
    request.theme.send_closing_html()
tw-public@0
  1140
tw@3151
  1141
tw-public@0
  1142
#############################################################################
tw-public@0
  1143
### File attachment administration
tw-public@0
  1144
#############################################################################
tw-public@0
  1145
tw-public@0
  1146
def do_admin_browser(request):
tw@3150
  1147
    """ Browser for SystemAdmin macro. """
tw-public@0
  1148
    from MoinMoin.util.dataset import TupleDataset, Column
tw-public@0
  1149
    _ = request.getText
tw-public@0
  1150
tw-public@0
  1151
    data = TupleDataset()
tw-public@0
  1152
    data.columns = [
tw-public@0
  1153
        Column('page', label=('Page')),
tw-public@0
  1154
        Column('file', label=('Filename')),
tw@889
  1155
        Column('size', label=_('Size'), align='right'),
tw-public@0
  1156
    ]
tw-public@0
  1157
tw-public@0
  1158
    # iterate over pages that might have attachments
tw-public@0
  1159
    pages = request.rootpage.getPageList()
tw-public@0
  1160
    for pagename in pages:
tw-public@0
  1161
        # check for attachments directory
tw-public@0
  1162
        page_dir = getAttachDir(request, pagename)
tw-public@0
  1163
        if os.path.isdir(page_dir):
tw-public@0
  1164
            # iterate over files of the page
tw-public@0
  1165
            files = os.listdir(page_dir)
tw-public@0
  1166
            for filename in files:
tw-public@0
  1167
                filepath = os.path.join(page_dir, filename)
tw-public@0
  1168
                data.addRow((
tw-public@0
  1169
                    Page(request, pagename).link_to(request, querystr="action=AttachFile"),
tw-public@0
  1170
                    wikiutil.escape(filename.decode(config.charset)),
tw-public@0
  1171
                    os.path.getsize(filepath),
tw-public@0
  1172
                ))
tw-public@0
  1173
tw-public@0
  1174
    if data:
tw-public@0
  1175
        from MoinMoin.widget.browser import DataBrowserWidget
tw-public@0
  1176
tw-public@0
  1177
        browser = DataBrowserWidget(request)
tw-public@0
  1178
        browser.setData(data)
rb@4093
  1179
        return browser.render(method="GET")
tw-public@0
  1180
tw-public@0
  1181
    return ''
tw-public@0
  1182