# -*- coding: iso-8859-1 -*- """ MoinMoin - AttachFile action This action lets a page have multiple attachment files. It creates a folder /pages//attachments and keeps everything in there. Form values: action=Attachment 1. with no 'do' key: returns file upload form 2. do=attach: accept file upload and saves the file in ../attachment/pagename/ 3. /pagename/fname?action=Attachment&do=get[&mimetype=type]: return contents of the attachment file with the name fname. 4. /pataname/fname, do=view[&mimetype=type]:create a page to view the content of the file To insert an attachment into the page, use the "attachment:" pseudo schema. @copyright: 2001 by Ken Sugino (sugino@mediaone.net) @copyright: 2001-2004 by Jürgen Hermann @license: GNU GPL, see COPYING for details. """ import os, mimetypes, time, urllib from MoinMoin import config, user, util, wikiutil, re from MoinMoin.Page import Page from MoinMoin.util import MoinMoinNoFooter, filesys, web action_name = __name__.split('.')[-1] def htdocs_access(request): return isinstance(request.cfg.attachments, type({})) ############################################################################# ### External interface - these are called from the core code ############################################################################# def getBasePath(request): """ Get base path where page dirs for attachments are stored. """ if htdocs_access(request): return request.cfg.attachments['dir'] else: return request.rootpage.getPagePath('pages') def getAttachDir(request, pagename, create=0): """ Get directory where attachments for page `pagename` are stored. """ if htdocs_access(request): # direct file access via webserver, from public htdocs area pagename = wikiutil.quoteWikinameFS(pagename) attach_dir = os.path.join(request.cfg.attachments['dir'], pagename, "attachments") if create and not os.path.isdir(attach_dir): filesys.makeDirs(attach_dir) else: # send file via CGI, from page storage area attach_dir = Page(request, pagename).getPagePath("attachments", check_create=create) return attach_dir def getAttachUrl(pagename, filename, request, addts=0, escaped=0): """ Get URL that points to attachment `filename` of page `pagename`. If 'addts' is true, a timestamp with the file's modification time is added, so that browsers reload a changed file. """ if htdocs_access(request): # direct file access via webserver timestamp = '' if addts: try: timestamp = '?ts=%s' % os.path.getmtime( getFilename(request, pagename, filename)) except IOError: pass url = "%s/%s/attachments/%s%s" % ( request.cfg.attachments['url'], wikiutil.quoteWikinameFS(pagename), urllib.quote(filename.encode(config.charset)), timestamp) else: # send file via CGI url = "%s/%s?action=%s&do=get&target=%s" % ( request.getScriptname(), wikiutil.quoteWikinameURL(pagename), action_name, urllib.quote_plus(filename.encode(config.charset))) if escaped: url = wikiutil.escape(url) return url def getIndicator(request, pagename): """ Get an attachment indicator for a page (linked clip image) or an empty string if not attachments exist. """ _ = request.getText attach_dir = getAttachDir(request, pagename) if not os.path.exists(attach_dir): return '' files = os.listdir(attach_dir) if not files: return '' attach_count = _('[%d attachments]') % len(files) attach_icon = request.theme.make_icon('attach', vars={ 'attach_count': attach_count }) attach_link = wikiutil.link_tag(request, "%s?action=AttachFile" % wikiutil.quoteWikinameURL(pagename), attach_icon) return attach_link def getFilename(request, pagename, name): return os.path.join(getAttachDir(request, pagename), name).encode(config.charset) def info(pagename, request): """ Generate snippet with info on the attachment for page `pagename`. """ _ = request.getText attach_dir = getAttachDir(request, pagename) files = [] if os.path.isdir(attach_dir): files = os.listdir(attach_dir) page = Page(request, pagename) # TODO: remove escape=0 in 1.4 link = page.url(request, {'action': 'AttachFile'}, escape=0) attach_info = _('There are %(count)s attachment(s) stored for this page.', formatted=False) % { 'count': len(files), 'link': wikiutil.escape(link) } return "\n

\n%s\n

