diff --git a/CHANGES.rst b/CHANGES.rst index 820b874..5ebfd19 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,11 @@ Changelog 1.0.12 (unreleased) ------------------- +- Add Base64 data converter for NamedImage and NamedFile widgets on ASCII + fields with base64 encoded data and filename. Now the NamedImage and + NamedFile widgets can be used with ``zope.schema.ASCII`` fields. + [thet] + - PEP 8. [thet] diff --git a/plone/formwidget/namedfile/configure.zcml b/plone/formwidget/namedfile/configure.zcml index de01907..bce3532 100644 --- a/plone/formwidget/namedfile/configure.zcml +++ b/plone/formwidget/namedfile/configure.zcml @@ -12,6 +12,7 @@ + diff --git a/plone/formwidget/namedfile/converter.py b/plone/formwidget/namedfile/converter.py index 25a77ca..71395ca 100644 --- a/plone/formwidget/namedfile/converter.py +++ b/plone/formwidget/namedfile/converter.py @@ -1,9 +1,15 @@ from ZPublisher.HTTPRequest import FileUpload from plone.formwidget.namedfile.interfaces import INamedFileWidget -from plone.namedfile.interfaces import INamedField, INamed +from plone.formwidget.namedfile.interfaces import INamedImageWidget +from plone.namedfile.file import NamedFile +from plone.namedfile.file import NamedImage +from plone.namedfile.interfaces import INamed +from plone.namedfile.interfaces import INamedField from plone.namedfile.utils import safe_basename from z3c.form.converter import BaseDataConverter from zope.component import adapts +from zope.schema.interfaces import IASCII +import base64 class NamedDataConverter(BaseDataConverter): @@ -39,3 +45,67 @@ def toFieldValue(self, value): else: return self.field._type(data=str(value)) + + +def b64encode_file(filename, data): + # encode filename and data using the standard alphabet, so that ";" can be + # used as delimiter. + if isinstance(filename, unicode): + filename = filename.encode('utf-8') + filenameb64 = base64.standard_b64encode(filename or '') + datab64 = base64.standard_b64encode(data) + return "filenameb64:{};datab64:{}".format( + filenameb64, datab64 + ).encode('ascii') + + +def b64decode_file(value): + filename, data = value.split(';') + + filename = filename.split(':')[1] + filename = base64.standard_b64decode(filename) + filename = filename.decode('utf-8') + + data = data.split(':')[1] + data = base64.standard_b64decode(data) + + return filename, data + + +class Base64Converter(BaseDataConverter): + """Converts between ASCII fields with base64 encoded data and a filename + and INamedImage/INamedFile values. + """ + adapts(IASCII, INamedFileWidget) + + def toWidgetValue(self, value): + + if not isinstance(value, basestring): + return None + + filename, data = b64decode_file(value) + + if INamedImageWidget.providedBy(self.widget): + value = NamedImage(data=data, filename=filename) + else: + value = NamedFile(data=data, filename=filename) + return value + + def toFieldValue(self, value): + + filename = None + data = None + + if INamed.providedBy(value): + filename = value.filename + data = value.data + + elif isinstance(value, FileUpload): + filename = safe_basename(value.filename) + value.seek(0) + data = value.read() + + if not data: + return self.field.missing_value + + return b64encode_file(filename, data) diff --git a/plone/formwidget/namedfile/widget.py b/plone/formwidget/namedfile/widget.py index 204bf07..05854bb 100644 --- a/plone/formwidget/namedfile/widget.py +++ b/plone/formwidget/namedfile/widget.py @@ -4,8 +4,11 @@ from Products.Five.browser import BrowserView from Products.MimetypesRegistry.common import MimeTypeException from ZPublisher.HTTPRequest import FileUpload +from plone.formwidget.namedfile.converter import b64decode_file from plone.formwidget.namedfile.interfaces import INamedFileWidget from plone.formwidget.namedfile.interfaces import INamedImageWidget +from plone.namedfile.file import NamedFile +from plone.namedfile.file import NamedImage from plone.namedfile.interfaces import INamed from plone.namedfile.interfaces import INamedFileField from plone.namedfile.interfaces import INamedImage @@ -27,6 +30,7 @@ from zope.interface import implementsOnly from zope.publisher.interfaces import IPublishTraverse from zope.publisher.interfaces import NotFound +from zope.schema.interfaces import IASCII from zope.size import byteDisplay import urllib @@ -231,7 +235,6 @@ def publishTraverse(self, request, name): return self def __call__(self): - # TODO: Security check on form view/widget if self.context.ignoreContext: @@ -246,6 +249,16 @@ def __call__(self): dm = getMultiAdapter((content, field,), IDataManager) file_ = dm.get() + + if isinstance(file_, basestring) and IASCII.providedBy(field): + """Encoded data. + """ + filename, data = b64decode_file(file_) + if INamedImageWidget.providedBy(self.context): + file_ = NamedImage(data=data, filename=filename) + else: + file_ = NamedFile(data=data, filename=filename) + if file_ is None: raise NotFound(self, self.filename, self.request) diff --git a/plone/formwidget/namedfile/widget.rst b/plone/formwidget/namedfile/widget.rst index c38a84a..24c806b 100644 --- a/plone/formwidget/namedfile/widget.rst +++ b/plone/formwidget/namedfile/widget.rst @@ -427,6 +427,7 @@ stored in the field:: >>> image_widget.extract() is content.image_field True + Download view ------------- @@ -469,6 +470,7 @@ Any additional traversal will result in an error:: ... NotFound: ... 'daisy.txt' + The converter ------------- @@ -553,8 +555,150 @@ being returned:: >>> field_value is IContent['file_field'].missing_value True >>> field_value = image_converter.toFieldValue(FileUpload(aFieldStorage)) - >>> field_value is IContent['file_field'].missing_value + >>> field_value is IContent['image_field'].missing_value + True + + +The Base64Converter for ASCII fields +------------------------------------ + +There is another converter, which converts between a NamedFile or file upload +instance and base64 encoded data, which can be stored in a ASCII field:: + + >>> from zope import schema + >>> from zope.interface import implements, Interface + >>> class IASCIIContent(Interface): + ... file_field = schema.ASCII(title=u"File") + ... image_field = schema.ASCII(title=u"Image") + + >>> from plone.formwidget.namedfile.converter import Base64Converter + >>> provideAdapter(Base64Converter) + + >>> from zope.component import getMultiAdapter + >>> from z3c.form.interfaces import IDataConverter + + >>> ascii_file_converter = getMultiAdapter( + ... (IASCIIContent['file_field'], file_widget), + ... IDataConverter + ... ) + >>> ascii_image_converter = getMultiAdapter( + ... (IASCIIContent['image_field'], image_widget), + ... IDataConverter + ... ) + +A value of None or '' results in the field's missing_value being returned:: + + >>> ascii_file_converter.toFieldValue(u'') is IASCIIContent['file_field'].missing_value True + >>> ascii_file_converter.toFieldValue(None) is IASCIIContent['file_field'].missing_value + True + + >>> ascii_image_converter.toFieldValue(u'') is IASCIIContent['image_field'].missing_value + True + >>> ascii_image_converter.toFieldValue(None) is IASCIIContent['image_field'].missing_value + True + +A named file/image instance is returned as Base 64 encoded string in the +following form:: + + filenameb64:BASE64_ENCODED_FILENAME;data64:BASE64_ENCODED_DATA + +Like so:: + + >>> ascii_file_converter.toFieldValue( + ... NamedFile(data='testfile', filename=u'test.txt')) + 'filenameb64:dGVzdC50eHQ=;datab64:dGVzdGZpbGU=' + >>> ascii_image_converter.toFieldValue( + ... NamedImage(data='testimage', filename=u'test.png')) + 'filenameb64:dGVzdC5wbmc=;datab64:dGVzdGltYWdl' + +A Base 64 encoded structure like descibed above is converted to the appropriate +type:: + + >>> afile = ascii_file_converter.toWidgetValue( + ... 'filenameb64:dGVzdC50eHQ=;datab64:dGVzdGZpbGU=') + >>> afile + + >>> afile.data + 'testfile' + >>> afile.filename + u'test.txt' + + >>> aimage = ascii_image_converter.toWidgetValue( + ... 'filenameb64:dGVzdC5wbmc=;datab64:dGVzdGltYWdl') + >>> aimage + + >>> aimage.data + 'testimage' + >>> aimage.filename + u'test.png' + +Finally, some tests with image uploads converted to the field value. + +Convert a file upload to the Base 64 encoded field value and handle the +filename too:: + + + >>> myfile = cStringIO.StringIO('File upload contents.') + >>> # \xc3\xb8 is UTF-8 for a small letter o with slash + >>> aFieldStorage = FieldStorageStub(myfile, filename='rand\xc3\xb8m.txt', + ... headers={'Content-Type': 'text/x-dummy'}) + >>> ascii_file_converter.toFieldValue(FileUpload(aFieldStorage)) + 'filenameb64:cmFuZMO4bS50eHQ=;datab64:RmlsZSB1cGxvYWQgY29udGVudHMu' + +A zero-length, unnamed FileUpload results in the field's missing_value +being returned:: + + >>> myfile = cStringIO.StringIO('') + >>> aFieldStorage = FieldStorageStub(myfile, filename='', headers={'Content-Type': 'application/octet-stream'}) + >>> field_value = ascii_file_converter.toFieldValue(FileUpload(aFieldStorage)) + >>> field_value is IASCIIContent['file_field'].missing_value + True + >>> field_value = ascii_image_converter.toFieldValue(FileUpload(aFieldStorage)) + >>> field_value is IASCIIContent['image_field'].missing_value + True + + +The Download view on ASCII fields +--------------------------------- +:: + + >>> class ASCIIContent(object): + ... implements(IASCIIContent) + ... def __init__(self, file, image): + ... self.file_field = file + ... self.image_field = image + ... + ... def absolute_url(self): + ... return 'http://example.com/content2' + + >>> content = ASCIIContent( + ... NamedFile(data="testfile", filename=u"test.txt"), + ... NamedImage(data="testimage", filename=u"test.png")) + + >>> from z3c.form.widget import FieldWidget + + >>> ascii_file_widget = FieldWidget(IASCIIContent['file_field'], NamedFileWidget(TestRequest())) + >>> ascii_file_widget.context = content + + >>> ascii_image_widget = FieldWidget(IASCIIContent['image_field'], NamedImageWidget(TestRequest())) + >>> ascii_image_widget.context = content + + >>> request = TestRequest() + >>> view = Download(ascii_file_widget, request) + >>> view() + 'testfile' + + >>> request.response.getHeader('Content-Disposition') + "attachment; filename*=UTF-8''test.txt" + + >>> view = Download(ascii_image_widget, request) + >>> view() + 'testimage' + + >>> request.response.getHeader('Content-Disposition') + "attachment; filename*=UTF-8''test.png" + The validator -------------