Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/news/21.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added forwarding of request HTTP headers configured on the form block (``httpHeaders``, one header name per line) into the administration email. @ericof
63 changes: 35 additions & 28 deletions backend/src/plone/formblock/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from plone.dexterity.content import DexterityContent
from plone.schema import JSONField
from typing import Any
from typing import Literal
from typing import NotRequired
from typing import TypedDict
from zope import schema
from zope.interface import Attribute
Expand Down Expand Up @@ -61,34 +63,39 @@ class RichTextField(TypedDict):
data: str


class SchemaFormBlock(TypedDict):
"""A form block schema definition."""

schema: FormSchema
title: str
description: str
submit_label: str
show_cancel: bool
cancel_label: str
forward_user_to: str
success: str
thankyou: str
captcha: str
send: bool
recipients: str
bcc: str
admin_info: str
sender: str
sender_name: str
subject: str
mail_header: RichTextField
mail_footer: RichTextField
store: bool
data_wipe: int
send_confirmation: bool
confirmation_recipients: str
fixed_attachment: Any
mail_template: str
SchemaFormBlock = TypedDict(
"SchemaFormBlock",
{
"@type": Literal["schemaForm"],
"schema": FormSchema,
"title": NotRequired[str],
"description": NotRequired[str],
"submit_label": NotRequired[str],
"show_cancel": bool,
"cancel_label": NotRequired[str],
"forward_user_to": NotRequired[str],
"success": NotRequired[str],
"thankyou": NotRequired[str],
"captcha": NotRequired[str],
"captcha_props": NotRequired[dict],
"send": NotRequired[bool],
"recipients": NotRequired[str],
"bcc": NotRequired[str],
"admin_info": NotRequired[str],
"httpHeaders": NotRequired[str],
"sender": NotRequired[str],
"sender_name": NotRequired[str],
"subject": str,
"mail_header": NotRequired[RichTextField],
"mail_footer": NotRequired[RichTextField],
"store": NotRequired[bool],
"data_wipe": NotRequired[int],
"send_confirmation": NotRequired[bool],
"confirmation_recipients": NotRequired[str],
"fixed_attachment": NotRequired[Any],
"mail_template": NotRequired[str],
},
)