\n" % attach_info ############################################################################# ### Internal helpers ############################################################################# def _addLogEntry(request, action, pagename, filename): """ Add an entry to the edit log on uploads and deletes. `action` should be "ATTNEW" or "ATTDEL" """ from MoinMoin.logfile import editlog t = wikiutil.timestamp2version(time.time()) # urllib always return ascii fname = unicode(urllib.quote(filename.encode(config.charset))) # TODO: for now we simply write 2 logs, maybe better use some multilog stuff # Write to global log log = editlog.EditLog(request) log.add(request, t, 99999999, action, pagename, request.remote_addr, fname) # Write to local log log = editlog.EditLog(request, rootpagename=pagename) log.add(request, t, 99999999, action, pagename, request.remote_addr, fname) def _access_file(pagename, request): """ Check form parameter `target` and return a tuple of `(filename, filepath)` for an existing attachment. Return `(None, None)` if an error occurs. """ _ = request.getText error = None if not request.form.get('target', [''])[0]: error = _("Filename of attachment not specified!") else: filename = wikiutil.taintfilename(request.form['target'][0]) fpath = getFilename(request, pagename, filename) if os.path.isfile(fpath): return (filename, fpath) error = _("Attachment '%(filename)s' does not exist!") % {'filename': filename} error_msg(pagename, request, error) return (None, None) def _backup_file(fpath): """Move the file `fpath` to `fpath,nnn` where `nnn` is one plus `mmm`, where `mmm` is the highest number for which `fpath.mmm` exists, or 000 if no such file exists.""" # find the right sequence number dirname, basename = os.path.split(fpath) backup_re = re.compile(r'^%s\,(?P\d\d\d)$' % basename) backups = filter(backup_re.match, os.listdir(dirname)) backups.sort() if backups == []: seq_no = 0 else: highest = backups[-1] seq_no = int(backup_re.match(highest).group('seq')) + 1 # do the move new_name = "%s,%03d" % (fpath, seq_no) os.rename(fpath, new_name) def _build_filelist(request, pagename, showheader, readonly): _ = request.getText # access directory attach_dir = getAttachDir(request, pagename) files = _get_files(request, pagename) str = "" if files: if showheader: str = str + _( "To refer to attachments on a page, use '''{{{attachment:filename}}}''', \n" "as shown below in the list of files. \n" "Do '''NOT''' use the URL of the {{{[get]}}} link, \n" "since this is subject to change and can break easily." ) str = str + "
    " label_del = _("del") label_get = _("get") label_edit = _("edit") label_view = _("view") backup_re = re.compile(r'.*\,\d\d$') files = filter(lambda x: not backup_re.match(x), files) for file in files: fsize = float(os.stat(os.path.join(attach_dir,file).encode(config.charset))[6]) # in byte fsize = "%.1f" % (fsize / 1024) baseurl = request.getScriptname() action = action_name urlpagename = wikiutil.quoteWikinameURL(pagename) urlfile = urllib.quote_plus(file.encode(config.charset)) base, ext = os.path.splitext(file) get_url = getAttachUrl(pagename, file, request, escaped=1) parmdict = {'baseurl': baseurl, 'urlpagename': urlpagename, 'action': action, 'urlfile': urlfile, 'label_del': label_del, 'base': base, 'label_edit': label_edit, 'label_view': label_view, 'get_url': get_url, 'label_get': label_get, 'file': file, 'fsize': fsize, 'pagename': pagename} del_link = '' if request.user.may.delete(pagename) and not readonly: del_link = '%(label_del)s | ' % parmdict if ext == '.draw': viewlink = '%(label_edit)s' % parmdict else: viewlink = '%(label_view)s' % parmdict parmdict['viewlink'] = viewlink parmdict['del_link'] = del_link str = str + ('
  • [%(del_link)s' '%(label_get)s | %(viewlink)s]' ' (%(fsize)s KB) attachment:%(file)s
  • ') % parmdict str = str + "
" else: if showheader: str = '%s

%s

' % (str, _("No attachments stored for %(pagename)s") % {'pagename': pagename}) return str def _get_files(request, pagename): attach_dir = getAttachDir(request, pagename) if os.path.isdir(attach_dir): files = map(lambda a: a.decode(config.charset), os.listdir(attach_dir)) files.sort() return files return [] def _get_filelist(request, pagename): return _build_filelist(request, pagename, 1, 0) def error_msg(pagename, request, msg): Page(request, pagename).send_page(request, msg=msg) ############################################################################# ### Create parts of the Web interface ############################################################################# def send_link_rel(request, pagename): files = _get_files(request, pagename) for file in files: get_url = getAttachUrl(pagename, file, request, escaped=1) request.write(u'\n' % ( wikiutil.escape(file), get_url)) def send_hotdraw(pagename, request): _ = request.getText now = time.time() pubpath = request.cfg.url_prefix + "/applets/TWikiDrawPlugin" basename = request.form['drawing'][0] drawpath = getAttachUrl(pagename, request.form['drawing'][0] + '.draw', request, escaped=1) pngpath = getAttachUrl(pagename, request.form['drawing'][0] + '.png', request, escaped=1) querystr = {'action': 'AttachFile', 'ts': now} querystr = wikiutil.escape(web.makeQueryString(querystr)) pagelink = '%s?%s' % (wikiutil.quoteWikinameURL(pagename), querystr) savelink = Page(request, pagename).url(request) # XXX include target filename param here for twisted # request, {'savename': request.form['drawing'][0]+'.draw'} #savelink = '/cgi-bin/dumpform.bat' if htdocs_access(request): timestamp = '?ts=%s' % now else: timestamp = '&ts=%s' % now request.write('

' + _("Edit drawing") + '

') request.write("""

