diff --git a/Build/Sources/TypeScript/backend/mail-link-handler.ts b/Build/Sources/TypeScript/backend/mail-link-handler.ts index b71f467e50c6cc392a021089a67324cb569e5b5f..3c90b977de90dd4d980a1fc27f8d9eb23c9e8a9c 100644 --- a/Build/Sources/TypeScript/backend/mail-link-handler.ts +++ b/Build/Sources/TypeScript/backend/mail-link-handler.ts @@ -24,16 +24,20 @@ class MailLinkHandler { new RegularEvent('submit', (evt: MouseEvent, targetEl: HTMLElement): void => { evt.preventDefault(); const inputField = targetEl.querySelector('[name="lemail"]') as HTMLInputElement; - let value = inputField.value; - if (value === 'mailto:') { - return; + const value = inputField.value; + const params = new URLSearchParams(); + for (const elementName of ['subject', 'cc', 'bcc', 'body']) { + const element = targetEl.querySelector('[data-mailto-part="' + elementName + '"]') as HTMLInputElement|null; + if (element?.value.length) { + params.set(elementName, encodeURIComponent(element.value)); + } } - - while (value.substr(0, 7) === 'mailto:') { - value = value.substr(7); + let mailtoLink = 'mailto:' + value; + if ([...params].length > 0) { + mailtoLink += '?' + params.toString(); } - LinkBrowser.finalizeFunction('mailto:' + value); + LinkBrowser.finalizeFunction(mailtoLink); }).delegateTo(document, '#lmailform'); } } diff --git a/typo3/sysext/backend/Classes/Controller/AbstractLinkBrowserController.php b/typo3/sysext/backend/Classes/Controller/AbstractLinkBrowserController.php index af78a545c95f57619a8a34a4f62c63a36e730ce8..3b61854028af7bb4ea8d35e7fe306e0d9dd3063a 100644 --- a/typo3/sysext/backend/Classes/Controller/AbstractLinkBrowserController.php +++ b/typo3/sysext/backend/Classes/Controller/AbstractLinkBrowserController.php @@ -446,6 +446,7 @@ abstract class AbstractLinkBrowserController foreach ($this->linkAttributeFields as $attribute) { $content .= $fieldRenderingDefinitions[$attribute] ?? ''; } + $view->assign('allowedLinkAttributes', array_combine($this->linkAttributeFields, $this->linkAttributeFields)); // add update button if appropriate if (!empty($this->currentLinkParts) && $this->displayedLinkHandler === $this->currentLinkHandler && $this->currentLinkHandler->isUpdateSupported()) { diff --git a/typo3/sysext/backend/Classes/LinkHandler/MailLinkHandler.php b/typo3/sysext/backend/Classes/LinkHandler/MailLinkHandler.php index f4ea526b076b575f77c24097f5a3b40312be2da6..ff60c295065d2777065f7714e912e1d4fca739e8 100644 --- a/typo3/sysext/backend/Classes/LinkHandler/MailLinkHandler.php +++ b/typo3/sysext/backend/Classes/LinkHandler/MailLinkHandler.php @@ -32,26 +32,18 @@ class MailLinkHandler extends AbstractLinkHandler implements LinkHandlerInterfac protected $linkParts = []; /** - * We don't support updates since there is no difference to simply set the link again. + * "target" and "rel" are not allowed * - * @var bool + * @var string[] */ - protected $updateSupported = false; + protected $linkAttributes = ['title', 'class', 'subject', 'body', 'cc', 'bcc']; /** - * Constructor + * We don't support updates since there is no difference to simply set the link again. + * + * @var bool */ - public function __construct() - { - parent::__construct(); - // remove unsupported link attributes - foreach (['target', 'rel'] as $attribute) { - $position = array_search($attribute, $this->linkAttributes, true); - if ($position !== false) { - unset($this->linkAttributes[$position]); - } - } - } + protected $updateSupported = false; /** * Checks if this is the handler for the given link @@ -87,7 +79,11 @@ class MailLinkHandler extends AbstractLinkHandler implements LinkHandlerInterfac public function render(ServerRequestInterface $request): string { $this->pageRenderer->loadJavaScriptModule('@typo3/backend/mail-link-handler.js'); - $this->view->assign('email', !empty($this->linkParts) ? $this->linkParts['url']['email'] : ''); + if (is_array($this->linkParts['url'] ?? null)) { + foreach ($this->linkParts['url'] as $name => $value) { + $this->view->assign($name, rawurldecode($value)); + } + } return $this->view->render('LinkBrowser/Mail'); } diff --git a/typo3/sysext/backend/Resources/Private/Language/locallang_browse_links.xlf b/typo3/sysext/backend/Resources/Private/Language/locallang_browse_links.xlf index da272d961e7b35052e2b369765fab4a84a44b376..82f3f2aa06d8f45c789ad312fcc958b256cfe803 100644 --- a/typo3/sysext/backend/Resources/Private/Language/locallang_browse_links.xlf +++ b/typo3/sysext/backend/Resources/Private/Language/locallang_browse_links.xlf @@ -30,6 +30,18 @@ <trans-unit id="email" resname="email"> <source>Email</source> </trans-unit> + <trans-unit id="email.subject" resname="email.subject"> + <source>Subject</source> + </trans-unit> + <trans-unit id="email.cc" resname="email.cc"> + <source>CC</source> + </trans-unit> + <trans-unit id="email.bcc" resname="email.bcc"> + <source>BCC</source> + </trans-unit> + <trans-unit id="email.body" resname="email.body"> + <source>Body</source> + </trans-unit> <trans-unit id="telephone" resname="telephone"> <source>Telephone</source> </trans-unit> diff --git a/typo3/sysext/backend/Resources/Private/Templates/LinkBrowser/Mail.html b/typo3/sysext/backend/Resources/Private/Templates/LinkBrowser/Mail.html index 9b4b160bf2cacc833531aa44f47df8d55c79f8e3..b84d26fa979946800c4e0fa425856ef7c65c28d7 100644 --- a/typo3/sysext/backend/Resources/Private/Templates/LinkBrowser/Mail.html +++ b/typo3/sysext/backend/Resources/Private/Templates/LinkBrowser/Mail.html @@ -11,19 +11,70 @@ <label for="lemail" class="form-label"> <f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:emailAddress" /> </label> - <div class="input-group"> + <input + id="lemail" + type="email" + name="lemail" + class="form-control" + value="{email}" + required /> + </div> + <f:if condition="{allowedLinkAttributes.subject}"> + <div class="element-browser-form-group"> + <label for="emailSubject" class="form-label"> + <f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:email.subject" /> + </label> + <input + id="emailSubject" + name="emailSubject" + class="form-control" + value="{subject}" + data-mailto-part="subject"/> + </div> + </f:if> + <f:if condition="{allowedLinkAttributes.cc}"> + <div class="element-browser-form-group"> + <label for="emailCc" class="form-label"> + <f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:email.cc" /> + </label> + <input + id="emailCc" + name="emailCc" + class="form-control" + value="{cc}" + data-mailto-part="cc"/> + </div> + </f:if> + <f:if condition="{allowedLinkAttributes.bcc}"> + <div class="element-browser-form-group"> + <label for="emailBcc" class="form-label"> + <f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:email.bcc" /> + </label> <input - id="lemail" - type="email" - name="lemail" + id="emailBcc" + name="emailBcc" + class="form-control" + value="{bcc}" + data-mailto-part="bcc"/> + </div> + </f:if> + <f:if condition="{allowedLinkAttributes.body}"> + <div class="element-browser-form-group"> + <label for="emailBody" class="form-label"> + <f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:email.body" /> + </label> + <textarea + id="emailBody" + name="emailBody" + rows="10" + cols="50" size="20" class="form-control" - value="{email}" - required /> - <div class="input-group-btn"> - <input class="btn btn-primary" type="submit" value="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:setLink')}" /> - </div> + data-mailto-part="body">{body}</textarea> </div> + </f:if> + <div class="element-browser-form-group"> + <input class="btn btn-primary" type="submit" value="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:setLink')}" /> </div> </form> </f:section> diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/mail-link-handler.js b/typo3/sysext/backend/Resources/Public/JavaScript/mail-link-handler.js index 5b581f536153ee84854c6bfbda739b942606491a..1f338641239fcd4e3847eab4ab6cb6663d8ad66b 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/mail-link-handler.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/mail-link-handler.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -import LinkBrowser from"@typo3/backend/link-browser.js";import RegularEvent from"@typo3/core/event/regular-event.js";class MailLinkHandler{constructor(){new RegularEvent("submit",((e,r)=>{e.preventDefault();let t=r.querySelector('[name="lemail"]').value;if("mailto:"!==t){for(;"mailto:"===t.substr(0,7);)t=t.substr(7);LinkBrowser.finalizeFunction("mailto:"+t)}})).delegateTo(document,"#lmailform")}}export default new MailLinkHandler; \ No newline at end of file +import LinkBrowser from"@typo3/backend/link-browser.js";import RegularEvent from"@typo3/core/event/regular-event.js";class MailLinkHandler{constructor(){new RegularEvent("submit",((e,t)=>{e.preventDefault();const r=t.querySelector('[name="lemail"]').value,n=new URLSearchParams;for(const e of["subject","cc","bcc","body"]){const r=t.querySelector('[data-mailto-part="'+e+'"]');r?.value.length&&n.set(e,encodeURIComponent(r.value))}let o="mailto:"+r;[...n].length>0&&(o+="?"+n.toString()),LinkBrowser.finalizeFunction(o)})).delegateTo(document,"#lmailform")}}export default new MailLinkHandler; \ No newline at end of file diff --git a/typo3/sysext/core/Classes/LinkHandling/EmailLinkHandler.php b/typo3/sysext/core/Classes/LinkHandling/EmailLinkHandler.php index a2d1455c69e78fc4df1d6d9b38987127d0373658..815ac8411ace1ffb8e92b3a3184bd69546ea72db 100644 --- a/typo3/sysext/core/Classes/LinkHandling/EmailLinkHandler.php +++ b/typo3/sysext/core/Classes/LinkHandling/EmailLinkHandler.php @@ -27,7 +27,18 @@ class EmailLinkHandler implements LinkHandlingInterface */ public function asString(array $parameters): string { - return 'mailto:' . $parameters['email']; + $queryParameters = []; + foreach (['subject', 'cc', 'bcc', 'body'] as $additionalInfo) { + if (isset($parameters[$additionalInfo])) { + $queryParameters[$additionalInfo] = rawurldecode($parameters[$additionalInfo]); + } + } + $result = 'mailto:' . $parameters['email']; + if ($queryParameters !== []) { + // We need to percent-encode additional parameters (RFC 3986) + $result .= '?' . http_build_query($queryParameters, '', '&', PHP_QUERY_RFC3986); + } + return $result; } /** @@ -36,9 +47,17 @@ class EmailLinkHandler implements LinkHandlingInterface */ public function resolveHandlerData(array $data): array { - if (stripos($data['email'], 'mailto:') === 0) { - return ['email' => substr($data['email'], 7)]; + $linkParts = parse_url($data['email'] ?? ''); + $data['email'] = $linkParts['path']; + if (isset($linkParts['query'])) { + $result = []; + parse_str($linkParts['query'], $result); + foreach (['subject', 'cc', 'bcc', 'body'] as $additionalInfo) { + if (isset($result[$additionalInfo])) { + $data[$additionalInfo] = $result[$additionalInfo]; + } + } } - return ['email' => $data['email']]; + return $data; } } diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Feature-84594-AdditionalParametersToEmailLinks.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-84594-AdditionalParametersToEmailLinks.rst new file mode 100644 index 0000000000000000000000000000000000000000..a208112dba01de16732ac498c7e361fb539e2aa1 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-84594-AdditionalParametersToEmailLinks.rst @@ -0,0 +1,66 @@ +.. include:: /Includes.rst.txt + +.. _feature-84594-1674211080: + +====================================================== +Feature: #84594 - Additional parameters to email links +====================================================== + +See :issue:`84594` + +Description +=========== + +Editors in TYPO3 now have more possibilities to set options when +creating a link to a specific email address, according to the "mailto:" +protocol. + +This way, editors can now pre-fill the fields "subject", "CC", "BCC" +and "body" in the TYPO3 Backend when creating a link to an email +address, which are then percent-encoded to the actual email link. + +In addition, the `<f:link.email>` ViewHelper has the same additional +attributes as well: + + <f:link.email + email="foo@bar.tld" + subject="Check out this website" + cc="foo@example.com" + bcc="bar@example.com" + > + some custom content + </f:link.email> + +All of the properties and the link fields are optional. + +For custom email links, it is also now possible to restrict the additional +options via TCA: + +Example configuration +--------------------- + +.. code-block:: php + + 'header_link' => [ + 'label' => 'Link', + 'config' => [ + 'type' => 'link', + 'allowedTypes' => ['email'], + 'size' => 50, + 'appearance' => [ + // new options are "body", "cc", "bcc" and "subject" + 'allowedOptions' => ['body', 'cc'], + ], + ], + ], + +Impact +====== + +Editors now have more flexibility when creating links to emails in the +TYPO3 Backend. + +Integrators have more flexibility when creating links within Fluid +templates. + +.. index:: Backend diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Link/EmailViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Link/EmailViewHelper.php index 78f35d82ca8349cfa79521c822d015f20a146a59..11f61350e0c4b01cbebf0cda2308758e69d9ab77 100644 --- a/typo3/sysext/fluid/Classes/ViewHelpers/Link/EmailViewHelper.php +++ b/typo3/sysext/fluid/Classes/ViewHelpers/Link/EmailViewHelper.php @@ -19,6 +19,7 @@ namespace TYPO3\CMS\Fluid\ViewHelpers\Link; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Http\ApplicationType; +use TYPO3\CMS\Core\LinkHandling\EmailLinkHandler; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Fluid\Core\Rendering\RenderingContext; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; @@ -57,7 +58,17 @@ use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper; * * Output:: * - * <a href="#" data-mailto-token="ocknvq,hqqBdct0vnf" data-mailto-vector="1">some custom content</a> + * + * Email link with custom subject and prefilled cc + * ----------------------------------------------- + * + * :: + * + * <f:link.email email="foo@bar.tld" subject="Check out this website" cc="foo@example.com"">some custom content</f:link.email> + * + * Output:: + * + * <a href="mailto:foo@bar.tld?subject=Check%20out%20this%20website&cc=foo%40example.com">some custom content</a> * * Depending on `spamProtectEmailAddresses`_ setting. */ @@ -72,6 +83,10 @@ final class EmailViewHelper extends AbstractTagBasedViewHelper { parent::initializeArguments(); $this->registerArgument('email', 'string', 'The email address to be turned into a link', true); + $this->registerArgument('cc', 'string', 'The email address(es) for CC of the email link'); + $this->registerArgument('bcc', 'string', 'The email address(es) for BCC of the email link'); + $this->registerArgument('subject', 'string', 'A prefilled subject for the email link'); + $this->registerArgument('body', 'string', 'A prefilled body for the email link'); $this->registerUniversalTagAttributes(); $this->registerTagAttribute('name', 'string', 'Specifies the name of an anchor'); $this->registerTagAttribute('rel', 'string', 'Specifies the relationship between the current document and the linked document'); @@ -82,7 +97,7 @@ final class EmailViewHelper extends AbstractTagBasedViewHelper public function render(): string { $email = $this->arguments['email']; - $linkHref = 'mailto:' . $email; + $linkHref = GeneralUtility::makeInstance(EmailLinkHandler::class)->asString($this->arguments); $attributes = []; $linkText = htmlspecialchars($email); /** @var RenderingContext $renderingContext */ diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/EmailViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/EmailViewHelperTest.php index 4e376b307aaf840d0e8686328fcb9310d819a9d6..c351f71b19ff9101095df5a5a2a3b30371630c31 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/EmailViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/EmailViewHelperTest.php @@ -58,6 +58,21 @@ class EmailViewHelperTest extends FunctionalTestCase ['config.spamProtectEmailAddresses = 0'], '<a href="mailto:some@email.tld">some@email.tld</a>', ], + 'Plain email with prefilled subject' => [ + '<f:link.email email="some@email.tld" subject="This is a test email" />', + ['config.spamProtectEmailAddresses = 0'], + '<a href="mailto:some@email.tld?subject=This%20is%20a%20test%20email">some@email.tld</a>', + ], + 'Plain email with prefilled subject and body' => [ + '<f:link.email email="some@email.tld" subject="This is a test email" body="Hey Andrew,\nyou should check out these simple email links." />', + ['config.spamProtectEmailAddresses = 0'], + '<a href="mailto:some@email.tld?subject=This%20is%20a%20test%20email&body=Hey%20Andrew%2C%5Cnyou%20should%20check%20out%20these%20simple%20email%20links.">some@email.tld</a>', + ], + 'Plain email with subject, cc and bcc' => [ + '<f:link.email email="some@email.tld" subject="This is a test email" cc="benni@example.com,mack@example.com" bcc="all-team-members@example.com" />', + ['config.spamProtectEmailAddresses = 0'], + '<a href="mailto:some@email.tld?subject=This%20is%20a%20test%20email&cc=benni%40example.com%2Cmack%40example.com&bcc=all-team-members%40example.com">some@email.tld</a>', + ], 'Plain email with spam protection' => [ '<f:link.email email="some@email.tld" />', ['config.spamProtectEmailAddresses = 1'], diff --git a/typo3/sysext/frontend/Classes/Typolink/EmailLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/EmailLinkBuilder.php index 7f23174e9af6138f30bfc28022a568525d1dc7f8..ec39ed5c5ba925a2018a5314b572eb53009b4d35 100644 --- a/typo3/sysext/frontend/Classes/Typolink/EmailLinkBuilder.php +++ b/typo3/sysext/frontend/Classes/Typolink/EmailLinkBuilder.php @@ -21,6 +21,7 @@ use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use TYPO3\CMS\Core\LinkHandling\LinkService; use TYPO3\CMS\Core\Page\DefaultJavaScriptAssetTrait; +use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; /** @@ -34,7 +35,7 @@ class EmailLinkBuilder extends AbstractTypolinkBuilder implements LoggerAwareInt public function build(array &$linkDetails, string $linkText, string $target, array $conf): LinkResultInterface { - [$url, $linkText, $attributes] = $this->processEmailLink($linkDetails['email'], $linkText); + [$url, $linkText, $attributes] = $this->processEmailLink($linkDetails['email'], $linkText, $linkDetails); return (new LinkResult(LinkService::TYPE_EMAIL, $url)) ->withTarget($target) ->withLinkConfiguration($conf) @@ -57,11 +58,16 @@ class EmailLinkBuilder extends AbstractTypolinkBuilder implements LoggerAwareInt * @return array{0: string, 1: string, 2: array<string, string>} A numerical array with three items * @internal this method is not part of TYPO3's public API */ - public function processEmailLink(string $mailAddress, string $linkText): array + public function processEmailLink(string $mailAddress, string $linkText, array $linkDetails = []): array { $linkText = $linkText ?: htmlspecialchars($mailAddress); - $mailToUrl = 'mailto:' . $mailAddress; $attributes = []; + if ($linkDetails !== []) { + // Ensure to add also additional query parameters to the string + $mailToUrl = GeneralUtility::makeInstance(LinkService::class)->asString($linkDetails); + } else { + $mailToUrl = 'mailto:' . $mailAddress; + } // no processing happened, therefore, the default processing kicks in $tsfe = $this->getTypoScriptFrontendController(); diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/TypoLinkGeneratorTest.php b/typo3/sysext/frontend/Tests/Functional/SiteHandling/TypoLinkGeneratorTest.php index f29107ee9dfa902130f710b3d7e713c5c6962611..da770287d5a3a5f3d3c16fcc15b8e0c9e7a1e1bd 100644 --- a/typo3/sysext/frontend/Tests/Functional/SiteHandling/TypoLinkGeneratorTest.php +++ b/typo3/sysext/frontend/Tests/Functional/SiteHandling/TypoLinkGeneratorTest.php @@ -103,7 +103,7 @@ class TypoLinkGeneratorTest extends AbstractTestCase ], [ 't3://email?email=user@example.org?subject=Hello%20World#other', - '<a href="mailto:user@example.org?subject=Hello World">user@example.org?subject=Hello World</a>', + '<a href="mailto:user@example.org?subject=Hello%20World">user@example.org</a>', ], [ 't3://file?uid=1&type=1&other=other#other', diff --git a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php index f8366d1684c40ffe28138fc5f56d405fe6dbcb8a..bceb5f3188bbd871f83e1f82c1f19662d9d9bee4 100644 --- a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php +++ b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php @@ -2557,6 +2557,13 @@ class ContentObjectRendererTest extends UnitTestCase ], '<a href="mailto:foo@bar.org">Email address</a>', ], + 'Link to email address with subject + cc' => [ + 'Email address', + [ + 'parameter' => 'foo@bar.org?subject=This%20is%20a%20test', + ], + '<a href="mailto:foo@bar.org?subject=This%20is%20a%20test">Email address</a>', + ], 'Link to email address without link text' => [ '', [ @@ -2780,8 +2787,7 @@ class ContentObjectRendererTest extends UnitTestCase ], 'some.body@test.typo3.org', 'mailto:some.body@test.typo3.org?subject=foo%20bar', - // @todo no substitution, due to `?subject=` extension (which cannot be found in the link-text) - '<a href="#" data-mailto-token="ocknvq,uqog0dqfaBvguv0varq50qti?uwdlgev=hqq%42dct" data-mailto-vector="2">some.body@test.typo3.org</a>', + '<a href="#" data-mailto-token="ocknvq,uqog0dqfaBvguv0varq50qti?uwdlgev=hqq%42dct" data-mailto-vector="2">some.body(at)test.typo3(dot)org</a>', ], ]; }