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