NOTE: You need a Java enabled browser to edit the drawing example.

""" % { 'pngpath': pngpath, 'timestamp': timestamp, 'pubpath': pubpath, 'drawpath': drawpath, 'savelink': savelink, 'pagelink': pagelink, 'basename': basename }) def send_uploadform(pagename, request): """ Send the HTML code for the list of already stored attachments and the file upload form. """ _ = request.getText if not request.user.may.read(pagename): request.write('

%s

' % _('You are not allowed to view this page.')) return request.write('

' + _("Attached Files") + '

') request.write(_get_filelist(request, pagename)) if not request.user.may.write(pagename): request.write('

%s

' % _('You are not allowed to attach a file to this page.')) return if request.form.get('drawing', [None])[0]: send_hotdraw(pagename, request) return request.write('

' + _("New Attachment") + '

' + _("""

When uploading under a filename for which an attachment already exists, the old version is stored - ask the administrator to recover it.

If "Rename to" is left blank, the original filename will be used.

""") request.write("""
%(upload_label_file)s
%(upload_label_mime)s
%(upload_label_rename)s

""" % { 'baseurl': request.getScriptname(), 'pagename': wikiutil.quoteWikinameURL(pagename), 'action_name': action_name, 'upload_label_file': _('File to upload'), 'upload_label_mime': _('MIME Type (optional)'), 'upload_label_rename': _('Save as'), 'rename': request.form.get('rename', [''])[0], 'upload_button': _('Upload'), }) ############################################################################# ### Web interface for file upload, viewing and deletion ############################################################################# def execute(pagename, request): """ Main dispatcher for the 'AttachFile' action. """ _ = request.getText msg = None if action_name in request.cfg.excluded_actions: msg = _('File attachments are not allowed in this wiki!') elif request.form.has_key('filepath'): if request.user.may.write(pagename): save_drawing(pagename, request) request.http_headers() request.write("OK") else: msg = _('You are not allowed to save a drawing on this page.') elif not request.form.has_key('do'): upload_form(pagename, request) elif request.form['do'][0] == 'upload': if request.user.may.write(pagename): if request.form.has_key('file'): do_upload(pagename, request) else: # This might happen when trying to upload file names # with non-ascii characters on Safari. msg = _("No file content. Delete non ASCII characters from the file name and try again.") else: msg = _('You are not allowed to attach a file to this page.') elif request.form['do'][0] == 'del': if request.user.may.delete(pagename): del_file(pagename, request) else: msg = _('You are not allowed to delete attachments on this page.') elif request.form['do'][0] == 'get': if request.user.may.read(pagename): get_file(pagename, request) else: msg = _('You are not allowed to get attachments from this page.') elif request.form['do'][0] == 'view': if request.user.may.read(pagename): view_file(pagename, request) else: msg = _('You are not allowed to view attachments of this page.') else: msg = _('Unsupported upload action: %s') % (request.form['do'][0],) if msg: error_msg(pagename, request, msg) def upload_form(pagename, request, msg=''): _ = request.getText request.http_headers() # Use user interface language for this generated page request.setContentLanguage(request.lang) wikiutil.send_title(request, _('Attachments for "%(pagename)s"') % {'pagename': pagename}, pagename=pagename, msg=msg) request.write('
\n') # start content div send_uploadform(pagename, request) request.write('
\n') # end content div wikiutil.send_footer(request, pagename, showpage=1) def do_upload(pagename, request): _ = request.getText # make filename filename = None if request.form.has_key('file__filename__'): filename = request.form['file__filename__'] rename = None if request.form.has_key('rename'): rename = request.form['rename'][0].strip() # if we use twisted, "rename" field is NOT optional, because we # can't access the client filename if rename: filename = target = rename elif filename: target = filename else: error_msg(pagename, request, _("Filename of attachment not specified!")) return # get file content filecontent = request.form['file'][0] target = wikiutil.taintfilename(target) # set mimetype from extension, or from given mimetype type, encoding = mimetypes.guess_type(target) if not type: ext = None if request.form.has_key('mime'): ext = mimetypes.guess_extension(request.form['mime'][0]) if not ext: type, encoding = mimetypes.guess_type(filename) if type: ext = mimetypes.guess_extension(type) else: ext = '' target = target + ext # get directory, and possibly create it attach_dir = getAttachDir(request, pagename, create=1) # save file fpath = os.path.join(attach_dir, target).encode(config.charset) if os.path.exists(fpath): _backup_file(fpath) content = fileitem.file.read() stream = open(fpath, 'wb') try: stream.write(content) finally: stream.close() os.chmod(fpath, 0666 & config.umask) bytes = len(content) msg = _("Attachment '%(target)s' (remote name '%(filename)s')" " with %(bytes)d bytes saved.") % locals() _addLogEntry(request, 'ATTNEW', pagename, target) # return attachment list upload_form(pagename, request, msg) def save_drawing(pagename, request): filename = request.form['filename'][0] filecontent = request.form['filepath'][0] # there should be no difference in filename parsing with or without # htdocs_access, cause the filename param is used basepath, basename = os.path.split(filename) basename, ext = os.path.splitext(basename) # get directory, and possibly create it attach_dir = getAttachDir(request, pagename, create=1) if ext == '.draw': _addLogEntry(request, 'ATTDRW', pagename, basename + ext) savepath = os.path.join(getAttachDir(request, pagename), basename + ext) if ext == '.map' and filecontent.strip()=='': # delete map file if it is empty os.unlink(savepath) else: file = open(savepath, 'wb') try: file.write(filecontent) finally: file.close() # touch attachment directory to invalidate cache if new map is saved if ext == '.map': os.utime(getAttachDir(request, pagename), None) def del_file(pagename, request): _ = request.getText filename, fpath = _access_file(pagename, request) if not filename: return # error msg already sent in _access_file # delete file if os.path.exists(fpath): _backup_file(fpath) _addLogEntry(request, 'ATTDEL', pagename, filename) upload_form(pagename, request, msg=_("Attachment '%(filename)s' deleted.") % {'filename': filename}) def get_file(pagename, request): import shutil filename, fpath = _access_file(pagename, request) if not filename: return # error msg already sent in _access_file # get mimetype type, enc = mimetypes.guess_type(filename) if not type: type = "application/octet-stream" # send header request.http_headers([ "Content-Type: %s" % type, "Content-Length: %d" % os.path.getsize(fpath), "Content-Disposition: inline; filename=\"%s\"" % filename.encode(config.charset), ]) # send data shutil.copyfileobj(open(fpath, 'rb'), request, 8192) raise MoinMoinNoFooter def send_viewfile(pagename, request): _ = request.getText filename, fpath = _access_file(pagename, request) if not filename: return request.write('

' + _("Attachment '%(filename)s'") % {'filename': filename} + '

') type, enc = mimetypes.guess_type(filename) if type: if type[:5] == 'image': timestamp = htdocs_access(request) and "?%s" % time.time() or '' request.write('%s' % ( getAttachUrl(pagename, filename, request, escaped=1), timestamp, wikiutil.escape(filename, 1))) return elif type[:4] == 'text': # TODO: should use formatter here! request.write("
")
            # Try to decode file contents. It may return junk, but we
            # don't have enough information on attachments.
            content = open(fpath, 'r').read()
            content = wikiutil.decodeUnknownInput(content)
            content = wikiutil.escape(content)
            request.write(content)
            request.write("
") return request.write('

' + _("Unknown file type, cannot display this attachment inline.") + '

') request.write('%s' % ( getAttachUrl(pagename, filename, request, escaped=1), wikiutil.escape(filename))) def view_file(pagename, request): _ = request.getText filename, fpath = _access_file(pagename, request) if not filename: return # send header & title request.http_headers() # Use user interface language for this generated page request.setContentLanguage(request.lang) title = _('attachment:%(filename)s of %(pagename)s', formatted=True) % { 'filename': filename, 'pagename': pagename} wikiutil.send_title(request, title, pagename=pagename) # send body # TODO: use formatter startContent? request.write('
\n') # start content div send_viewfile(pagename, request) send_uploadform(pagename, request) request.write('
\n') # end content div # send footer wikiutil.send_footer(request, pagename) ############################################################################# ### File attachment administration ############################################################################# def do_admin_browser(request): """ Browser for SystemAdmin macro. """ from MoinMoin.util.dataset import TupleDataset, Column _ = request.getText data = TupleDataset() data.columns = [ Column('page', label=('Page')), Column('file', label=('Filename')), Column('size', label=_('Size'), align='right'), #Column('action', label=_('Action')), ] # iterate over pages that might have attachments pages = request.rootpage.getPageList() for pagename in pages: # check for attachments directory page_dir = getAttachDir(request, pagename) if os.path.isdir(page_dir): # iterate over files of the page files = os.listdir(page_dir) for filename in files: filepath = os.path.join(page_dir, filename) data.addRow(( Page(request, pagename).link_to(request, querystr="action=AttachFile"), wikiutil.escape(filename), os.path.getsize(filepath), # '', )) if data: from MoinMoin.widget.browser import DataBrowserWidget browser = DataBrowserWidget(request) browser.setData(data) return browser.toHTML() return ''