Back to index

moin  1.9.0~rc2
AttachFile.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - AttachFile action
00004 
00005     This action lets a page have multiple attachment files.
00006     It creates a folder <data>/pages/<pagename>/attachments
00007     and keeps everything in there.
00008 
00009     Form values: action=Attachment
00010     1. with no 'do' key: returns file upload form
00011     2. do=attach: accept file upload and saves the file in
00012        ../attachment/pagename/
00013     3. /pagename/fname?action=Attachment&do=get[&mimetype=type]:
00014        return contents of the attachment file with the name fname.
00015     4. /pathname/fname, do=view[&mimetype=type]:create a page
00016        to view the content of the file
00017 
00018     To link to an attachment, use [[attachment:file.txt]],
00019     to embed an attachment, use {{attachment:file.png}}.
00020 
00021     @copyright: 2001 by Ken Sugino (sugino@mediaone.net),
00022                 2001-2004 by Juergen Hermann <jh@web.de>,
00023                 2005 MoinMoin:AlexanderSchremmer,
00024                 2005 DiegoOngaro at ETSZONE (diego@etszone.com),
00025                 2005-2007 MoinMoin:ReimarBauer,
00026                 2007-2008 MoinMoin:ThomasWaldmann
00027     @license: GNU GPL, see COPYING for details.
00028 """
00029 
00030 import os, time, zipfile, errno, datetime
00031 from StringIO import StringIO
00032 
00033 from werkzeug import http_date
00034 
00035 from MoinMoin import log
00036 logging = log.getLogger(__name__)
00037 
00038 # keep both imports below as they are, order is important:
00039 from MoinMoin import wikiutil
00040 import mimetypes
00041 
00042 from MoinMoin import config, packages
00043 from MoinMoin.Page import Page
00044 from MoinMoin.util import filesys, timefuncs
00045 from MoinMoin.security.textcha import TextCha
00046 from MoinMoin.events import FileAttachedEvent, FileRemovedEvent, send_event
00047 from MoinMoin.support import tarfile
00048 
00049 action_name = __name__.split('.')[-1]
00050 
00051 #############################################################################
00052 ### External interface - these are called from the core code
00053 #############################################################################
00054 
00055 class AttachmentAlreadyExists(Exception):
00056     pass
00057 
00058 
00059 def getBasePath(request):
00060     """ Get base path where page dirs for attachments are stored. """
00061     return request.rootpage.getPagePath('pages')
00062 
00063 
00064 def getAttachDir(request, pagename, create=0):
00065     """ Get directory where attachments for page `pagename` are stored. """
00066     if request.page and pagename == request.page.page_name:
00067         page = request.page # reusing existing page obj is faster
00068     else:
00069         page = Page(request, pagename)
00070     return page.getPagePath("attachments", check_create=create)
00071 
00072 
00073 def absoluteName(url, pagename):
00074     """ Get (pagename, filename) of an attachment: link
00075         @param url: PageName/filename.ext or filename.ext (unicode)
00076         @param pagename: name of the currently processed page (unicode)
00077         @rtype: tuple of unicode
00078         @return: PageName, filename.ext
00079     """
00080     url = wikiutil.AbsPageName(pagename, url)
00081     pieces = url.split(u'/')
00082     if len(pieces) == 1:
00083         return pagename, pieces[0]
00084     else:
00085         return u"/".join(pieces[:-1]), pieces[-1]
00086 
00087 
00088 def get_action(request, filename, do):
00089     generic_do_mapping = {
00090         # do -> action
00091         'get': action_name,
00092         'view': action_name,
00093         'move': action_name,
00094         'del': action_name,
00095         'unzip': action_name,
00096         'install': action_name,
00097         'upload_form': action_name,
00098     }
00099     basename, ext = os.path.splitext(filename)
00100     do_mapping = request.cfg.extensions_mapping.get(ext, {})
00101     action = do_mapping.get(do, None)
00102     if action is None:
00103         # we have no special support for this,
00104         # look up whether we have generic support:
00105         action = generic_do_mapping.get(do, None)
00106     return action
00107 
00108 
00109 def getAttachUrl(pagename, filename, request, addts=0, do='get'):
00110     """ Get URL that points to attachment `filename` of page `pagename`.
00111         For upload url, call with do='upload_form'.
00112         Returns the URL to do the specified "do" action or None,
00113         if this action is not supported.
00114     """
00115     action = get_action(request, filename, do)
00116     if action:
00117         url = request.href(pagename, action=action, do=do, target=filename)
00118         return url
00119 
00120 
00121 def getIndicator(request, pagename):
00122     """ Get an attachment indicator for a page (linked clip image) or
00123         an empty string if not attachments exist.
00124     """
00125     _ = request.getText
00126     attach_dir = getAttachDir(request, pagename)
00127     if not os.path.exists(attach_dir):
00128         return ''
00129 
00130     files = os.listdir(attach_dir)
00131     if not files:
00132         return ''
00133 
00134     fmt = request.formatter
00135     attach_count = _('[%d attachments]') % len(files)
00136     attach_icon = request.theme.make_icon('attach', vars={'attach_count': attach_count})
00137     attach_link = (fmt.url(1, request.href(pagename, action=action_name), rel='nofollow') +
00138                    attach_icon +
00139                    fmt.url(0))
00140     return attach_link
00141 
00142 
00143 def getFilename(request, pagename, filename):
00144     """ make complete pathfilename of file "name" attached to some page "pagename"
00145         @param request: request object
00146         @param pagename: name of page where the file is attached to (unicode)
00147         @param filename: filename of attached file (unicode)
00148         @rtype: string (in config.charset encoding)
00149         @return: complete path/filename of attached file
00150     """
00151     if isinstance(filename, unicode):
00152         filename = filename.encode(config.charset)
00153     return os.path.join(getAttachDir(request, pagename, create=1), filename)
00154 
00155 
00156 def exists(request, pagename, filename):
00157     """ check if page <pagename> has a file <filename> attached """
00158     fpath = getFilename(request, pagename, filename)
00159     return os.path.exists(fpath)
00160 
00161 
00162 def size(request, pagename, filename):
00163     """ return file size of file attachment """
00164     fpath = getFilename(request, pagename, filename)
00165     return os.path.getsize(fpath)
00166 
00167 
00168 def info(pagename, request):
00169     """ Generate snippet with info on the attachment for page `pagename`. """
00170     _ = request.getText
00171 
00172     attach_dir = getAttachDir(request, pagename)
00173     files = []
00174     if os.path.isdir(attach_dir):
00175         files = os.listdir(attach_dir)
00176     page = Page(request, pagename)
00177     link = page.url(request, {'action': action_name})
00178     attach_info = _('There are <a href="%(link)s">%(count)s attachment(s)</a> stored for this page.') % {
00179         'count': len(files),
00180         'link': wikiutil.escape(link)
00181         }
00182     return "\n<p>\n%s\n</p>\n" % attach_info
00183 
00184 
00185 def _write_stream(content, stream, bufsize=8192):
00186     if hasattr(content, 'read'): # looks file-like
00187         import shutil
00188         shutil.copyfileobj(content, stream, bufsize)
00189     elif isinstance(content, str):
00190         stream.write(content)
00191     else:
00192         logging.error("unsupported content object: %r" % content)
00193         raise
00194 
00195 def add_attachment(request, pagename, target, filecontent, overwrite=0):
00196     """ save <filecontent> to an attachment <target> of page <pagename>
00197 
00198         filecontent can be either a str (in memory file content),
00199         or an open file object (file content in e.g. a tempfile).
00200     """
00201     # replace illegal chars
00202     target = wikiutil.taintfilename(target)
00203 
00204     # get directory, and possibly create it
00205     attach_dir = getAttachDir(request, pagename, create=1)
00206     fpath = os.path.join(attach_dir, target).encode(config.charset)
00207 
00208     exists = os.path.exists(fpath)
00209     if exists:
00210         if overwrite:
00211             remove_attachment(request, pagename, target)
00212         else:
00213             raise AttachmentAlreadyExists
00214 
00215     # save file
00216     stream = open(fpath, 'wb')
00217     try:
00218         _write_stream(filecontent, stream)
00219     finally:
00220         stream.close()
00221 
00222     _addLogEntry(request, 'ATTNEW', pagename, target)
00223 
00224     filesize = os.path.getsize(fpath)
00225     event = FileAttachedEvent(request, pagename, target, filesize)
00226     send_event(event)
00227 
00228     return target, filesize
00229 
00230 
00231 def remove_attachment(request, pagename, target):
00232     """ remove attachment <target> of page <pagename>
00233     """
00234     # replace illegal chars
00235     target = wikiutil.taintfilename(target)
00236 
00237     # get directory, do not create it
00238     attach_dir = getAttachDir(request, pagename, create=0)
00239     # remove file
00240     fpath = os.path.join(attach_dir, target).encode(config.charset)
00241     try:
00242         filesize = os.path.getsize(fpath)
00243         os.remove(fpath)
00244     except:
00245         # either it is gone already or we have no rights - not much we can do about it
00246         filesize = 0
00247     else:
00248         _addLogEntry(request, 'ATTDEL', pagename, target)
00249 
00250         event = FileRemovedEvent(request, pagename, target, filesize)
00251         send_event(event)
00252 
00253     return target, filesize
00254 
00255 
00256 #############################################################################
00257 ### Internal helpers
00258 #############################################################################
00259 
00260 def _addLogEntry(request, action, pagename, filename):
00261     """ Add an entry to the edit log on uploads and deletes.
00262 
00263         `action` should be "ATTNEW" or "ATTDEL"
00264     """
00265     from MoinMoin.logfile import editlog
00266     t = wikiutil.timestamp2version(time.time())
00267     fname = wikiutil.url_quote(filename)
00268 
00269     # Write to global log
00270     log = editlog.EditLog(request)
00271     log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
00272 
00273     # Write to local log
00274     log = editlog.EditLog(request, rootpagename=pagename)
00275     log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
00276 
00277 
00278 def _access_file(pagename, request):
00279     """ Check form parameter `target` and return a tuple of
00280         `(pagename, filename, filepath)` for an existing attachment.
00281 
00282         Return `(pagename, None, None)` if an error occurs.
00283     """
00284     _ = request.getText
00285 
00286     error = None
00287     if not request.values.get('target'):
00288         error = _("Filename of attachment not specified!")
00289     else:
00290         filename = wikiutil.taintfilename(request.values['target'])
00291         fpath = getFilename(request, pagename, filename)
00292 
00293         if os.path.isfile(fpath):
00294             return (pagename, filename, fpath)
00295         error = _("Attachment '%(filename)s' does not exist!") % {'filename': filename}
00296 
00297     error_msg(pagename, request, error)
00298     return (pagename, None, None)
00299 
00300 
00301 def _build_filelist(request, pagename, showheader, readonly, mime_type='*'):
00302     _ = request.getText
00303     fmt = request.html_formatter
00304 
00305     # access directory
00306     attach_dir = getAttachDir(request, pagename)
00307     files = _get_files(request, pagename)
00308 
00309     if mime_type != '*':
00310         files = [fname for fname in files if mime_type == mimetypes.guess_type(fname)[0]]
00311 
00312     html = []
00313     if files:
00314         if showheader:
00315             html.append(fmt.rawHTML(_(
00316                 "To refer to attachments on a page, use '''{{{attachment:filename}}}''', \n"
00317                 "as shown below in the list of files. \n"
00318                 "Do '''NOT''' use the URL of the {{{[get]}}} link, \n"
00319                 "since this is subject to change and can break easily.",
00320                 wiki=True
00321             )))
00322 
00323         label_del = _("del")
00324         label_move = _("move")
00325         label_get = _("get")
00326         label_edit = _("edit")
00327         label_view = _("view")
00328         label_unzip = _("unzip")
00329         label_install = _("install")
00330 
00331         may_read = request.user.may.read(pagename)
00332         may_write = request.user.may.write(pagename)
00333         may_delete = request.user.may.delete(pagename)
00334 
00335         html.append(fmt.bullet_list(1))
00336         for file in files:
00337             mt = wikiutil.MimeType(filename=file)
00338             fullpath = os.path.join(attach_dir, file).encode(config.charset)
00339             st = os.stat(fullpath)
00340             base, ext = os.path.splitext(file)
00341             parmdict = {'file': wikiutil.escape(file),
00342                         'fsize': "%.1f" % (float(st.st_size) / 1024),
00343                         'fmtime': request.user.getFormattedDateTime(st.st_mtime),
00344                        }
00345 
00346             links = []
00347             if may_delete and not readonly:
00348                 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='del')) +
00349                              fmt.text(label_del) +
00350                              fmt.url(0))
00351 
00352             if may_delete and not readonly:
00353                 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='move')) +
00354                              fmt.text(label_move) +
00355                              fmt.url(0))
00356 
00357             links.append(fmt.url(1, getAttachUrl(pagename, file, request)) +
00358                          fmt.text(label_get) +
00359                          fmt.url(0))
00360 
00361             links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
00362                          fmt.text(label_view) +
00363                          fmt.url(0))
00364 
00365             if may_write and not readonly:
00366                 edit_url = getAttachUrl(pagename, file, request, do='modify')
00367                 if edit_url:
00368                     links.append(fmt.url(1, edit_url) +
00369                                  fmt.text(label_edit) +
00370                                  fmt.url(0))
00371 
00372             try:
00373                 is_zipfile = zipfile.is_zipfile(fullpath)
00374                 if is_zipfile:
00375                     is_package = packages.ZipPackage(request, fullpath).isPackage()
00376                     if is_package and request.user.isSuperUser():
00377                         links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='install')) +
00378                                      fmt.text(label_install) +
00379                                      fmt.url(0))
00380                     elif (not is_package and mt.minor == 'zip' and
00381                           may_read and may_write and may_delete):
00382                         links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='unzip')) +
00383                                      fmt.text(label_unzip) +
00384                                      fmt.url(0))
00385             except RuntimeError:
00386                 # We don't want to crash with a traceback here (an exception
00387                 # here could be caused by an uploaded defective zip file - and
00388                 # if we crash here, the user does not get a UI to remove the
00389                 # defective zip file again).
00390                 # RuntimeError is raised by zipfile stdlib module in case of
00391                 # problems (like inconsistent slash and backslash usage in the
00392                 # archive).
00393                 logging.exception("An exception within zip file attachment handling occurred:")
00394 
00395             html.append(fmt.listitem(1))
00396             html.append("[%s]" % "&nbsp;| ".join(links))
00397             html.append(" (%(fmtime)s, %(fsize)s KB) [[attachment:%(file)s]]" % parmdict)
00398             html.append(fmt.listitem(0))
00399         html.append(fmt.bullet_list(0))
00400 
00401     else:
00402         if showheader:
00403             html.append(fmt.paragraph(1))
00404             html.append(fmt.text(_("No attachments stored for %(pagename)s") % {
00405                                    'pagename': pagename}))
00406             html.append(fmt.paragraph(0))
00407 
00408     return ''.join(html)
00409 
00410 
00411 def _get_files(request, pagename):
00412     attach_dir = getAttachDir(request, pagename)
00413     if os.path.isdir(attach_dir):
00414         files = [fn.decode(config.charset) for fn in os.listdir(attach_dir)]
00415         files.sort()
00416     else:
00417         files = []
00418     return files
00419 
00420 
00421 def _get_filelist(request, pagename):
00422     return _build_filelist(request, pagename, 1, 0)
00423 
00424 
00425 def error_msg(pagename, request, msg):
00426     msg = wikiutil.escape(msg)
00427     request.theme.add_msg(msg, "error")
00428     Page(request, pagename).send_page()
00429 
00430 
00431 #############################################################################
00432 ### Create parts of the Web interface
00433 #############################################################################
00434 
00435 def send_link_rel(request, pagename):
00436     files = _get_files(request, pagename)
00437     for fname in files:
00438         url = getAttachUrl(pagename, fname, request, do='view')
00439         request.write(u'<link rel="Appendix" title="%s" href="%s">\n' % (
00440                       wikiutil.escape(fname, 1),
00441                       wikiutil.escape(url, 1)))
00442 
00443 def send_uploadform(pagename, request):
00444     """ Send the HTML code for the list of already stored attachments and
00445         the file upload form.
00446     """
00447     _ = request.getText
00448 
00449     if not request.user.may.read(pagename):
00450         request.write('<p>%s</p>' % _('You are not allowed to view this page.'))
00451         return
00452 
00453     writeable = request.user.may.write(pagename)
00454 
00455     # First send out the upload new attachment form on top of everything else.
00456     # This avoids usability issues if you have to scroll down a lot to upload
00457     # a new file when the page already has lots of attachments:
00458     if writeable:
00459         request.write('<h2>' + _("New Attachment") + '</h2>')
00460         request.write("""
00461 <form action="%(url)s" method="POST" enctype="multipart/form-data">
00462 <dl>
00463 <dt>%(upload_label_file)s</dt>
00464 <dd><input type="file" name="file" size="50"></dd>
00465 <dt>%(upload_label_target)s</dt>
00466 <dd><input type="text" name="target" size="50" value="%(target)s"></dd>
00467 <dt>%(upload_label_overwrite)s</dt>
00468 <dd><input type="checkbox" name="overwrite" value="1" %(overwrite_checked)s></dd>
00469 </dl>
00470 %(textcha)s
00471 <p>
00472 <input type="hidden" name="action" value="%(action_name)s">
00473 <input type="hidden" name="do" value="upload">
00474 <input type="submit" value="%(upload_button)s">
00475 </p>
00476 </form>
00477 """ % {
00478     'url': request.href(pagename),
00479     'action_name': action_name,
00480     'upload_label_file': _('File to upload'),
00481     'upload_label_target': _('Rename to'),
00482     'target': wikiutil.escape(request.values.get('target', ''), 1),
00483     'upload_label_overwrite': _('Overwrite existing attachment of same name'),
00484     'overwrite_checked': ('', 'checked')[request.form.get('overwrite', '0') == '1'],
00485     'upload_button': _('Upload'),
00486     'textcha': TextCha(request).render(),
00487 })
00488 
00489     request.write('<h2>' + _("Attached Files") + '</h2>')
00490     request.write(_get_filelist(request, pagename))
00491 
00492     if not writeable:
00493         request.write('<p>%s</p>' % _('You are not allowed to attach a file to this page.'))
00494 
00495 #############################################################################
00496 ### Web interface for file upload, viewing and deletion
00497 #############################################################################
00498 
00499 def execute(pagename, request):
00500     """ Main dispatcher for the 'AttachFile' action. """
00501     _ = request.getText
00502 
00503     do = request.values.get('do', 'upload_form')
00504     handler = globals().get('_do_%s' % do)
00505     if handler:
00506         msg = handler(pagename, request)
00507     else:
00508         msg = _('Unsupported AttachFile sub-action: %s') % do
00509     if msg:
00510         error_msg(pagename, request, msg)
00511 
00512 
00513 def _do_upload_form(pagename, request):
00514     upload_form(pagename, request)
00515 
00516 
00517 def upload_form(pagename, request, msg=''):
00518     if msg:
00519         msg = wikiutil.escape(msg)
00520     _ = request.getText
00521 
00522     # Use user interface language for this generated page
00523     request.setContentLanguage(request.lang)
00524     request.theme.add_msg(msg, "dialog")
00525     request.theme.send_title(_('Attachments for "%(pagename)s"') % {'pagename': pagename}, pagename=pagename)
00526     request.write('<div id="content">\n') # start content div
00527     send_uploadform(pagename, request)
00528     request.write('</div>\n') # end content div
00529     request.theme.send_footer(pagename)
00530     request.theme.send_closing_html()
00531 
00532 
00533 def _do_upload(pagename, request):
00534     _ = request.getText
00535     # Currently we only check TextCha for upload (this is what spammers ususally do),
00536     # but it could be extended to more/all attachment write access
00537     if not TextCha(request).check_answer_from_form():
00538         return _('TextCha: Wrong answer! Go back and try again...')
00539 
00540     form = request.form
00541 
00542     file_upload = request.files.get('file')
00543     if not file_upload:
00544         # This might happen when trying to upload file names
00545         # with non-ascii characters on Safari.
00546         return _("No file content. Delete non ASCII characters from the file name and try again.")
00547 
00548     try:
00549         overwrite = int(form.get('overwrite', '0'))
00550     except:
00551         overwrite = 0
00552 
00553     if not request.user.may.write(pagename):
00554         return _('You are not allowed to attach a file to this page.')
00555 
00556     if overwrite and not request.user.may.delete(pagename):
00557         return _('You are not allowed to overwrite a file attachment of this page.')
00558 
00559     target = form.get('target', u'').strip()
00560     if not target:
00561         target = file_upload.filename or u''
00562 
00563     target = wikiutil.clean_input(target)
00564 
00565     if not target:
00566         return _("Filename of attachment not specified!")
00567 
00568     # add the attachment
00569     try:
00570         target, bytes = add_attachment(request, pagename, target, file_upload.stream, overwrite=overwrite)
00571         msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
00572                 " with %(bytes)d bytes saved.") % {
00573                 'target': target, 'filename': file_upload.filename, 'bytes': bytes}
00574     except AttachmentAlreadyExists:
00575         msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
00576             'target': target, 'filename': file_upload.filename}
00577 
00578     # return attachment list
00579     upload_form(pagename, request, msg)
00580 
00581 
00582 class ContainerItem:
00583     """ A storage container (multiple objects in 1 tarfile) """
00584 
00585     def __init__(self, request, pagename, containername):
00586         self.request = request
00587         self.pagename = pagename
00588         self.containername = containername
00589         self.container_filename = getFilename(request, pagename, containername)
00590 
00591     def member_url(self, member):
00592         """ return URL for accessing container member
00593             (we use same URL for get (GET) and put (POST))
00594         """
00595         url = Page(self.request, self.pagename).url(self.request, {
00596             'action': 'AttachFile',
00597             'do': 'box',  # shorter to type than 'container'
00598             'target': self.containername,
00599             #'member': member,
00600         })
00601         return url + '&member=%s' % member
00602         # member needs to be last in qs because twikidraw looks for "file extension" at the end
00603 
00604     def get(self, member):
00605         """ return a file-like object with the member file data
00606         """
00607         tf = tarfile.TarFile(self.container_filename)
00608         return tf.extractfile(member)
00609 
00610     def put(self, member, content, content_length=None):
00611         """ save data into a container's member """
00612         tf = tarfile.TarFile(self.container_filename, mode='a')
00613         if isinstance(member, unicode):
00614             member = member.encode('utf-8')
00615         ti = tarfile.TarInfo(member)
00616         if isinstance(content, str):
00617             if content_length is None:
00618                 content_length = len(content)
00619             content = StringIO(content) # we need a file obj
00620         elif not hasattr(content, 'read'):
00621             logging.error("unsupported content object: %r" % content)
00622             raise
00623         assert content_length >= 0  # we don't want -1 interpreted as 4G-1
00624         ti.size = content_length
00625         tf.addfile(ti, content)
00626         tf.close()
00627 
00628     def truncate(self):
00629         f = open(self.container_filename, 'w')
00630         f.close()
00631 
00632     def exists(self):
00633         return os.path.exists(self.container_filename)
00634 
00635 def _do_del(pagename, request):
00636     _ = request.getText
00637 
00638     pagename, filename, fpath = _access_file(pagename, request)
00639     if not request.user.may.delete(pagename):
00640         return _('You are not allowed to delete attachments on this page.')
00641     if not filename:
00642         return # error msg already sent in _access_file
00643 
00644     remove_attachment(request, pagename, filename)
00645 
00646     upload_form(pagename, request, msg=_("Attachment '%(filename)s' deleted.") % {'filename': filename})
00647 
00648 
00649 def move_file(request, pagename, new_pagename, attachment, new_attachment):
00650     _ = request.getText
00651 
00652     newpage = Page(request, new_pagename)
00653     if newpage.exists(includeDeleted=1) and request.user.may.write(new_pagename) and request.user.may.delete(pagename):
00654         new_attachment_path = os.path.join(getAttachDir(request, new_pagename,
00655                               create=1), new_attachment).encode(config.charset)
00656         attachment_path = os.path.join(getAttachDir(request, pagename),
00657                           attachment).encode(config.charset)
00658 
00659         if os.path.exists(new_attachment_path):
00660             upload_form(pagename, request,
00661                 msg=_("Attachment '%(new_pagename)s/%(new_filename)s' already exists.") % {
00662                     'new_pagename': new_pagename,
00663                     'new_filename': new_attachment})
00664             return
00665 
00666         if new_attachment_path != attachment_path:
00667             # move file
00668             filesys.rename(attachment_path, new_attachment_path)
00669             _addLogEntry(request, 'ATTDEL', pagename, attachment)
00670             _addLogEntry(request, 'ATTNEW', new_pagename, new_attachment)
00671             upload_form(pagename, request,
00672                         msg=_("Attachment '%(pagename)s/%(filename)s' moved to '%(new_pagename)s/%(new_filename)s'.") % {
00673                             'pagename': pagename,
00674                             'filename': attachment,
00675                             'new_pagename': new_pagename,
00676                             'new_filename': new_attachment})
00677         else:
00678             upload_form(pagename, request, msg=_("Nothing changed"))
00679     else:
00680         upload_form(pagename, request, msg=_("Page '%(new_pagename)s' does not exist or you don't have enough rights.") % {
00681             'new_pagename': new_pagename})
00682 
00683 
00684 def _do_attachment_move(pagename, request):
00685     _ = request.getText
00686 
00687     if 'cancel' in request.form:
00688         return _('Move aborted!')
00689     if not wikiutil.checkTicket(request, request.form['ticket']):
00690         return _('Please use the interactive user interface to move attachments!')
00691     if not request.user.may.delete(pagename):
00692         return _('You are not allowed to move attachments from this page.')
00693 
00694     if 'newpagename' in request.form:
00695         new_pagename = request.form.get('newpagename')
00696     else:
00697         upload_form(pagename, request, msg=_("Move aborted because new page name is empty."))
00698     if 'newattachmentname' in request.form:
00699         new_attachment = request.form.get('newattachmentname')
00700         if new_attachment != wikiutil.taintfilename(new_attachment):
00701             upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
00702                                   'filename': new_attachment})
00703             return
00704     else:
00705         upload_form(pagename, request, msg=_("Move aborted because new attachment name is empty."))
00706 
00707     attachment = request.form.get('oldattachmentname')
00708     move_file(request, pagename, new_pagename, attachment, new_attachment)
00709 
00710 
00711 def _do_move(pagename, request):
00712     _ = request.getText
00713 
00714     pagename, filename, fpath = _access_file(pagename, request)
00715     if not request.user.may.delete(pagename):
00716         return _('You are not allowed to move attachments from this page.')
00717     if not filename:
00718         return # error msg already sent in _access_file
00719 
00720     # move file
00721     d = {'action': action_name,
00722          'url': request.href(pagename),
00723          'do': 'attachment_move',
00724          'ticket': wikiutil.createTicket(request),
00725          'pagename': wikiutil.escape(pagename, 1),
00726          'attachment_name': wikiutil.escape(filename, 1),
00727          'move': _('Move'),
00728          'cancel': _('Cancel'),
00729          'newname_label': _("New page name"),
00730          'attachment_label': _("New attachment name"),
00731         }
00732     formhtml = '''
00733 <form action="%(url)s" method="POST">
00734 <input type="hidden" name="action" value="%(action)s">
00735 <input type="hidden" name="do" value="%(do)s">
00736 <input type="hidden" name="ticket" value="%(ticket)s">
00737 <table>
00738     <tr>
00739         <td class="label"><label>%(newname_label)s</label></td>
00740         <td class="content">
00741             <input type="text" name="newpagename" value="%(pagename)s" size="80">
00742         </td>
00743     </tr>
00744     <tr>
00745         <td class="label"><label>%(attachment_label)s</label></td>
00746         <td class="content">
00747             <input type="text" name="newattachmentname" value="%(attachment_name)s" size="80">
00748         </td>
00749     </tr>
00750     <tr>
00751         <td></td>
00752         <td class="buttons">
00753             <input type="hidden" name="oldattachmentname" value="%(attachment_name)s">
00754             <input type="submit" name="move" value="%(move)s">
00755             <input type="submit" name="cancel" value="%(cancel)s">
00756         </td>
00757     </tr>
00758 </table>
00759 </form>''' % d
00760     thispage = Page(request, pagename)
00761     request.theme.add_msg(formhtml, "dialog")
00762     return thispage.send_page()
00763 
00764 
00765 def _do_box(pagename, request):
00766     _ = request.getText
00767 
00768     pagename, filename, fpath = _access_file(pagename, request)
00769     if not request.user.may.read(pagename):
00770         return _('You are not allowed to get attachments from this page.')
00771     if not filename:
00772         return # error msg already sent in _access_file
00773 
00774     timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
00775     if_modified = request.if_modified_since
00776     if if_modified and if_modified >= timestamp:
00777         request.status_code = 304
00778     else:
00779         ci = ContainerItem(request, pagename, filename)
00780         filename = wikiutil.taintfilename(request.values['member'])
00781         mt = wikiutil.MimeType(filename=filename)
00782         content_type = mt.content_type()
00783         mime_type = mt.mime_type()
00784 
00785         # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
00786         # There is no solution that is compatible to IE except stripping non-ascii chars
00787         filename_enc = filename.encode(config.charset)
00788 
00789         # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
00790         # we just let the user store them to disk ('attachment').
00791         # For safe files, we directly show them inline (this also works better for IE).
00792         dangerous = mime_type in request.cfg.mimetypes_xss_protect
00793         content_dispo = dangerous and 'attachment' or 'inline'
00794 
00795         now = time.time()
00796         request.headers.add('Date', http_date(now))
00797         request.headers.add('Content-Type', content_type)
00798         request.headers.add('Last-Modified', http_date(timestamp))
00799         request.headers.add('Expires', http_date(now - 365 * 24 * 3600))
00800         #request.headers.add('Content-Length', os.path.getsize(fpath))
00801         content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
00802         request.headers.add('Content-Disposition', content_dispo_string)
00803 
00804         # send data
00805         request.send_file(ci.get(filename))
00806 
00807 
00808 def _do_get(pagename, request):
00809     _ = request.getText
00810 
00811     pagename, filename, fpath = _access_file(pagename, request)
00812     if not request.user.may.read(pagename):
00813         return _('You are not allowed to get attachments from this page.')
00814     if not filename:
00815         return # error msg already sent in _access_file
00816 
00817     timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
00818     if_modified = request.if_modified_since
00819     if if_modified and if_modified >= timestamp:
00820         request.status_code = 304
00821     else:
00822         mt = wikiutil.MimeType(filename=filename)
00823         content_type = mt.content_type()
00824         mime_type = mt.mime_type()
00825 
00826         # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
00827         # There is no solution that is compatible to IE except stripping non-ascii chars
00828         filename_enc = filename.encode(config.charset)
00829 
00830         # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
00831         # we just let the user store them to disk ('attachment').
00832         # For safe files, we directly show them inline (this also works better for IE).
00833         dangerous = mime_type in request.cfg.mimetypes_xss_protect
00834         content_dispo = dangerous and 'attachment' or 'inline'
00835 
00836         now = time.time()
00837         request.headers.add('Date', http_date(now))
00838         request.headers.add('Content-Type', content_type)
00839         request.headers.add('Last-Modified', http_date(timestamp))
00840         request.headers.add('Expires', http_date(now - 365 * 24 * 3600))
00841         request.headers.add('Content-Length', os.path.getsize(fpath))
00842         content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
00843         request.headers.add('Content-Disposition', content_dispo_string)
00844 
00845         # send data
00846         request.send_file(open(fpath, 'rb'))
00847 
00848 
00849 def _do_install(pagename, request):
00850     _ = request.getText
00851 
00852     pagename, target, targetpath = _access_file(pagename, request)
00853     if not request.user.isSuperUser():
00854         return _('You are not allowed to install files.')
00855     if not target:
00856         return
00857 
00858     package = packages.ZipPackage(request, targetpath)
00859 
00860     if package.isPackage():
00861         if package.installPackage():
00862             msg = _("Attachment '%(filename)s' installed.") % {'filename': target}
00863         else:
00864             msg = _("Installation of '%(filename)s' failed.") % {'filename': target}
00865         if package.msg:
00866             msg += " " + package.msg
00867     else:
00868         msg = _('The file %s is not a MoinMoin package file.') % target
00869 
00870     upload_form(pagename, request, msg=msg)
00871 
00872 
00873 def _do_unzip(pagename, request, overwrite=False):
00874     _ = request.getText
00875     pagename, filename, fpath = _access_file(pagename, request)
00876 
00877     if not (request.user.may.delete(pagename) and request.user.may.read(pagename) and request.user.may.write(pagename)):
00878         return _('You are not allowed to unzip attachments of this page.')
00879 
00880     if not filename:
00881         return # error msg already sent in _access_file
00882 
00883     try:
00884         if not zipfile.is_zipfile(fpath):
00885             return _('The file %(filename)s is not a .zip file.') % {'filename': filename}
00886 
00887         # determine how which attachment names we have and how much space each is occupying
00888         curr_fsizes = dict([(f, size(request, pagename, f)) for f in _get_files(request, pagename)])
00889 
00890         # Checks for the existance of one common prefix path shared among
00891         # all files in the zip file. If this is the case, remove the common prefix.
00892         # We also prepare a dict of the new filenames->filesizes.
00893         zip_path_sep = '/'  # we assume '/' is as zip standard suggests
00894         fname_index = None
00895         mapping = []
00896         new_fsizes = {}
00897         zf = zipfile.ZipFile(fpath)
00898         for zi in zf.infolist():
00899             name = zi.filename
00900             if not name.endswith(zip_path_sep):  # a file (not a directory)
00901                 if fname_index is None:
00902                     fname_index = name.rfind(zip_path_sep) + 1
00903                     path = name[:fname_index]
00904                 if (name.rfind(zip_path_sep) + 1 != fname_index  # different prefix len
00905                     or
00906                     name[:fname_index] != path): # same len, but still different
00907                     mapping = []  # zip is not acceptable
00908                     break
00909                 if zi.file_size >= request.cfg.unzip_single_file_size:  # file too big
00910                     mapping = []  # zip is not acceptable
00911                     break
00912                 finalname = name[fname_index:]  # remove common path prefix
00913                 finalname = finalname.decode(config.charset, 'replace')  # replaces trash with \uFFFD char
00914                 mapping.append((name, finalname))
00915                 new_fsizes[finalname] = zi.file_size
00916 
00917         # now we either have an empty mapping (if the zip is not acceptable),
00918         # an identity mapping (no subdirs in zip, just all flat), or
00919         # a mapping (origname, finalname) where origname is the zip member filename
00920         # (including some prefix path) and finalname is a simple filename.
00921 
00922         # calculate resulting total file size / count after unzipping:
00923         if overwrite:
00924             curr_fsizes.update(new_fsizes)
00925             total = curr_fsizes
00926         else:
00927             new_fsizes.update(curr_fsizes)
00928             total = new_fsizes
00929         total_count = len(total)
00930         total_size = sum(total.values())
00931 
00932         if not mapping:
00933             msg = _("Attachment '%(filename)s' not unzipped because some files in the zip "
00934                     "are either not in the same directory or exceeded the single file size limit (%(maxsize_file)d kB)."
00935                    ) % {'filename': filename,
00936                         'maxsize_file': request.cfg.unzip_single_file_size / 1000, }
00937         elif total_size > request.cfg.unzip_attachments_space:
00938             msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
00939                     "the per page attachment storage size limit (%(size)d kB).") % {
00940                         'filename': filename,
00941                         'size': request.cfg.unzip_attachments_space / 1000, }
00942         elif total_count > request.cfg.unzip_attachments_count:
00943             msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
00944                     "the per page attachment count limit (%(count)d).") % {
00945                         'filename': filename,
00946                         'count': request.cfg.unzip_attachments_count, }
00947         else:
00948             not_overwritten = []
00949             for origname, finalname in mapping:
00950                 try:
00951                     # Note: reads complete zip member file into memory. ZipFile does not offer block-wise reading:
00952                     add_attachment(request, pagename, finalname, zf.read(origname), overwrite)
00953                 except AttachmentAlreadyExists:
00954                     not_overwritten.append(finalname)
00955             if not_overwritten:
00956                 msg = _("Attachment '%(filename)s' partially unzipped (did not overwrite: %(filelist)s).") % {
00957                         'filename': filename,
00958                         'filelist': ', '.join(not_overwritten), }
00959             else:
00960                 msg = _("Attachment '%(filename)s' unzipped.") % {'filename': filename}
00961     except RuntimeError, err:
00962         # We don't want to crash with a traceback here (an exception
00963         # here could be caused by an uploaded defective zip file - and
00964         # if we crash here, the user does not get a UI to remove the
00965         # defective zip file again).
00966         # RuntimeError is raised by zipfile stdlib module in case of
00967         # problems (like inconsistent slash and backslash usage in the
00968         # archive).
00969         logging.exception("An exception within zip file attachment handling occurred:")
00970         msg = _("A severe error occurred:") + ' ' + str(err)
00971 
00972     upload_form(pagename, request, msg=msg)
00973 
00974 
00975 def send_viewfile(pagename, request):
00976     _ = request.getText
00977     fmt = request.html_formatter
00978 
00979     pagename, filename, fpath = _access_file(pagename, request)
00980     if not filename:
00981         return
00982 
00983     request.write('<h2>' + _("Attachment '%(filename)s'") % {'filename': filename} + '</h2>')
00984     # show a download link above the content
00985     label = _('Download')
00986     link = (fmt.url(1, getAttachUrl(pagename, filename, request, do='get'), css_class="download") +
00987             fmt.text(label) +
00988             fmt.url(0))
00989     request.write('%s<br><br>' % link)
00990 
00991     if filename.endswith('.tdraw') or filename.endswith('.adraw'):
00992         request.write(fmt.attachment_drawing(filename, ''))
00993         return
00994 
00995     mt = wikiutil.MimeType(filename=filename)
00996 
00997     # destinguishs if browser need a plugin in place
00998     if mt.major == 'image' and mt.minor in config.browser_supported_images:
00999         url = getAttachUrl(pagename, filename, request)
01000         request.write('<img src="%s" alt="%s">' % (
01001             wikiutil.escape(url, 1),
01002             wikiutil.escape(filename, 1)))
01003         return
01004     elif mt.major == 'text':
01005         ext = os.path.splitext(filename)[1]
01006         Parser = wikiutil.getParserForExtension(request.cfg, ext)
01007         if Parser is not None:
01008             try:
01009                 content = file(fpath, 'r').read()
01010                 content = wikiutil.decodeUnknownInput(content)
01011                 colorizer = Parser(content, request, filename=filename)
01012                 colorizer.format(request.formatter)
01013                 return
01014             except IOError:
01015                 pass
01016 
01017         request.write(request.formatter.preformatted(1))
01018         # If we have text but no colorizing parser we try to decode file contents.
01019         content = open(fpath, 'r').read()
01020         content = wikiutil.decodeUnknownInput(content)
01021         content = wikiutil.escape(content)
01022         request.write(request.formatter.text(content))
01023         request.write(request.formatter.preformatted(0))
01024         return
01025 
01026     try:
01027         package = packages.ZipPackage(request, fpath)
01028         if package.isPackage():
01029             request.write("<pre><b>%s</b>\n%s</pre>" % (_("Package script:"), wikiutil.escape(package.getScript())))
01030             return
01031 
01032         if zipfile.is_zipfile(fpath) and mt.minor == 'zip':
01033             zf = zipfile.ZipFile(fpath, mode='r')
01034             request.write("<pre>%-46s %19s %12s\n" % (_("File Name"), _("Modified")+" "*5, _("Size")))
01035             for zinfo in zf.filelist:
01036                 date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time
01037                 request.write(wikiutil.escape("%-46s %s %12d\n" % (zinfo.filename, date, zinfo.file_size)))
01038             request.write("</pre>")
01039             return
01040     except RuntimeError:
01041         # We don't want to crash with a traceback here (an exception
01042         # here could be caused by an uploaded defective zip file - and
01043         # if we crash here, the user does not get a UI to remove the
01044         # defective zip file again).
01045         # RuntimeError is raised by zipfile stdlib module in case of
01046         # problems (like inconsistent slash and backslash usage in the
01047         # archive).
01048         logging.exception("An exception within zip file attachment handling occurred:")
01049         return
01050 
01051     from MoinMoin import macro
01052     from MoinMoin.parser.text import Parser
01053 
01054     macro.request = request
01055     macro.formatter = request.html_formatter
01056     p = Parser("##\n", request)
01057     m = macro.Macro(p)
01058 
01059     # use EmbedObject to view valid mime types
01060     if mt is None:
01061         request.write('<p>' + _("Unknown file type, cannot display this attachment inline.") + '</p>')
01062         link = (fmt.url(1, getAttachUrl(pagename, filename, request)) +
01063                 fmt.text(filename) +
01064                 fmt.url(0))
01065         request.write('For using an external program follow this link %s' % link)
01066         return
01067     request.write(m.execute('EmbedObject', u'target="%s", pagename="%s"' % (filename, pagename)))
01068     return
01069 
01070 
01071 def _do_view(pagename, request):
01072     _ = request.getText
01073 
01074     orig_pagename = pagename
01075     pagename, filename, fpath = _access_file(pagename, request)
01076     if not request.user.may.read(pagename):
01077         return _('You are not allowed to view attachments of this page.')
01078     if not filename:
01079         return
01080 
01081     request.formatter.page = Page(request, pagename)
01082 
01083     # send header & title
01084     # Use user interface language for this generated page
01085     request.setContentLanguage(request.lang)
01086     title = _('attachment:%(filename)s of %(pagename)s') % {
01087         'filename': filename, 'pagename': pagename}
01088     request.theme.send_title(title, pagename=pagename)
01089 
01090     # send body
01091     request.write(request.formatter.startContent())
01092     send_viewfile(orig_pagename, request)
01093     send_uploadform(pagename, request)
01094     request.write(request.formatter.endContent())
01095 
01096     request.theme.send_footer(pagename)
01097     request.theme.send_closing_html()
01098 
01099 
01100 #############################################################################
01101 ### File attachment administration
01102 #############################################################################
01103 
01104 def do_admin_browser(request):
01105     """ Browser for SystemAdmin macro. """
01106     from MoinMoin.util.dataset import TupleDataset, Column
01107     _ = request.getText
01108 
01109     data = TupleDataset()
01110     data.columns = [
01111         Column('page', label=('Page')),
01112         Column('file', label=('Filename')),
01113         Column('size', label=_('Size'), align='right'),
01114     ]
01115 
01116     # iterate over pages that might have attachments
01117     pages = request.rootpage.getPageList()
01118     for pagename in pages:
01119         # check for attachments directory
01120         page_dir = getAttachDir(request, pagename)
01121         if os.path.isdir(page_dir):
01122             # iterate over files of the page
01123             files = os.listdir(page_dir)
01124             for filename in files:
01125                 filepath = os.path.join(page_dir, filename)
01126                 data.addRow((
01127                     Page(request, pagename).link_to(request, querystr="action=AttachFile"),
01128                     wikiutil.escape(filename.decode(config.charset)),
01129                     os.path.getsize(filepath),
01130                 ))
01131 
01132     if data:
01133         from MoinMoin.widget.browser import DataBrowserWidget
01134 
01135         browser = DataBrowserWidget(request)
01136         browser.setData(data)
01137         return browser.render(method="GET")
01138 
01139     return ''
01140