Thomas Waldmann committed 76a4e3c

add +download view for just downloading a revision with forced content-disposition: attachment

minor code cleanups, removed MimeType.content_disposition as it did not match send_file api,
replaced it by a as_attachment method.

removed duplicate get_item list entry.

removed unused content_disposition argument from wikiutil.file_headers.

Comments (0)

Files changed (6)


 Crawl-delay: 20
 Disallow: /+convert/
 Disallow: /+dom/
+Disallow: /+download/
 Disallow: /+modify/
 Disallow: /+copy/
 Disallow: /+delete/
     return item.do_get()
+@frontend.route('/+download/<itemname:item_name>', defaults=dict(rev=-1))
+def download_item(item_name, rev):
+    try:
+        item = Item.create(item_name, rev_no=rev)
+    except AccessDeniedError:
+        abort(403)
+    return item.do_get(force_attachment=True)
 def convert_item(item_name):


     ('item_views', [
         # (endpointname, label, check_item_exists
         ('frontend.show_item', L_('Show'), L_('Show'), False, ),
-        ('frontend.get_item', L_('Download'), L_('Download'), True, ),
+        ('frontend.download_item', L_('Download'), L_('Download'), True, ),
         ('frontend.history', L_('History'), L_('Revision History'), True, ),
         # note: when rendering a non-existing item, you'll be offered to
         # create it (in the content area), so we do not offer "Modify":


-    def do_get(self):
+    def do_get(self, force_attachment=False):
     def _convert(self):
         return _("Impossible to convert the data to the contenttype: %(contenttype)s",
-    def do_get(self):
+    def do_get(self, force_attachment=False):
         hash = self.rev.get(HASH_ALGORITHM)
         if is_resource_modified(request.environ, hash): # use hash as etag
-            return self._do_get_modified(hash)
+            return self._do_get_modified(hash, force_attachment=force_attachment)
             return Response(status=304)
-    def _do_get_modified(self, hash):
+    def _do_get_modified(self, hash, force_attachment=False):
         member = request.values.get('member')
-        return self._do_get(hash, member)
+        return self._do_get(hash, member, force_attachment=force_attachment)
-    def _do_get(self, hash, member=None):
-        filename = None
+    def _do_get(self, hash, member=None, force_attachment=False):
         if member: # content = file contained within a archive item revision
             path, filename = os.path.split(member)
             mt = MimeType(filename=filename)
-            content_disposition = mt.content_disposition(app.cfg)
-            content_type = mt.content_type()
             content_length = None
             file_to_send = self.get_member(member)
         else: # content = item revision
             rev = self.rev
+            filename =
                 mimestr = rev[CONTENTTYPE]
             except KeyError:
-                mt = MimeType(
+                mt = MimeType(filename=filename)
                 mt = MimeType(mimestr=mimestr)
-            content_disposition = mt.content_disposition(app.cfg)
-            content_type = mt.content_type()
             content_length = rev[SIZE]
             file_to_send = rev
-        # TODO: handle content_disposition is not None
-        # Important: empty filename keeps flask from trying to autodetect filename,
-        # as this would not work for us, because our file's are not necessarily fs files.
+        content_type = mt.content_type()
+        as_attachment = force_attachment or mt.as_attachment(app.cfg)
         return send_file(file=file_to_send,
-                         as_attachment=False, attachment_filename=filename,
+                         as_attachment=as_attachment, attachment_filename=filename,
                          cache_timeout=10, # wiki data can change rapidly
                          add_etags=True, etag=hash, conditional=True)
         return content_type, data
-    def _do_get_modified(self, hash):
+    def _do_get_modified(self, hash, force_attachment=False):
             width = int(request.values.get('w'))
         except (TypeError, ValueError):
                 headers, data = c
             return Response(data, headers=headers)
-            return self._do_get(hash)
+            return self._do_get(hash, force_attachment=force_attachment)
     def _render_data_diff(self, oldrev, newrev):
         if PIL is None:
         # We determine the different parameters for the reply
         mt = MimeType(mimestr='application/docbook+xml;charset=utf-8')
-        content_disposition = mt.content_disposition(app.cfg)
         content_type = mt.content_type()
+        as_attachment = mt.as_attachment(app.cfg)
         # After creation of the StringIO, we are at the end of the file
         # so position is the size the file.
         # and then we should move it back at the beginning of the file
         # as this would not work for us, because our file's are not necessarily fs files.
         return send_file(file=file_to_send,
-                         as_attachment=False, attachment_filename=None,
+                         as_attachment=as_attachment, attachment_filename=None,
                          cache_timeout=10, # wiki data can change rapidly
                          add_etags=False, etag=None, conditional=True)


     {% for endpoint, label, title, check_exists in cfg.item_views if not endpoint in cfg.endpoints_excluded %}
         {% if (not check_exists or check_exists and exists) and endpoint in [
                'frontend.show_item', 'frontend.index', 'frontend.index2',
-               'frontend.highlight_item', 'frontend.show_item_meta', 'frontend.get_item',
-               'frontend.get_item', 'frontend.history', 'frontend.backrefs', 'frontend.sitemap',
+               'frontend.highlight_item', 'frontend.show_item_meta', 'frontend.download_item',
+               'frontend.history', 'frontend.backrefs', 'frontend.sitemap',
                'frontend.copy_item', 'frontend.rename_item', 'frontend.delete_item', 'frontend.destroy_item',


         """ return a string major/minor only, no params """
         return "%s/%s" % (self.major, self.minor)
-    def content_disposition(self, cfg):
+    def as_attachment(self, cfg):
         # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
         # we just let the user store them to disk ('attachment').
         # For safe files, we directly show them inline (this also works better for IE).
         mime_type = self.mime_type()
-        dangerous = mime_type in cfg.mimetypes_xss_protect
-        content_disposition = dangerous and 'attachment' or 'inline'
-        filename = self.filename
-        if filename is not None:
-            # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
-            # There is no solution that is compatible to IE except stripping non-ascii chars
-            if isinstance(filename, unicode):
-                filename = filename.encode(config.charset)
-            content_disposition += '; filename="%s"' % filename
-        return content_disposition
+        return mime_type in cfg.mimetypes_xss_protect
     def module_name(self):
         """ convert this mimetype to a string useable as python module name,


-def file_headers(filename=None,
-                 content_type=None, content_length=None, content_disposition=None):
+def file_headers(filename=None, content_type=None, content_length=None):
         Compute http headers for sending a file
-        :param filename: filename for content-disposition header and for autodetecting
-                         content_type (unicode, default: None)
+        :param filename: filename for autodetecting content_type (unicode, default: None)
         :param content_type: content-type header value (str, default: autodetect from filename)
-        :param content_disposition: type for content-disposition header (str, default: None)
         :param content_length: for content-length header (int, default:None)
         if filename:
         headers = [('Content-Type', content_type)]
         if content_length is not None:
             headers.append(('Content-Length', str(content_length)))
-        if content_disposition is None and mt is not None:
-            content_disposition = mt.content_disposition(app.cfg)
-        if content_disposition:
-            headers.append(('Content-Disposition', content_disposition))
         return headers