@dataclasses.dataclass
Expand Down
8 changes: 2 additions & 6 deletions backend/src/plone/formblock/processors/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,8 @@ def _prepare_msg_admin(
body = self.prepare_message(True)
body_text = self._body_as_plain_text(body)

headers = {}
headers_to_forward = self.block.get("httpHeaders", [])
for header in headers_to_forward:
header_value = self.request.get(header)
if header_value:
headers[header] = header_value
# Extract headers from the block and request
headers = utils.request_headers_from_block(self.block, self.request)

return utils.create_message(
mfrom=mfrom,
Expand Down
21 changes: 21 additions & 0 deletions backend/src/plone/formblock/utils/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from plone.formblock.interfaces import AddressesFromBlock
from plone.formblock.interfaces import DEFAULT_TEMPLATE
from plone.formblock.interfaces import SchemaFormBlock
from zope.publisher.http import HTTPRequest

import codecs
import os
Expand Down Expand Up @@ -156,3 +157,23 @@ def create_message(
attachments = attachments or {}
add_attachaments_to_msg(msg=msg, attachments=attachments)
return msg


def request_headers_from_block(
block: SchemaFormBlock, request: HTTPRequest
) -> dict[str, str]:
"""Extract HTTP headers from a form block and request.

Substituting variables if necessary.
"""
raw_http_headers: str = block.get("httpHeaders", "") or ""
headers_to_forward: list[str] = [
header.strip().upper()
for header in raw_http_headers.splitlines()
if header.strip()
]
headers: dict[str, str] = {}
for header in headers_to_forward:
if header_value := request.getHeader(header):
headers[header] = header_value
return headers
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ def document_blocks() -> dict:
"sender": "john@doe.com",
"sender_name": "John Doe",
"subject": "test subject",
"httpHeaders": [
"REMOTE_ADDR",
"PATH_INFO",
],
"httpHeaders": "REMOTE_ADDR\nPATH_INFO",
"schema": {
"fieldsets": [
{
Expand Down
92 changes: 92 additions & 0 deletions backend/tests/utils/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from plone.formblock.interfaces import SchemaFormBlock

import pytest


@pytest.fixture
def block_info() -> SchemaFormBlock:
return {
"@type": "schemaForm",
"admin_info": "Foo bar",
"cancel_label": "Cancel",
"captcha": "honeypot",
"captcha_props": {"provider": "protected_1"},
"data_wipe": -1,
"httpHeaders": "X_FORWARDED_FOR\nREMOTE_ADDR\nUSER_AGENT",
"recipients": "foo@bar.edu",
"schema": {
"fieldsets": [
{
"fields": [
"name",
"email",
"interests",
"message",
"captchaWidget",
],
"id": "default",
"title": "Default",
}
],
"properties": {
"captchaToken": {},
"captchaWidget": {
"captcha_props": {"provider": "protected_1"},
"title": "honeypot",
"widget": "honeypot",
},
"email": {
"description": "Your e-mail",
"factory": "label_email",
"id": "email",
"title": "E-mail",
"type": "string",
"widget": "email",
},
"interests": {
"choices": [
["Plone", "Plone"],
["Python", "Python"],
["React", "React"],
["Star Trek", "Star Trek"],
["World Cup", "World Cup"],
],
"description": "Select some topics you want to know more",
"factory": "checkbox_group",
"id": "interests",
"title": "Your Interests",
"type": "array",
"values": ["Plone", "Python", "React", "Star Trek", "World Cup"],
"widget": "checkbox_group",
},
"message": {
"description": "Please, let us know a bit more about you",
"factory": "textarea",
"id": "message",
"minLength": "50",
"title": "Additional information",
"type": "string",
"widget": "textarea",
},
"name": {
"description": "Please, tell us your name",
"factory": {"label": "Text", "value": "label_text_field"},
"id": "name",
"placeholder": "John Smith",
"title": "Your name",
"type": "string",
},
},
"required": ["name", "email", "interests", "message"],
},
"send": True,
"sender": "noreply@plone.org",
"sender_name": "Plone",
"show_cancel": True,
"store": True,
"subject": "Plone Form Block",
"submit_label": "Submit",
"success": "Thank you! You have submitted the following data:",
"thankyou": "${formfields}",
"title": "Contact our Team",
}
42 changes: 42 additions & 0 deletions backend/tests/utils/test_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
need ``api`` access and live in :class:`TestApiSettings`.
"""

from copy import deepcopy
from email import policy
from email.message import EmailMessage
from plone import api
Expand Down Expand Up @@ -403,3 +404,44 @@ def test_addresses_from_block_empty_recipients_not_defaulted(self):
# absent; an explicit empty string is preserved as-is.
addresses = addresses_from_block({"recipients": ""}, {})
assert addresses.admin_recipients == ""


class TestRequestHeadersFromBlock:
@pytest.fixture(autouse=True)
def _setup(self, functional_portal, functional_http_request):
from plone.formblock.utils.email import request_headers_from_block

self.portal = functional_portal
self.request = functional_http_request
self.func = request_headers_from_block

@pytest.mark.parametrize(
"httpHeaders,headers,total",
(
("", {"HTTP_USER_AGENT": "Mozilla/5.0"}, 0),
("USER_AGENT", {"HTTP_USER_AGENT": "Mozilla/5.0"}, 1),
("REMOTE_ADDR", {"HTTP_USER_AGENT": "Mozilla/5.0"}, 0),
(
"X_FORWARDED_FOR\nREMOTE_ADDR\nUSER_AGENT",
{"HTTP_USER_AGENT": "Mozilla/5.0"},
1,
),
(
"X_FORWARDED_FOR\nREMOTE_ADDR\nUSER_AGENT",
{"HTTP_USER_AGENT": "Mozilla/5.0", "REMOTE_ADDR": "200.162.135.12"},
2,
),
),
)
def test_request_headers_from_block_empty(
self, block_info, httpHeaders: str, headers: dict[str, str], total: int
):
# Set values in the block
block = deepcopy(block_info)
block["httpHeaders"] = httpHeaders
# Set headers in request
request = self.request
for key, value in headers.items():
request.environ[key] = value
headers = self.func(block, request)
assert len(headers) == total
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,16 @@ msgstr "Falls eine Seite angegeben ist, wird der User nach dem Absenden des Form
msgid "hidden"
msgstr "Versteckt"

#. Default: "HTTP Headers"
#: components/Blocks/SchemaForm/schema
msgid "http_headers"
msgstr ""

#. Default: "HTTP headers to be included in the email sent to the administration email (not for the user email), one per line. Exemple: X-Forwarded-For, User-Agent, Referer. The values will be taken from the request headers."
#: components/Blocks/SchemaForm/schema
msgid "http_headers_description"
msgstr ""

#. Default: "Mail template"
#: components/Blocks/SchemaForm/schema
msgid "mail_template"
Expand Down Expand Up @@ -383,7 +393,7 @@ msgstr "Textbereichsfeld"
msgid "thankyou"
msgstr "Dankesnachricht"

#. Default: "A text with simple formatting can be entered. Also it is possible to use variables; ${field_id} can be used to display the value of a field inside the form. The ${formfields} variable lists all form fields in a tabular view."
#. Default: "A text with simple formatting can be entered. Also it is possible to use variables; $'{field_id}' can be used to display the value of a field inside the form. The $'{formfields}' variable lists all form fields in a tabular view."
#: components/Blocks/SchemaForm/schema
msgid "thankyou_description"
msgstr "Ein Text mit einfacher Formatierung kann eingegeben werden. Außerdem ist es möglich, Variablen zu verwenden; ${field_id} kann verwendet werden, um den Wert eines Feldes im Formular anzuzeigen. Die Variable ${formfields} listet alle Formularfelder in einer tabellarischen Ansicht auf."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,16 @@ msgstr ""
msgid "hidden"
msgstr ""

#. Default: "HTTP Headers"
#: components/Blocks/SchemaForm/schema
msgid "http_headers"
msgstr ""

#. Default: "HTTP headers to be included in the email sent to the administration email (not for the user email), one per line. Exemple: X-Forwarded-For, User-Agent, Referer. The values will be taken from the request headers."
#: components/Blocks/SchemaForm/schema
msgid "http_headers_description"
msgstr ""

#. Default: "Mail template"
#: components/Blocks/SchemaForm/schema
msgid "mail_template"
Expand Down Expand Up @@ -383,7 +393,7 @@ msgstr ""
msgid "thankyou"
msgstr ""

#. Default: "A text with simple formatting can be entered. Also it is possible to use variables; ${field_id} can be used to display the value of a field inside the form. The ${formfields} variable lists all form fields in a tabular view."
#. Default: "A text with simple formatting can be entered. Also it is possible to use variables; $'{field_id}' can be used to display the value of a field inside the form. The $'{formfields}' variable lists all form fields in a tabular view."
#: components/Blocks/SchemaForm/schema
msgid "thankyou_description"
msgstr ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,16 @@ msgstr ""
msgid "hidden"
msgstr ""

#. Default: "HTTP Headers"
#: components/Blocks/SchemaForm/schema
msgid "http_headers"
msgstr ""

#. Default: "HTTP headers to be included in the email sent to the administration email (not for the user email), one per line. Exemple: X-Forwarded-For, User-Agent, Referer. The values will be taken from the request headers."
#: components/Blocks/SchemaForm/schema
msgid "http_headers_description"
msgstr ""

#. Default: "Mail template"
#: components/Blocks/SchemaForm/schema
msgid "mail_template"
Expand Down Expand Up @@ -389,7 +399,7 @@ msgstr ""
msgid "thankyou"
msgstr ""

#. Default: "A text with simple formatting can be entered. Also it is possible to use variables; ${field_id} can be used to display the value of a field inside the form. The ${formfields} variable lists all form fields in a tabular view."
#. Default: "A text with simple formatting can be entered. Also it is possible to use variables; $'{field_id}' can be used to display the value of a field inside the form. The $'{formfields}' variable lists all form fields in a tabular view."
#: components/Blocks/SchemaForm/schema
msgid "thankyou_description"
msgstr ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,16 @@ msgstr ""
msgid "hidden"
msgstr ""

#. Default: "HTTP Headers"
#: components/Blocks/SchemaForm/schema
msgid "http_headers"
msgstr ""

#. Default: "HTTP headers to be included in the email sent to the administration email (not for the user email), one per line. Exemple: X-Forwarded-For, User-Agent, Referer. The values will be taken from the request headers."
#: components/Blocks/SchemaForm/schema
msgid "http_headers_description"
msgstr ""

#. Default: "Mail template"
#: components/Blocks/SchemaForm/schema
msgid "mail_template"
Expand Down Expand Up @@ -385,7 +395,7 @@ msgstr ""
msgid "thankyou"
msgstr ""

#. Default: "A text with simple formatting can be entered. Also it is possible to use variables; ${field_id} can be used to display the value of a field inside the form. The ${formfields} variable lists all form fields in a tabular view."
#. Default: "A text with simple formatting can be entered. Also it is possible to use variables; $'{field_id}' can be used to display the value of a field inside the form. The $'{formfields}' variable lists all form fields in a tabular view."
#: components/Blocks/SchemaForm/schema
msgid "thankyou_description"
msgstr ""
Expand Down
Loading
Loading