diff --git a/news/122.bugfix b/news/122.bugfix new file mode 100644 index 00000000..367fd1c3 --- /dev/null +++ b/news/122.bugfix @@ -0,0 +1,4 @@ +Fix #122 validating if image is supported by PIL showing a validation error if not. +Include Pillow dependency in setup.py. +Fix ValueError: User could not be found in BaseTest setUp adding a transaction.commit(). +[rber474] \ No newline at end of file diff --git a/plone/app/users/browser/account.py b/plone/app/users/browser/account.py index bf02d69b..720e53f9 100644 --- a/plone/app/users/browser/account.py +++ b/plone/app/users/browser/account.py @@ -1,5 +1,7 @@ from AccessControl import Unauthorized from Acquisition import aq_inner +from PIL import Image +from PIL import UnidentifiedImageError from plone.app.layout.navigation.interfaces import INavigationRoot from plone.app.users.browser.interfaces import IAccountPanelForm from plone.app.users.browser.schemaeditor import getFromBaseSchema @@ -44,6 +46,11 @@ ), ) +MESSAGE_IMAGE_NOT_SUPPORTED = _( + "message_image_not_supported", + "The file you selected is not supported by Pillow. " "Please choose another.", +) + def getSchema(schema_interface, schema_adapter, form_name=None): request = getRequest() @@ -259,6 +266,23 @@ def validate_email(self, action, data): if err_str: notifyWidgetActionExecutionError(action, "email", err_str) + def validate_portrait(self, action, data): + """Portrait validation. + Checks if image is supported by Pillow. + SVG files are not yet supported. + """ + error_keys = [error.field.getName() for error in action.form.widgets.errors] + if "portrait" not in error_keys and data["portrait"] is not None: + portrait = data["portrait"].open() + try: + Image.open(portrait) + except UnidentifiedImageError: + notifyWidgetActionExecutionError( + action, "portrait", MESSAGE_IMAGE_NOT_SUPPORTED + ) + except Exception as exc: + raise exc + @button.buttonAndHandler(_("Save")) def handleSave(self, action): CheckAuthenticator(self.request) @@ -269,6 +293,11 @@ def handleSave(self, action): if "email" in data: self.validate_email(action, data) + # Validate portrait, upload image could be not supported + # by PIL what raises an exception when scaling image. + if "portrait" in data: + self.validate_portrait(action, data) + if action.form.widgets.errors: self.status = self.formErrorsMessage return diff --git a/plone/app/users/tests/base.py b/plone/app/users/tests/base.py index 52e4b160..60924f4d 100644 --- a/plone/app/users/tests/base.py +++ b/plone/app/users/tests/base.py @@ -25,6 +25,7 @@ from zope.component import getSiteManager from zope.component import getUtility +import transaction import unittest @@ -43,6 +44,7 @@ def setUp(self): self.browser = Browser(self.layer["app"]) self.request = self.layer["request"] + transaction.commit() def tearDown(self): login(self.portal, "admin") diff --git a/plone/app/users/tests/test_portrait.py b/plone/app/users/tests/test_portrait.py new file mode 100644 index 00000000..fb158fa7 --- /dev/null +++ b/plone/app/users/tests/test_portrait.py @@ -0,0 +1,45 @@ +from pkg_resources import resource_stream +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.users.tests.base import BaseTestCase + + +class TestPortrait(BaseTestCase): + def test_regression_supported_image_type_122(self): + # https://github.com/plone/plone.app.users/issues/122 + + self.browser.open("http://nohost/plone/") + self.browser.getLink("Log in").click() + self.browser.getControl("Login Name").value = SITE_OWNER_NAME + self.browser.getControl("Password").value = SITE_OWNER_PASSWORD + self.browser.getControl("Log in").click() + self.browser.open("http://nohost/plone/@@personal-information") + self.browser.getControl(name="form.widgets.email").value = "test@test.com" + portrait_file = resource_stream("plone.app.users.tests", "onepixel.jpg") + self.browser.getControl(name="form.widgets.portrait").add_file( + portrait_file, "image/jpg", "onepixel.# jpg" + ) + self.browser.getControl("Save").click() + self.assertIn("Changes saved.", self.browser.contents) + + def test_not_supported_image_type_122(self): + # https://github.com/plone/plone.app.users/issues/122 + + self.browser.open("http://nohost/plone/") + self.browser.getLink("Log in").click() + self.browser.getControl("Login Name").value = SITE_OWNER_NAME + self.browser.getControl("Password").value = SITE_OWNER_PASSWORD + self.browser.getControl("Log in").click() + self.browser.open("http://nohost/plone/@@personal-information") + self.browser.getControl(name="form.widgets.email").value = "test@test.com" + portrait_file = resource_stream( + "plone.app.users.tests", "transparent_square.svg" + ) + self.browser.getControl(name="form.widgets.portrait").add_file( + portrait_file, "image/svg+xml", "onepixel.# jpg" + ) + self.browser.getControl("Save").click() + self.assertIn( + "The file you selected is not supported by Pillow. Please choose another.", + self.browser.contents, + ) diff --git a/plone/app/users/tests/transparent_square.svg b/plone/app/users/tests/transparent_square.svg new file mode 100644 index 00000000..0b11a65e --- /dev/null +++ b/plone/app/users/tests/transparent_square.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 125c535a..ff638bef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,6 +120,7 @@ Zope = [ ] python-dateutil = ['dateutil'] ignore-packages = ['Products.CMFPlone'] +Pillow = ['PIL'] ## # Add extra configuration options in .meta.toml: diff --git a/setup.py b/setup.py index c774b37e..81bc08e7 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ extras_require=extras_require, install_requires=[ "Acquisition", + "Pillow", "Products.GenericSetup", "Products.PlonePAS >= 5.0.1", "Products.statusmessages",