diff --git a/typo3/sysext/core/Classes/Mail/FluidEmail.php b/typo3/sysext/core/Classes/Mail/FluidEmail.php index def7cdb93991a1582380001e19dbcd26115e7270..b35884316a6c493f38fdd10823b206ade87bc8e1 100644 --- a/typo3/sysext/core/Classes/Mail/FluidEmail.php +++ b/typo3/sysext/core/Classes/Mail/FluidEmail.php @@ -146,6 +146,15 @@ class FluidEmail extends Email if (in_array(static::FORMAT_PLAIN, $this->format, true)) { $this->text(trim($this->renderContent('txt'))); } + + $subjectFromTemplate = $this->view->renderSection( + 'Subject', + $this->view->getRenderingContext()->getVariableProvider()->getAll(), + true + ); + if (!empty($subjectFromTemplate)) { + $this->subject($subjectFromTemplate); + } } protected function renderContent(string $format): string diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-90411-HTML-basedWorkspaceNotificationEmailsOnStageChange.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-90411-HTML-basedWorkspaceNotificationEmailsOnStageChange.rst new file mode 100644 index 0000000000000000000000000000000000000000..0be1aa3393c5fa53c8e9668d9efb61b29cd3fe3a --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-90411-HTML-basedWorkspaceNotificationEmailsOnStageChange.rst @@ -0,0 +1,53 @@ +.. include:: ../../Includes.txt + +========================================================================== +Feature: #90411 - HTML-based workspace notification emails on stage change +========================================================================== + +See :issue:`90411` + +Description +=========== + +When inside workspaces, it is possible to notify affected (or all) +users belonging to that workspace to send out an email when +items have been moved to the next stage in the workflow process. + +These emails have been limited in the past due to marker-based templating and plain-text only. + +The emails have been reworked and migrated to Fluid-based templated +emails, allowing for administrators to customize the contents of +these emails. + +The following TSconfig options have been added: + +.. code-block:: typoscript + + # defines whether a preview link should be generated and populating + # the sys_preview database. A new variable {previewLink} + # is then available within the templated email + tx_workspaces.emails.stageChangeNotification.generatePreviewLink = 0 + + # path where to look for templates / layouts / partials + tx_workspaces.emails.layoutRootPaths.100 = EXT:myproject/... + tx_workspaces.emails.partialRootPaths.100 = EXT:myproject/... + tx_workspaces.emails.templateRootPaths.100 = EXT:myproject/... + tx_workspaces.emails.format = html/text/both + +The template name is always called `StageChangeNotification`. + +It is still possible to use the existing plain-text variant +by setting the format to "text" and using the previous email +contents, if applicable. It is however recommended to make use +of the Fluid-based variables to make output more efficient. + +The old TSconfig options have been superseded for defining the template via XLF labels. + + +Impact +====== + +Stage Change Notification emails are now sent as HTML+text by +default with the email template given in `EXT:workspaces/Resources/Private/Templates/Emails/StageChangeNotification`. + +.. index:: TSConfig, ext:workspaces \ No newline at end of file diff --git a/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php b/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php index 25e585757eb42b24a0199835090d0bcbf1a6326f..e123fd8c35f49405d673fbcabab2be36869c24eb 100644 --- a/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php +++ b/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php @@ -16,7 +16,6 @@ namespace TYPO3\CMS\Workspaces\Hook; use Doctrine\DBAL\DBALException; use Doctrine\DBAL\Platforms\SQLServerPlatform; -use Symfony\Component\Mime\Address; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Core\Environment; @@ -29,8 +28,6 @@ use TYPO3\CMS\Core\Database\RelationHandler; use TYPO3\CMS\Core\DataHandling\DataHandler; use TYPO3\CMS\Core\DataHandling\PlaceholderShadowColumnsResolver; use TYPO3\CMS\Core\Localization\LanguageService; -use TYPO3\CMS\Core\Mail\MailMessage; -use TYPO3\CMS\Core\Service\MarkerBasedTemplateService; use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction; use TYPO3\CMS\Core\SysLog\Action\Database as DatabaseAction; use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification; @@ -39,7 +36,7 @@ use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Versioning\VersionState; use TYPO3\CMS\Workspaces\DataHandler\CommandMap; -use TYPO3\CMS\Workspaces\Preview\PreviewUriBuilder; +use TYPO3\CMS\Workspaces\Notification\StageChangeNotification; use TYPO3\CMS\Workspaces\Service\StagesService; use TYPO3\CMS\Workspaces\Service\WorkspaceService; @@ -53,7 +50,6 @@ class DataHandlerHook /** * For accumulating information about workspace stages raised * on elements so a single mail is sent as notification. - * previously called "accumulateForNotifEmail" in DataHandler * * @var array */ @@ -100,8 +96,8 @@ class DataHandlerHook } $commandIsProcessed = true; $action = (string)$value['action']; - $comment = !empty($value['comment']) ? $value['comment'] : ''; - $notificationAlternativeRecipients = is_array($value['notificationAlternativeRecipients'] ?? null) ? $value['notificationAlternativeRecipients'] : []; + $comment = $value['comment'] ?: ''; + $notificationAlternativeRecipients = $value['notificationAlternativeRecipients'] ?? []; switch ($action) { case 'new': $dataHandler->versionizeRecord($table, $id, $value['label']); @@ -149,10 +145,14 @@ class DataHandlerHook */ public function processCmdmap_afterFinish(DataHandler $dataHandler) { - // Empty accumulation array: - foreach ($this->notificationEmailInfo as $notifItem) { - $this->notifyStageChange($notifItem['shared'][0], $notifItem['shared'][1], implode(', ', $notifItem['elements']), 0, $notifItem['shared'][2], $dataHandler, $notifItem['alternativeRecipients']); - } + // Empty accumulation array + $emailNotificationService = GeneralUtility::makeInstance(StageChangeNotification::class); + $this->sendStageChangeNotification( + $this->notificationEmailInfo, + $emailNotificationService, + $dataHandler + ); + // Reset notification array $this->notificationEmailInfo = []; // Reset remapped IDs @@ -161,6 +161,38 @@ class DataHandlerHook $this->flushWorkspaceCacheEntriesByWorkspaceId((int)$dataHandler->BE_USER->workspace); } + protected function sendStageChangeNotification( + array $accumulatedNotificationInformation, + StageChangeNotification $notificationService, + DataHandler $dataHandler + ): void { + foreach ($accumulatedNotificationInformation as $groupedNotificationInformation) { + $emails = (array)$groupedNotificationInformation['recipients']; + if (empty($emails)) { + continue; + } + $workspaceRec = BackendUtility::getRecord('sys_workspace', $groupedNotificationInformation['shared'][0]); + if (!is_array($workspaceRec)) { + continue; + } + $notificationService->notifyStageChange( + $workspaceRec, + (int)$groupedNotificationInformation['shared'][1], + $groupedNotificationInformation['elements'], + $groupedNotificationInformation['shared'][2], + $emails, + $dataHandler->BE_USER + ); + + if ($dataHandler->enableLogging) { + [$elementTable, $elementUid] = reset($groupedNotificationInformation['elements']); + $propertyArray = $dataHandler->getRecordProperties($elementTable, $elementUid); + $pid = $propertyArray['pid']; + $dataHandler->log($elementTable, $elementUid, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Notification email for stage change was sent to "' . implode('", "', $emails) . '"', -1, [], $dataHandler->eventPid($elementTable, $elementUid, $pid)); + } + } + } + /** * hook that is called when an element shall get deleted * @@ -454,246 +486,6 @@ class DataHandlerHook } } - /**************************** - ***** Notifications ****** - ****************************/ - /** - * Send an email notification to users in workspace - * - * @param array $stat Workspace access array from \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::checkWorkspace() - * @param int $stageId New Stage number: 0 = editing, 1= just ready for review, 10 = ready for publication, -1 = rejected! - * @param string $table Table name of element (or list of element names if $id is zero) - * @param int $id Record uid of element (if zero, then $table is used as reference to element(s) alone) - * @param string $comment User comment sent along with action - * @param DataHandler $dataHandler DataHandler object - * @param array $notificationAlternativeRecipients List of recipients to notify instead of be_users selected by sys_workspace, list is generated by workspace extension module - */ - protected function notifyStageChange(array $stat, $stageId, $table, $id, $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = []): void - { - $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']); - // So, if $id is not set, then $table is taken to be the complete element name! - $elementName = $id ? $table . ':' . $id : $table; - if (!is_array($workspaceRec)) { - return; - } - - // Get the new stage title - $stageService = GeneralUtility::makeInstance(StagesService::class); - $newStage = $stageService->getStageTitle((int)$stageId); - if (empty($notificationAlternativeRecipients)) { - // Compile list of recipients: - $emails = []; - switch ((int)$stat['stagechg_notification']) { - // Notify users of next stage only - case 1: - switch ((int)$stageId) { - case 1: - case 10: - $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true); - break; - case -1: - // List of elements to reject: - $allElements = explode(',', $elementName); - // Traverse them, and find the history of each - foreach ($allElements as $elRef) { - [$eTable, $eUid] = explode(':', $elRef); - - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_log'); - - $queryBuilder->getRestrictions()->removeAll(); - - $result = $queryBuilder - ->select('log_data', 'tstamp', 'userid') - ->from('sys_log') - ->where( - $queryBuilder->expr()->eq( - 'action', - $queryBuilder->createNamedParameter(6, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->eq( - 'details_nr', - $queryBuilder->createNamedParameter(30, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->eq( - 'tablename', - $queryBuilder->createNamedParameter($eTable, \PDO::PARAM_STR) - ), - $queryBuilder->expr()->eq( - 'recuid', - $queryBuilder->createNamedParameter($eUid, \PDO::PARAM_INT) - ) - ) - ->orderBy('uid', 'DESC') - ->execute(); - - // Find all implicated since the last stage-raise from editing to review: - while ($dat = $result->fetch()) { - $data = unserialize($dat['log_data']); - $emails = $this->getEmailsForStageChangeNotification($dat['userid'], true) + $emails; - if ($data['stage'] == 1) { - break; - } - } - } - break; - case 0: - $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']); - break; - default: - $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true); - } - break; - // Notify all users at all changes - case 10: - $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true); - $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']) + $emails; - break; - default: - // Do nothing - } - } else { - $emails = $notificationAlternativeRecipients; - } - // prepare and then send the emails - if (!empty($emails)) { - $previewUriBuilder = GeneralUtility::makeInstance(PreviewUriBuilder::class); - // Path to record is found: - [$elementTable, $elementUid] = explode(':', $elementName); - $elementUid = (int)$elementUid; - $elementRecord = BackendUtility::getRecord($elementTable, $elementUid); - $recordTitle = BackendUtility::getRecordTitle($elementTable, $elementRecord); - if ($elementTable === 'pages') { - $pageUid = $elementUid; - } else { - BackendUtility::fixVersioningPid($elementTable, $elementRecord); - $pageUid = ($elementUid = $elementRecord['pid']); - } - - // new way, options are - // pageTSconfig: tx_version.workspaces.stageNotificationEmail.subject - // userTSconfig: page.tx_version.workspaces.stageNotificationEmail.subject - $pageTsConfig = BackendUtility::getPagesTSconfig($pageUid); - $emailConfig = $pageTsConfig['tx_version.']['workspaces.']['stageNotificationEmail.']; - $markers = [ - '###RECORD_TITLE###' => $recordTitle, - '###RECORD_PATH###' => BackendUtility::getRecordPath($elementUid, '', 20), - '###SITE_NAME###' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], - '###SITE_URL###' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir, - '###WORKSPACE_TITLE###' => $workspaceRec['title'], - '###WORKSPACE_UID###' => $workspaceRec['uid'], - '###ELEMENT_NAME###' => $elementName, - '###NEXT_STAGE###' => $newStage, - '###COMMENT###' => $comment, - // See: #30212 - keep both markers for compatibility - '###USER_REALNAME###' => $dataHandler->BE_USER->user['realName'], - '###USER_FULLNAME###' => $dataHandler->BE_USER->user['realName'], - '###USER_USERNAME###' => $dataHandler->BE_USER->user['username'] - ]; - // only generate the link if the marker is in the template - prevents database from getting to much entries - if (GeneralUtility::isFirstPartOfStr($emailConfig['message'], 'LLL:')) { - $tempEmailMessage = $this->getLanguageService()->sL($emailConfig['message']); - } else { - $tempEmailMessage = $emailConfig['message']; - } - if (strpos($tempEmailMessage, '###PREVIEW_LINK###') !== false) { - $markers['###PREVIEW_LINK###'] = $previewUriBuilder->buildUriForPage((int)$elementUid, 0); - } - unset($tempEmailMessage); - - $markers['###SPLITTED_PREVIEW_LINK###'] = (string)$previewUriBuilder->buildUriForWorkspaceSplitPreview((int)$elementUid); - // Hook for preprocessing of the content for formmails: - foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'] ?? [] as $className) { - $_procObj = GeneralUtility::makeInstance($className); - $markers = $_procObj->postModifyMarkers($markers, $this); - } - // send an email to each individual user, to ensure the - // multilanguage version of the email - $emailRecipients = []; - // an array of language objects that are needed - // for emails with different languages - $languageObjects = [ - $this->getLanguageService()->lang => $this->getLanguageService() - ]; - // loop through each recipient and send the email - foreach ($emails as $recipientData) { - // don't send an email twice - if (isset($emailRecipients[$recipientData['email']])) { - continue; - } - $emailSubject = $emailConfig['subject']; - $emailMessage = $emailConfig['message']; - $emailRecipients[$recipientData['email']] = $recipientData['email']; - // check if the email needs to be localized - // in the users' language - if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:') || GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) { - $recipientLanguage = $recipientData['lang'] ?: 'default'; - if (!isset($languageObjects[$recipientLanguage])) { - // a LANG object in this language hasn't been - // instantiated yet, so this is done here - $languageObject = GeneralUtility::makeInstance(LanguageService::class); - $languageObject->init($recipientLanguage); - $languageObjects[$recipientLanguage] = $languageObject; - } else { - $languageObject = $languageObjects[$recipientLanguage]; - } - if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:')) { - $emailSubject = $languageObject->sL($emailSubject); - } - if (GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) { - $emailMessage = $languageObject->sL($emailMessage); - } - } - $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class); - $emailSubject = $templateService->substituteMarkerArray($emailSubject, $markers, '', true, true); - $emailMessage = $templateService->substituteMarkerArray($emailMessage, $markers, '', true, true); - // Send an email to the recipient - $mail = GeneralUtility::makeInstance(MailMessage::class); - $recipient = new Address($recipientData['email'], $recipientData['realName']); - $mail->to($recipient) - ->subject($emailSubject) - ->html($emailMessage); - $mail->send(); - } - $emailRecipients = implode(',', $emailRecipients); - if ($dataHandler->enableLogging) { - $propertyArray = $dataHandler->getRecordProperties($table, $id); - $pid = $propertyArray['pid']; - $dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Notification email for stage change was sent to "' . $emailRecipients . '"', -1, [], $dataHandler->eventPid($table, $id, $pid)); - } - } - } - - /** - * Return be_users that should be notified on stage change from input list. - * previously called notifyStageChange_getEmails() in DataHandler - * - * @param string $listOfUsers List of backend users, on the form "be_users_10,be_users_2" or "10,2" in case noTablePrefix is set. - * @param bool $noTablePrefix If set, the input list are integers and not strings. - * @return array Array of emails - */ - protected function getEmailsForStageChangeNotification($listOfUsers, bool $noTablePrefix = false): array - { - $users = GeneralUtility::trimExplode(',', $listOfUsers, true); - $emails = []; - foreach ($users as $userIdent) { - $table = ''; - if ($noTablePrefix) { - $id = (int)$userIdent; - } else { - [$table, $id] = GeneralUtility::revExplode('_', $userIdent, 2); - } - if ($table === 'be_users' || $noTablePrefix) { - if ($userRecord = BackendUtility::getRecord('be_users', $id, 'uid,email,lang,realName', BackendUtility::BEenableFields('be_users'))) { - if (trim($userRecord['email']) !== '') { - $emails[$id] = $userRecord; - } - } - } - } - return $emails; - } - /**************************** ***** Stage Changes ****** ****************************/ @@ -713,7 +505,7 @@ class DataHandlerHook $dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, SystemLogErrorClassification::USER_ERROR); } elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) { $record = BackendUtility::getRecord($table, $id); - $stat = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']); + $workspaceInfo = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']); // check if the user is allowed to the current stage, so it's also allowed to send to next stage if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($record['t3ver_stage'])) { // Set stage of record: @@ -734,10 +526,10 @@ class DataHandlerHook } // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere! $dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]); - if ((int)$stat['stagechg_notification'] > 0) { - $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$stat, $stageId, $comment]; - $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = $table . ':' . $id; - $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['alternativeRecipients'] = $notificationAlternativeRecipients; + if ((int)$workspaceInfo['stagechg_notification'] > 0) { + $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$workspaceInfo, $stageId, $comment]; + $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = [$table, $id]; + $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['recipients'] = $notificationAlternativeRecipients; } } else { $dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', SystemLogErrorClassification::USER_ERROR); @@ -1038,8 +830,8 @@ class DataHandlerHook $stageId = StagesService::STAGE_PUBLISH_EXECUTE_ID; $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment; $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment]; - $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = $table . ':' . $id; - $this->notificationEmailInfo[$notificationEmailInfoKey]['alternativeRecipients'] = $notificationAlternativeRecipients; + $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = [$table, $id]; + $this->notificationEmailInfo[$notificationEmailInfoKey]['recipients'] = $notificationAlternativeRecipients; // Write to log with stageId -20 (STAGE_PUBLISH_EXECUTE_ID) if ($dataHandler->enableLogging) { $propArr = $dataHandler->getRecordProperties($table, $id); diff --git a/typo3/sysext/workspaces/Classes/Notification/StageChangeNotification.php b/typo3/sysext/workspaces/Classes/Notification/StageChangeNotification.php new file mode 100644 index 0000000000000000000000000000000000000000..eb5fd8a64730c92c3ad3f8346dc9fc23195fb5cd --- /dev/null +++ b/typo3/sysext/workspaces/Classes/Notification/StageChangeNotification.php @@ -0,0 +1,153 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Workspaces\Notification; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Psr\Http\Message\ServerRequestInterface; +use Symfony\Component\Mime\Address; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Mail\FluidEmail; +use TYPO3\CMS\Core\Mail\Mailer; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Fluid\View\TemplatePaths; +use TYPO3\CMS\Workspaces\Preview\PreviewUriBuilder; +use TYPO3\CMS\Workspaces\Service\StagesService; + +/** + * Responsible for sending out emails when one or multiple records have been changed / sent to the next stage. + * + * Relevant options are "tx_workspaces.emails.*" via userTS / pageTS. + * + * @internal This is a concrete implementation of sending out emails, and not part of the public TYPO3 Core API + */ +class StageChangeNotification +{ + /** + * @var StagesService + */ + protected $stagesService; + + /** + * @var PreviewUriBuilder + */ + protected $previewUriBuilder; + + /** + * @var Mailer + */ + protected $mailer; + + public function __construct() + { + $this->stagesService = GeneralUtility::makeInstance(StagesService::class); + $this->previewUriBuilder = GeneralUtility::makeInstance(PreviewUriBuilder::class); + $this->mailer = GeneralUtility::makeInstance(Mailer::class); + } + + /** + * Send an email notification to users in workspace in multiple languages, depending on each BE users' langauge + * preference. + * + * @param array $workspaceRecord + * @param int $stageId Next Stage Number + * @param array $affectedElements List of element names (table / uid pairs) + * @param string $comment User comment sent along with action + * @param array $recipients List of recipients to notify, list is generated by workspace extension module + * @param BackendUserAuthentication $currentUser + */ + public function notifyStageChange(array $workspaceRecord, int $stageId, array $affectedElements, string $comment, array $recipients, BackendUserAuthentication $currentUser): void + { + [$elementTable, $elementUid] = reset($affectedElements); + $elementUid = (int)$elementUid; + $elementRecord = BackendUtility::getRecord($elementTable, $elementUid); + $recordTitle = BackendUtility::getRecordTitle($elementTable, $elementRecord); + $pageUid = $this->findFirstPageId($elementTable, $elementUid, $elementRecord); + + $emailConfig = BackendUtility::getPagesTSconfig($pageUid)['tx_workspaces.']['emails.'] ?? []; + $emailConfig = GeneralUtility::removeDotsFromTS($emailConfig); + $viewPlaceholders = [ + 'pageId' => $pageUid, + 'workspace' => $workspaceRecord, + 'rootLine' => BackendUtility::getRecordPath($pageUid, '', 20), + 'currentUser' => $currentUser->user, + 'additionalMessage' => $comment, + 'recordTitle' => $recordTitle, + 'affectedElements' => $affectedElements, + 'nextStage' => $this->stagesService->getStageTitle($stageId), + 'comparisonView' => (string)$this->previewUriBuilder->buildUriForWorkspaceSplitPreview($pageUid) + ]; + + if ($emailConfig['stageChangeNotification']['generatePreviewLink']) { + $viewPlaceholders['previewLink'] = $this->previewUriBuilder->buildUriForPage($pageUid, 0); + } + + $sentEmails = []; + foreach ($recipients as $recipientData) { + // don't send an email twice + if (in_array($recipientData['email'], $sentEmails, true)) { + continue; + } + $sentEmails[] = $recipientData['email']; + $this->sendEmail($recipientData, $emailConfig, $viewPlaceholders); + } + } + + /** + * As it is possible that multiple elements are sent out, or multiple pages, the first "real" page ID is found. + * + * @param string $elementTable the table of the first element found + * @param int $elementUid the uid of the first element in the list + * @param array $elementRecord the full record + * @return int the corresponding page ID + */ + protected function findFirstPageId(string $elementTable, int $elementUid, array $elementRecord): int + { + if ($elementTable === 'pages') { + return $elementUid; + } + BackendUtility::fixVersioningPid($elementTable, $elementRecord); + return (int)$elementRecord['pid']; + } + + /** + * Send one email to a specific person, apply multi-language possibilities for sending this email out. + * + * @param array $recipientData + * @param array $emailConfig + * @param array $variablesForView + */ + protected function sendEmail(array $recipientData, array $emailConfig, array $variablesForView): void + { + $templatePaths = new TemplatePaths(array_replace_recursive($GLOBALS['TYPO3_CONF_VARS']['MAIL'], $emailConfig)); + $emailObject = GeneralUtility::makeInstance(FluidEmail::class, $templatePaths); + $emailObject + ->to(new Address($recipientData['email'], $recipientData['realName'] ?? '')) + // Will be overridden by the template + ->subject('TYPO3 Workspaces: Stage Change') + ->setTemplate('StageChangeNotification') + ->assignMultiple($variablesForView) + ->assign('language', $recipientData['lang'] ?? 'default'); + + // Injecting normalized params + if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) { + $emailObject->setRequest($GLOBALS['TYPO3_REQUEST']); + } + if ($emailConfig['format']) { + $emailObject->format($emailConfig['format']); + } + $this->mailer->send($emailObject); + } +} diff --git a/typo3/sysext/workspaces/Classes/Service/StagesService.php b/typo3/sysext/workspaces/Classes/Service/StagesService.php index cbff406126c3942d7dea8619c9b8666ad5e0d9bd..765aee629b9692b7ce888a6b69795d3c88f3d621 100644 --- a/typo3/sysext/workspaces/Classes/Service/StagesService.php +++ b/typo3/sysext/workspaces/Classes/Service/StagesService.php @@ -36,9 +36,6 @@ class StagesService implements SingletonInterface // ready to publish stage const STAGE_PUBLISH_ID = -10; const STAGE_EDIT_ID = 0; - const MODE_NOTIFY_SOMEONE = 0; - const MODE_NOTIFY_ALL = 1; - const MODE_NOTIFY_ALL_STRICT = 2; /** * Path to the locallang file diff --git a/typo3/sysext/workspaces/Resources/Private/Language/locallang_emails.xlf b/typo3/sysext/workspaces/Resources/Private/Language/locallang_emails.xlf deleted file mode 100644 index 4914aa499ebffd73872a91e1e90773677b589f21..0000000000000000000000000000000000000000 --- a/typo3/sysext/workspaces/Resources/Private/Language/locallang_emails.xlf +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff"> - <file t3:id="1415815010" source-language="en" datatype="plaintext" original="EXT:workspaces/Resources/Private/Language/locallang_emails.xlf" date="2011-10-17T20:22:37Z" product-name="workspaces"> - <header/> - <body> - <trans-unit id="subject" resname="subject"> - <source>TYPO3 Workspace Note: Stage Change for ###ELEMENT_NAME###</source> - </trans-unit> - <trans-unit id="message" resname="message" xml:space="preserve"> - <source>At the TYPO3 site "###SITE_NAME###" (###SITE_URL###) -in workspace "###WORKSPACE_TITLE###" (###WORKSPACE_UID###) -the stage has changed for the element(s) "###RECORD_TITLE###" (###ELEMENT_NAME###) at location "###RECORD_PATH###" in the page tree: - -=> ###NEXT_STAGE### - -User Comment: -"###COMMENT###" - -State was changed by ###USER_FULLNAME### (username: ###USER_USERNAME###)</source> - </trans-unit> - </body> - </file> -</xliff> diff --git a/typo3/sysext/workspaces/Resources/Private/Templates/Email/StageChangeNotification.html b/typo3/sysext/workspaces/Resources/Private/Templates/Email/StageChangeNotification.html new file mode 100644 index 0000000000000000000000000000000000000000..37f220927dec5232615fc258dba92c89d856e7b0 --- /dev/null +++ b/typo3/sysext/workspaces/Resources/Private/Templates/Email/StageChangeNotification.html @@ -0,0 +1,35 @@ +<f:layout name="SystemEmail" /> +<f:section name="Subject">There are new changes in workspace "{workspace.title}"</f:section> +<f:section name="Title">{affectedElements -> f:count()} items sent to stage "{nextStage}" in workspace "{workspace.title}"</f:section> +<f:section name="Main"> + <p> + At the TYPO3 site "{typo3.sitename}" in workspace "{workspace.title}" ({workspace.uid}) + the stage has changed to "{nextStage}" for the element(s): + </p> + <ul> + <f:for each="{affectedElements}" as="element"> + <li>{element.0}:{element.1}</li> + </f:for> + </ul> + + <p> + The first entry was "{recordTitle}" at location <code>"{rootLine}"</code> in the page tree. + </p> + <p> + See the changes in the <a href="{comparisonView}">comparison view</a>. + </p> + + <f:if condition="{previewLink}"> + <p><a href="{previewLink}" rel="noopener">See a preview of the changed page.</a> + </f:if> + + <p> + The stage was changed by <strong>{currentUser.realName}</strong> ({currentUser.username}). + </p> + + <f:if condition="{additionalMessage}"> + <p> </p> + <h3>Additional comment</h3> + <p><f:format.nl2br>{additionalMessage}</f:format.nl2br></p> + </f:if> +</f:section> diff --git a/typo3/sysext/workspaces/Resources/Private/Templates/Email/StageChangeNotification.txt b/typo3/sysext/workspaces/Resources/Private/Templates/Email/StageChangeNotification.txt new file mode 100644 index 0000000000000000000000000000000000000000..60ff0a1a2f0e2619a6a014a1c4816f38ccf7673f --- /dev/null +++ b/typo3/sysext/workspaces/Resources/Private/Templates/Email/StageChangeNotification.txt @@ -0,0 +1,26 @@ +<f:layout name="SystemEmail" /> +<f:section name="Subject">There are new changes in workspace "{workspace.title}"</f:section> +<f:section name="Title">{affectedElements -> f:count()} items sent to stage "{nextStage}" in workspace "{workspace.title}"</f:section> +<f:section name="Main"> +At the TYPO3 site "{typo3.sitename}" in workspace "{workspace.title}" ({workspace.uid}) the stage has changed to "{nextStage}" for the element(s): + +<f:for each="{affectedElements}" as="element"> + *{element.0}:{element.1} +</f:for> + +The first entry was "{recordTitle}" at location "{rootLine}" in the page tree. + +See the changes in the comparison view {comparisonView}. +<f:if condition="{previewLink}"> +See a preview of the changed page here {previewLink} +</f:if> + +The stage was changed by <strong>{currentUser.realName}</strong> ({currentUser.username}). + +<f:if condition="{additionalMessage}"> + +Additional comment: + +<f:format.nl2br>{additionalMessage}</f:format.nl2br> +</f:if> +</f:section> diff --git a/typo3/sysext/workspaces/ext_localconf.php b/typo3/sysext/workspaces/ext_localconf.php index 2fed93507488c2abd01b335b376e58e4cb908a0b..f8ec77a8be60a9cc6e0a08e29ef0051cef000d2f 100644 --- a/typo3/sysext/workspaces/ext_localconf.php +++ b/typo3/sysext/workspaces/ext_localconf.php @@ -3,9 +3,11 @@ defined('TYPO3_MODE') or die(); // add default notification options to every page \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig(' -tx_version.workspaces.stageNotificationEmail.subject = LLL:EXT:workspaces/Resources/Private/Language/locallang_emails.xlf:subject -tx_version.workspaces.stageNotificationEmail.message = LLL:EXT:workspaces/Resources/Private/Language/locallang_emails.xlf:message -# tx_version.workspaces.stageNotificationEmail.additionalHeaders = +tx_workspaces.emails.stageChangeNotification.generatePreviewLink = 0 +tx_workspaces.emails.layoutRootPaths.90 = EXT:workspaces/Resources/Private/Layouts/ +tx_workspaces.emails.partialRootPaths.90 = EXT:workspaces/Resources/Private/Partials/ +tx_workspaces.emails.templateRootPaths.90 = EXT:workspaces/Resources/Private/Templates/Email/ +tx_workspaces.emails.format = html '); // register the hook to actually do the work within DataHandler