diff --git a/CHANGELOG.md b/CHANGELOG.md index 3df67288..01eba9f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- [#56](https://github.com/OS2Forms/os2forms/pull/56) + Handled anonymous users in notifications and flow tasks + ## [3.10.0] 2023-08-23 - [OSF-55] DAWA Address-Matrikula (autocomplete) (required) diff --git a/composer.json b/composer.json index cbb71254..08d5ed51 100644 --- a/composer.json +++ b/composer.json @@ -67,6 +67,7 @@ "drupal/webform_validation": "^2.0", "drupal/webform_views": "^5.0@alpha", "drupal/workflow_participants": "^2.4", + "os2forms/os2forms_digital_post": "^3.0", "os2web/os2web_datalookup": "^1.0", "os2web/os2web_nemlogin": "^1.0", "phpoffice/phpword": "^0.18.2", diff --git a/modules/os2forms_forloeb/CHANGELOG.md b/modules/os2forms_forloeb/CHANGELOG.md index f985467c..0ab070ac 100644 --- a/modules/os2forms_forloeb/CHANGELOG.md +++ b/modules/os2forms_forloeb/CHANGELOG.md @@ -4,15 +4,19 @@ All notable changes to this project should be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -See ["how do I make a good changelog record?"](https://keepachangelog.com/en/1.0.0/#how) +See ["how do I make a good changelog record?"](https://keepachangelog.com/en/1.0.0/#how) before starting to add changes. ## [Unreleased] +- Implemented `hook_maestro_zero_user_notification` and added *Maestro + notification* handler for sending notifications via email or digital post. +- Cleaned up prefilling of forms in `MaestroWebformInheritTask`. + ## 2.5.2 - 27.03.2023 ### Updated -- Bumped drupal/ultimate_cron version fixing [Deprecated function: Implicit conversion from float-string](https://www.drupal.org/project/ultimate_cron/issues/3256142). +- Bumped drupal/ultimate_cron version fixing [Deprecated function: Implicit conversion from float-string](https://www.drupal.org/project/ultimate_cron/issues/3256142). ## 2.5.1 - 10.03.2023 - Added github action for checking changelog changes when creating pull requests @@ -24,11 +28,11 @@ before starting to add changes. ## 2.5.0 - 11.10.2022 -### Added +### Added - retry task controller action - Added support for inheriting values without creating a submission -## 2.4.0 +## 2.4.0 ### Added - Github CI action for checking Drupal Coding standards with PHP Code Sniffer diff --git a/modules/os2forms_forloeb/README.md b/modules/os2forms_forloeb/README.md index 8ebcf2c7..e1e0ec3c 100644 --- a/modules/os2forms_forloeb/README.md +++ b/modules/os2forms_forloeb/README.md @@ -1,10 +1,56 @@ # OS2forms 2.1 med Forløb [![Build Status](https://app.travis-ci.com/OS2Forms/os2forms_forloeb.svg?branch=develop)](https://app.travis-ci.com/OS2Forms/os2forms_forloeb) + Adds a Maestro workflow engine and advanced workflow functionality to OS2forms. ## Installing OS2forms 2.1 med Forløb + This module requires the codebase from the [OS2forms core project](https://github.com/OS2Forms/os2forms8) installed per the documentation and by selecting the os2forms_forloeb_profile at installation. After succesful installation you should have the OS2forms med Forløb Module available for install via gui. You can also install the module by using Drush: - ``` - ./vendor/bin/drush en os2forms_forloeb - ``` + +``` +./vendor/bin/drush pm:enable os2forms_forloeb +``` + +------------------------------------------------------------------------------- + +## Maestro notifications + +Maestro 3.1 adds a `hook_webform_submission_form_alter` hook which we utilize to +send assignment, reminder and escalation notifications by adding a *Maestro +notification* handler to a form that spawns a Maestro workflow or assigns a +task. If the notification recipient is identified by an an email address, the +notification is sent as an email, and if the identifier is a Danish CPR number, +the notifications is sent as digital post. + +See [Opret flow-notifikationer](https://os2forms.os2.eu/node/457) (in Danish) +for details. + +### Settings + +Settings for OS2Forms forløb are defined on `/admin/config/system/os2forms_forloeb`. + +#### Known anonymous roles + +In order to make the notifications work, Maestro workflow tasks must be assigned +to a *known anonymous role* and these roles are defined under *Known anonymous +roles*. + +#### Processing + +A notification is not sent to a user immediately, but added to a queue which +must be processed asynchronously. Specify the queue handling notification jobs. + +#### Templates + +Define templates for emails and digital post (PDF). + +### Note on digital post + +Digital post is sent using the API provided by the [OS2Forms Digital Post +module](https://github.com/itk-dev/os2forms_digital_post) +(`os2forms_digital_post`) which in turn uses [SF1600: Print på +serviceplatformen](https://digitaliseringskataloget.dk/integration/sf1600). Not +all OS2Forms projects use `os2forms_digital_post` and in the future we should +generalize the API for sending digital post to allow other implementations (not +based on [SF1600](https://digitaliseringskataloget.dk/integration/sf1600)). diff --git a/modules/os2forms_forloeb/os2forms_forloeb.info.yml b/modules/os2forms_forloeb/os2forms_forloeb.info.yml index b82b723d..437899ee 100644 --- a/modules/os2forms_forloeb/os2forms_forloeb.info.yml +++ b/modules/os2forms_forloeb/os2forms_forloeb.info.yml @@ -2,8 +2,9 @@ name: 'OS2forms med Forløb Module' description: 'Adds a Maestro workflow engine and advanced workflow functionality to OS2forms.' package: OS2Forms type: module -core: 8.x -core_version_requirement: ^8 || ^9 +core_version_requirement: ^9 || ^10 + +configure: os2forms_forloeb.settings dependencies: - 'drupal:admin_toolbar_tools' @@ -51,5 +52,6 @@ dependencies: - 'drupal:webform_submission_log' - 'drupal:webform_templates' - 'drupal:workflow_participants' + - 'os2forms_digital_post:os2forms_digital_post' 'interface translation project': os2forms_forloeb 'interface translation server pattern': modules/contrib/os2forms_forloeb/translations/os2forms_forloeb.da.po diff --git a/modules/os2forms_forloeb/os2forms_forloeb.links.menu.yml b/modules/os2forms_forloeb/os2forms_forloeb.links.menu.yml new file mode 100644 index 00000000..a561d495 --- /dev/null +++ b/modules/os2forms_forloeb/os2forms_forloeb.links.menu.yml @@ -0,0 +1,5 @@ +os2forms_forloeb.settings: + title: OS2Forms forløb + description: Configure the OS2Forms forløb module + parent: system.admin_config_system + route_name: os2forms_forloeb.settings diff --git a/modules/os2forms_forloeb/os2forms_forloeb.module b/modules/os2forms_forloeb/os2forms_forloeb.module index 4c5fac0d..1878073a 100644 --- a/modules/os2forms_forloeb/os2forms_forloeb.module +++ b/modules/os2forms_forloeb/os2forms_forloeb.module @@ -5,18 +5,13 @@ * Install, update and uninstall functions for the os2forms_forloeb. */ -use Drupal\Core\Access\AccessResult; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Render\BubbleableMetadata; -use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Url; use Drupal\maestro\Engine\MaestroEngine; +use Drupal\os2forms_forloeb\MaestroHelper; use Drupal\os2forms_forloeb\Plugin\EngineTasks\MaestroWebformInheritTask; use Drupal\user\Entity\User; use Drupal\webform\Entity\WebformSubmission; use Drupal\webform\WebformInterface; -use Drupal\webform\WebformSubmissionInterface; /** * Implements hook_maestro_interactive_handlers(). @@ -289,66 +284,93 @@ function os2forms_forloeb_preprocess_page(&$variables) { } /** - * Implements hook_token_info_alter(). + * Implements hook_webform_submission_form_alter(). */ -function os2forms_forloeb_token_info_alter(&$data) { - $data['tokens']['webform_submission']['os2forms_forloeb_execute_task'] = [ - 'name' => t('Execute task path for webform submission'), - 'description' => t("The token that can be user to get path for webform submission redirect URL."), - 'type' => 'webform_submission', - ]; +function os2forms_forloeb_webform_submission_form_alter(array &$form, FormStateInterface $formState, string $formId) { + MaestroWebformInheritTask::webformSubmissionFormAlter($form, $formState, $formId); +} + +/** + * Implements hook_maestro_zero_user_notification(). + */ +function os2forms_forloeb_maestro_zero_user_notification($templateMachineName, $taskMachineName, $queueID, $notificationType) { + _os2forms_forloeb_helper()->maestroZeroUserNotification($templateMachineName, $taskMachineName, $queueID, $notificationType); } /** - * Implements hook_tokens(). + * Implements hook_maestro_can_user_execute_task_alter(). * - * Provides token value for webform_submission:os2forms_forloeb_execute_task. + * For OS2Forms, you may have a consistent assignment to an "anonymous" user via + * a role. Use the QueueID and userID to drill into the task and alter the + * returnValue to TRUE if this is a user that should be looking at this task. + * + * You can make this as complex as you'd like it to be, checking things like + * sessions, login tokens, email addresses etc. */ -function os2forms_forloeb_tokens($type, array $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { - $replacements = []; - - if ($type === 'webform_submission' && !empty($data['webform_submission']) && isset($tokens['os2forms_forloeb_execute_task'])) { - $replacements[$tokens['os2forms_forloeb_execute_task']] = Url::fromRoute( - 'os2forms_forloeb.forloeb_task_console_controller_execute', - ['os2forms-forloeb-ws-token' => $data['webform_submission']->getToken()], - ['absolute' => TRUE] - )->toString(TRUE)->getGeneratedUrl(); - } +function os2forms_forloeb_maestro_can_user_execute_task_alter(&$returnValue, $queueID, $userID) { + _os2forms_forloeb_helper()->maestroCanUserExecuteTaskAlter($returnValue, $queueID, $userID); +} - return $replacements; +/** + * Implements hook_mail(). + */ +function os2forms_forloeb_mail($key, &$message, $params) { + _os2forms_forloeb_helper()->mail($key, $message, $params); } /** - * Implements hook_entity_access(). - * - * Allows requests with tokens to view the entity. + * Implements hook_mail_alter(). */ -function os2forms_forloeb_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { - if ($operation == 'update' && $entity instanceof WebformSubmission) { - $token = \Drupal::request()->query->get('os2forms-forloeb-ws-token'); - if ($token && $token === $entity->getToken()) { - return AccessResult::allowed(); - } - } - return AccessResult::neutral(); +function os2forms_forloeb_mail_alter(&$message) { + _os2forms_forloeb_helper()->mailAlter($message); } /** - * Implements hook_maestro_post_fetch_assigned_queue_tasks(). + * Implements hook_theme(). */ -function os2forms_forloeb_maestro_post_fetch_assigned_queue_tasks($userID, &$queueIDs) { - $token = \Drupal::request()->query->get('os2forms-forloeb-ws-token', ''); - if ($token) { - $forloebTaskConsole = Drupal::service('os2forms_forloeb.task_console'); - $queueRecord = $forloebTaskConsole->getQueueIdByWebformSubmissionToken($token); - $queueIDs[] = $queueRecord->id(); - $queueIDs = array_unique($queueIDs); - } +function os2forms_forloeb_theme(array &$variables) { + $theme['os2forms_forloeb_notification_preview'] = [ + 'variables' => [ + 'webform' => NULL, + 'handler' => NULL, + 'notification_type' => NULL, + 'subject' => NULL, + 'recipient' => NULL, + 'content_type' => NULL, + 'submission' => NULL, + 'return_url' => NULL, + 'render_url' => NULL, + 'preview_urls' => [ + 'prev' => NULL, + 'self' => NULL, + 'next' => NULL, + ], + ], + ]; + + $theme['os2forms_forloeb_notification_message_email_html'] = [ + 'variables' => [ + 'message' => [ + 'content' => [ + 'value' => NULL, + 'format' => NULL, + ], + ], + 'task_url' => NULL, + 'action_label' => NULL, + 'webform_submission' => NULL, + 'handler' => NULL, + ], + ]; + + $theme['os2forms_forloeb_notification_message_pdf_html'] = $theme['os2forms_forloeb_notification_message_email_html']; + + return $theme; } /** - * Implements hook_ENTITY_TYPE_prepare_form(). + * Get MaestroHelper. */ -function os2forms_forloeb_webform_submission_prepare_form(WebformSubmissionInterface $webform_submission, string $operation, FormStateInterface $form_state) { - MaestroWebformInheritTask::webformSubmissionPrepareForm($webform_submission, $operation, $form_state); +function _os2forms_forloeb_helper(): MaestroHelper { + return Drupal::service(MaestroHelper::class); } diff --git a/modules/os2forms_forloeb/os2forms_forloeb.routing.yml b/modules/os2forms_forloeb/os2forms_forloeb.routing.yml index 8304af4d..a51f54e5 100644 --- a/modules/os2forms_forloeb/os2forms_forloeb.routing.yml +++ b/modules/os2forms_forloeb/os2forms_forloeb.routing.yml @@ -1,19 +1,42 @@ -os2forms_forloeb.forloeb_task_console_controller_execute: - path: 'os2forms-forloeb/execute-task' +os2forms_forloeb.settings: + path: '/admin/config/system/os2forms_forloeb' defaults: - _controller: '\Drupal\os2forms_forloeb\Controller\ForloebTaskConsoleController::execute' - _title: 'Execute task' + _form: '\Drupal\os2forms_forloeb\Form\SettingsForm' + _title: 'OS2Forms forløb' requirements: - _permission: 'access content' - options: - no_cache: TRUE + _permission: 'administer site configuration' -os2forms_forloeb.forloeb_task_console_controller_execute_retry: - path: 'os2forms-forloeb/execute-task-retry' +os2forms_forloeb.meastro_notification.preview: + path: '/admin/structure/webform/manage/{webform}/os2forms_forloeb/notification/{handler}/preview/{notification_type}/{content_type}' defaults: - _controller: '\Drupal\os2forms_forloeb\Controller\ForloebTaskConsoleController::retry' - _title: 'Task not yet ready' + _controller: '\Drupal\os2forms_forloeb\Controller\MaestroNotificationController::preview' + _title: 'Maestro notification preview' + notification_type: assignment + options: + parameters: + webform: + type: 'entity:webform' requirements: - _permission: 'access content' + _permission: 'view any webform submission' + +os2forms_forloeb.meastro_notification.preview_render: + path: '/admin/structure/webform/manage/{webform}/os2forms_forloeb/notification/{handler}/preview/{notification_type}/{content_type}/render/{submission}' + defaults: + _controller: '\Drupal\os2forms_forloeb\Controller\MaestroNotificationController::previewRender' + _title: 'Maestro notification render preview' options: - no_cache: TRUE + parameters: + webform: + type: 'entity:webform' + submission: + type: 'entity:webform_submission' + requirements: + _permission: 'view any webform submission' + +os2forms_forloeb.meastro_notification.preview_message: + path: '/os2forms_forloeb/notification/message' + defaults: + _controller: '\Drupal\os2forms_forloeb\Controller\MaestroNotificationController::message' + _title: 'Maestro notification message' + requirements: + _permission: 'view any webform submission' diff --git a/modules/os2forms_forloeb/os2forms_forloeb.services.yml b/modules/os2forms_forloeb/os2forms_forloeb.services.yml index eb0fa89e..2d2b73ca 100644 --- a/modules/os2forms_forloeb/os2forms_forloeb.services.yml +++ b/modules/os2forms_forloeb/os2forms_forloeb.services.yml @@ -2,6 +2,21 @@ services: logger.channel.os2forms_forloeb: parent: logger.channel_base arguments: ['os2forms_forloeb'] - os2forms_forloeb.task_console: - class: Drupal\os2forms_forloeb\ForloebTaskConsole - arguments: ['@entity_type.manager', '@logger.channel.os2forms_forloeb'] + + logger.channel.os2forms_forloeb_submission: + parent: logger.channel_base + arguments: ['webform_submission'] + + Drupal\os2forms_forloeb\MaestroHelper: + arguments: + - '@entity_type.manager' + - '@config.factory' + - '@webform.token_manager' + - '@plugin.manager.mail' + - '@language_manager' + - '@webform.theme_manager' + - '@logger.channel.os2forms_forloeb' + - '@logger.channel.os2forms_forloeb_submission' + - '@module_handler' + - '@plugin.manager.entity_print.print_engine' + - '@Drupal\os2forms_digital_post\Helper\DigitalPostHelper' diff --git a/modules/os2forms_forloeb/src/Controller/ForloebTaskConsoleController.php b/modules/os2forms_forloeb/src/Controller/ForloebTaskConsoleController.php deleted file mode 100644 index 8f2e95b0..00000000 --- a/modules/os2forms_forloeb/src/Controller/ForloebTaskConsoleController.php +++ /dev/null @@ -1,177 +0,0 @@ -forloebTaskConsole = $forloeb_task_console; - $this->entityTypeManager = $entity_type_manager; - $this->requestStack = $request_stack; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('os2forms_forloeb.task_console'), - $container->get('entity_type.manager'), - $container->get('request_stack') - ); - } - - /** - * Redirects to the task execution URL. - * - * In case it's not possible to define task, redirects to task console. - * - * @return \Symfony\Component\HttpFoundation\RedirectResponse - * Redirect object. - */ - public function execute() { - $redirect_to = Url::fromRoute('maestro_taskconsole.taskconsole'); - - $request = $this->requestStack->getCurrentRequest(); - // Check webform submission token. - $token = $request->query->get('os2forms-forloeb-ws-token', ''); - if ($token) { - $queueRecord = $this->forloebTaskConsole->getQueueIdByWebformSubmissionToken($token); - if (empty($queueRecord)) { - return new RedirectResponse( - Url::fromRoute('os2forms_forloeb.forloeb_task_console_controller_execute_retry', - ['referrer' => $request->getRequestUri()])->toString() - ); - } - } - else { - // For empty token there is user last task from taskconsole queue. - $queueIDs = MaestroEngine::getAssignedTaskQueueIds($this->currentUser()->id()); - $queueRecord = count($queueIDs) ? $this->entityTypeManager->getStorage('maestro_queue')->load(end($queueIDs)) : NULL; - - // In case there are more than 1 task warning message will be shown. - if (count($queueIDs) > 1) { - $this->messenger()->addWarning($this->t('You have @amount @tasks available for you. See list of the all tasks on taskconsole', [ - ':tasksonsole' => Url::fromRoute('maestro_taskconsole.taskconsole')->toString(), - '@amount' => count($queueIDs), - '@tasks' => new PluralTranslatableMarkup(count($queueIDs), 'task', 'tasks'), - ])); - } - } - - if (empty($queueRecord)) { - $this->messenger()->addWarning($this->t('No tasks found to execute.')); - return new RedirectResponse($redirect_to->toString()); - } - - // Processing QueueRecord to get execution URL to redirect to. - $handler = $queueRecord->handler->getString(); - $query_options = [ - 'queueid' => $queueRecord->id(), - 'modal' => 'notmodal', - ]; - - // As inspiration MaestroTaskConsoleController::getTasks() method was used. - if ($handler && !empty($handler) && $queueRecord->is_interactive->getString() == '1') { - global $base_url; - $handler = str_replace($base_url, '', $handler); - $handler_type = TaskHandler::getType($handler); - - $handler_url_parts = UrlHelper::parse($handler); - $query_options += $handler_url_parts['query']; - - } - elseif ($queueRecord->is_interactive->getString() == '1' && empty($handler)) { - // Handler is empty. - // If this is an interactive task and has no handler, we're still OK. - // This is an interactive function that uses a default handler then. - $handler_type = 'function'; - } - else { - $this->messenger()->addWarning($this->t('Undefined handler')); - } - - switch ($handler_type) { - case 'external': - $redirect_to = Url::fromUri($handler, ['query' => $query_options]); - break; - - case 'internal': - $redirect_to = Url::fromUserInput($handler, ['query' => $query_options]); - break; - - case 'function': - if ($token) { - $query_options['os2forms-forloeb-ws-token'] = $token; - } - $redirect_to = Url::fromRoute('maestro.execute', $query_options); - break; - } - - return new RedirectResponse($redirect_to->toString()); - } - - /** - * Show message about task not yet ready. - * - * @return array - * The render array. - */ - public function retry() { - $referrer = $this->requestStack->getCurrentRequest()->query->get('referrer', '#'); - - return [ - '#markup' => $this->t('Your task is not yet ready. Please try again in 5 minutes.', [':referrer' => $referrer]), - ]; - } - -} diff --git a/modules/os2forms_forloeb/src/Controller/MaestroNotificationController.php b/modules/os2forms_forloeb/src/Controller/MaestroNotificationController.php new file mode 100644 index 00000000..1264d730 --- /dev/null +++ b/modules/os2forms_forloeb/src/Controller/MaestroNotificationController.php @@ -0,0 +1,135 @@ +get('entity_type.manager')->getStorage('webform_submission'), + $container->get(MaestroHelper::class) + ); + } + + /** + * Preview action. + */ + public function preview(Request $request, WebformInterface $webform, string $handler, string $notification_type, string $content_type, RouteMatchInterface $routeMatch) { + $handler = $webform->getHandler($handler); + $submissionIds = array_keys($this->webformSubmissionStorage->getQuery() + ->condition('webform_id', $webform->id()) + ->sort('created', 'DESC') + ->execute()); + $currentSubmission = (int) $request->query->get('submission'); + $index = array_search($currentSubmission, $submissionIds); + if (FALSE === $index) { + $currentSubmission = reset($submissionIds) ?: NULL; + $index = array_search($currentSubmission, $submissionIds); + } + + $previewUrls = array_map( + static fn ($submission) => Url::fromRoute('os2forms_forloeb.meastro_notification.preview', [ + 'webform' => $webform->id(), + 'handler' => $handler->getHandlerId(), + 'content_type' => $content_type, + 'submission' => $submission, + ]), + array_filter([ + 'prev' => $submissionIds[$index + 1] ?? NULL, + 'self' => $currentSubmission, + 'next' => $submissionIds[$index - 1] ?? NULL, + ]) + ); + + $renderUrl = NULL !== $currentSubmission + ? Url::fromRoute('os2forms_forloeb.meastro_notification.preview_render', [ + 'webform' => $webform->id(), + 'handler' => $handler->getHandlerId(), + 'notification_type' => $notification_type, + 'content_type' => $content_type, + 'submission' => $currentSubmission, + ]) + : NULL; + + $submission = $this->webformSubmissionStorage->load($currentSubmission); + $templateTask = []; + $maestroQueueID = 0; + [ + 'recipient' => $recipient, + 'subject' => $subject, + ] = $this->maestroHelper->renderNotification($submission, $handler->getHandlerId(), $notification_type, $templateTask, $maestroQueueID, $content_type); + + return [ + '#theme' => 'os2forms_forloeb_notification_preview', + '#webform' => $webform, + '#handler' => $handler, + '#notification_type' => $notification_type, + '#subject' => $subject, + '#recipient' => $recipient, + '#content_type' => $content_type, + '#submission' => $currentSubmission, + '#return_url' => $webform->toUrl('handlers'), + '#render_url' => $renderUrl, + '#preview_urls' => $previewUrls, + ]; + } + + /** + * Render notification preview. + */ + public function previewRender(Request $request, WebformInterface $webform, string $handler, string $notification_type, string $content_type, WebformSubmissionInterface $submission) { + $templateTask = []; + $maestroQueueID = 0; + [ + 'content' => $content, + 'contentType' => $contentType, + ] = $this->maestroHelper->renderNotification($submission, $handler, $notification_type, $templateTask, $maestroQueueID, $content_type); + + $response = new Response($content); + if ('pdf' === $contentType) { + $response->headers->set('content-type', Document::MIME_TYPE_PDF); + } + + return $response; + } + + /** + * Message action. + */ + public function message(Request $request): Response { + $content[] = '

' . $request->get('message') . '

'; + if ($referer = $request->headers->get('referer')) { + $content[] = sprintf('Go back', $referer); + } + + return new Response(implode(PHP_EOL, $content)); + } + +} diff --git a/modules/os2forms_forloeb/src/Exception/RuntimeException.php b/modules/os2forms_forloeb/src/Exception/RuntimeException.php new file mode 100644 index 00000000..004049d8 --- /dev/null +++ b/modules/os2forms_forloeb/src/Exception/RuntimeException.php @@ -0,0 +1,10 @@ +get('config.factory'), + $container->get('entity_type.manager')->getStorage('user_role'), + $container->get('entity_type.manager')->getStorage('advancedqueue_queue'), + $container->get('extension.list.module'), + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'os2forms_forloeb_config'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return [ + self::SETTINGS, + ]; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config(static::SETTINGS); + + $roles = $this->roleStorage->loadMultiple(); + $form['known_anonymous_roles'] = [ + '#title' => $this->t('Known anonymous roles'), + '#type' => 'checkboxes', + '#options' => array_map(static fn (RoleInterface $role) => $role->label(), $roles), + '#default_value' => $config->get('known_anonymous_roles') ?: [], + '#description' => $this->t('Roles that can act as “known anonymous”'), + ]; + + $form['processing'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Processing'), + '#tree' => TRUE, + ]; + + $defaultValue = $config->get('processing')['queue'] ?? NULL; + $form['processing']['queue'] = [ + '#type' => 'select', + '#required' => TRUE, + '#title' => $this->t('Queue'), + '#options' => array_map( + static fn(EntityInterface $queue) => $queue->label(), + $this->queueStorage->loadMultiple() + ), + '#default_value' => $defaultValue, + '#description' => $this->t("Queue for handling notification jobs. The queue must be run via Drupal's cron or via drush advancedqueue:queue:process @queue (in a cron job).", [ + '@queue' => $defaultValue, + ':queue_url' => '/admin/config/system/queues/jobs/' . urlencode($defaultValue ?? ''), + ]), + ]; + + $form['templates'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Templates'), + '#tree' => TRUE, + ]; + + $templatePath = $this->moduleHandler->getPath('os2forms_forloeb') . '/templates/os2forms-forloeb-notification-message-email-html.html.twig'; + $defaultTemplate = file_exists($templatePath) ? file_get_contents($templatePath) : NULL; + $form['templates']['notification_email'] = [ + '#type' => 'textarea', + '#rows' => 20, + '#required' => TRUE, + '#title' => $this->t('Email template'), + '#default_value' => $config->get('templates')['notification_email'] ?? $defaultTemplate, + '#description' => $this->t('HTML template for email notifications. If the template is a path, e.g. @templatePath, the template will be loaded from this path.', [ + '@templatePath' => $templatePath, + ]), + ]; + + $templatePath = $this->moduleHandler->getPath('os2forms_forloeb') . '/templates/os2forms-forloeb-notification-message-pdf-html.html.twig'; + $defaultTemplate = file_exists($templatePath) ? file_get_contents($templatePath) : NULL; + $form['templates']['notification_pdf'] = [ + '#type' => 'textarea', + '#rows' => 20, + '#required' => TRUE, + '#title' => $this->t('PDF template'), + '#default_value' => $config->get('templates')['notification_pdf'] ?? $defaultTemplate, + '#description' => $this->t('HTML template for PDF notifications (digital post). If the template is a path, e.g. @templatePath, the template will be loaded from this path.', [ + '@templatePath' => $templatePath, + ]), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $formState) { + $this->config(static::SETTINGS) + ->set('known_anonymous_roles', $formState->getValue('known_anonymous_roles')) + ->set('processing', $formState->getValue('processing')) + ->set('templates', $formState->getValue('templates')) + ->save(); + + parent::submitForm($form, $formState); + } + +} diff --git a/modules/os2forms_forloeb/src/MaestroHelper.php b/modules/os2forms_forloeb/src/MaestroHelper.php new file mode 100644 index 00000000..5f8a16f4 --- /dev/null +++ b/modules/os2forms_forloeb/src/MaestroHelper.php @@ -0,0 +1,614 @@ +config = $configFactory->get(SettingsForm::SETTINGS); + $this->webformSubmissionStorage = $entityTypeManager->getStorage('webform_submission'); + $this->queueStorage = $entityTypeManager->getStorage('advancedqueue_queue'); + } + + /** + * Implements hook_maestro_zero_user_notification(). + */ + public function maestroZeroUserNotification($templateMachineName, $taskMachineName, $queueID, $notificationType) { + // @todo Clean up and align with MaestroWebformInheritTask::webformSubmissionFormAlter(). + $templateTask = MaestroEngine::getTemplateTaskByID($templateMachineName, $taskMachineName); + if (MaestroWebformInheritTask::isWebformTask($templateTask)) { + if ($inheritWebformUniqueId = ($templateTask['data'][MaestroWebformInheritTask::INHERIT_WEBFORM_UNIQUE_ID] ?? NULL)) { + if ($processID = (MaestroEngine::getProcessIdFromQueueId($queueID) ?: NULL)) { + if ($entityIdentifier = (self::getWebformSubmissionIdentifiersForProcess($processID)[$inheritWebformUniqueId] ?? NULL)) { + $submission = $this->webformSubmissionStorage->load($entityIdentifier['entity_id']); + if ($submission) { + $this->handleSubmissionNotification($notificationType, $submission, $templateTask, $queueID); + } + } + } + } + } + } + + /** + * Get webform submission identifiers for a process. + * + * @param int $processID + * The Maestro Process ID. + * + * @return array + * The webform submission identifiers sorted ascendingly by creation time. + */ + public static function getWebformSubmissionIdentifiersForProcess(int $processID): array { + // Get webform submissions in process. + $entityIdentifiers = array_filter( + MaestroEngine::getAllEntityIdentifiersForProcess($processID), + static fn (array $entityIdentifier) => 'webform_submission' === ($entityIdentifier['entity_type'] ?? NULL) + ); + + // Sort by entity ID. + uasort($entityIdentifiers, static fn (array $a, array $b) => ($b['entity_id'] ?? 0) <=> ($a['entity_id'] ?? 0)); + + return $entityIdentifiers; + } + + /** + * Handle submission notification. + */ + private function handleSubmissionNotification( + string $notificationType, + WebformSubmissionInterface $submission, + array $templateTask, + int $maestroQueueID + ): ?Job { + $context = [ + 'webform_submission' => $submission, + ]; + + try { + $job = Job::create(SendMeastroNotification::class, [ + 'notificationType' => $notificationType, + 'templateTask' => $templateTask, + 'queueID' => $maestroQueueID, + 'submissionID' => $submission->id(), + 'webformID' => $submission->getWebform()->id(), + ]); + + $queue = $this->loadQueue(); + $queue->enqueueJob($job); + $context['@queue'] = $queue->id(); + $this->notice('Job for sending notification added to the queue @queue.', $context + [ + 'handler_id' => 'os2forms_forloeb', + 'operation' => 'notification queued for sending', + ]); + + return $job; + } + catch (\Exception $exception) { + $this->error('Error creating job for sending notification: @message', $context + [ + '@message' => $exception->getMessage(), + 'handler_id' => 'os2forms_forloeb', + 'operation' => 'notification failed', + 'exception' => $exception, + ]); + + return NULL; + } + } + + /** + * Process a job. + */ + public function processJob(Job $job): JobResult { + $payload = $job->getPayload(); + [ + 'notificationType' => $notificationType, + 'templateTask' => $templateTask, + 'queueID' => $maestroQueueID, + 'submissionID' => $submissionID, + ] = $payload; + + $submission = $this->webformSubmissionStorage->load($submissionID); + + $this->sendNotification($notificationType, $submission, $templateTask, $maestroQueueID); + + return JobResult::success(); + } + + /** + * Send notification. + */ + private function sendNotification( + string $notificationType, + WebformSubmissionInterface $submission, + array $templateTask, + int $maestroQueueID + ) { + $context = [ + 'webform_submission' => $submission, + ]; + + try { + $handlers = $submission->getWebform()->getHandlers(); + foreach ($handlers as $handler) { + if (!($handler instanceof MaestroNotificationHandler) + || $handler->isDisabled() + || $handler->isExcluded() + || !$handler->isNotificationEnabled($notificationType) + ) { + continue; + } + + [ + 'content' => $content, + 'contentType' => $contentType, + 'recipient' => $recipient, + 'subject' => $subject, + 'taskUrl' => $taskUrl, + 'actionLabel' => $actionLabel, + ] = $this->renderNotification($submission, $handler->getHandlerId(), $notificationType, $templateTask, $maestroQueueID); + + if ('email' === $contentType) { + $this->sendNotificationEmail($recipient, $subject, $content, $submission, $notificationType); + } + else { + $this->sendNotificationDigitalPost($recipient, $subject, $content, $taskUrl, $actionLabel, $submission, $notificationType); + } + } + } + catch (\Exception $exception) { + $this->error('Error sending notification: @message', $context + [ + '@message' => $exception->getMessage(), + 'handler_id' => 'os2forms_forloeb', + 'operation' => 'notification failed', + 'exception' => $exception, + ]); + + return NULL; + } + } + + /** + * Load advanced queue if any. + * + * @return \Drupal\advancedqueue\Entity\QueueInterface + * The queue. + */ + private function loadQueue(): QueueInterface { + $queueId = $this->config->get('processing')['queue'] ?? NULL; + + if (NULL === $queueId) { + throw new RuntimeException('Cannot get queue ID'); + } + + $queue = $this->queueStorage->load($queueId); + if (NULL === $queue) { + throw new RuntimeException(sprintf('Cannot load queue %s', $queueId)); + } + + return $queue; + } + + /** + * Send notification email. + */ + private function sendNotificationEmail( + string $recipient, + string $subject, + string $body, + WebformSubmissionInterface $submission, + string $notificationType + ): void { + try { + $message = [ + 'subject' => $subject, + 'body' => $body, + 'html' => TRUE, + ]; + + $langcode = $this->languageManager->getCurrentLanguage()->getId(); + + $result = $this->mailManager->mail( + 'os2forms_forloeb', + 'notification', + $recipient, + $langcode, + $message + ); + + if (!$result['result']) { + throw new RuntimeException(sprintf('Error sending notification (%s) email to %s', $notificationType, $recipient)); + } + + $this->notice('Email notification (@type) sent to @recipient', [ + '@type' => $notificationType, + 'webform_submission' => $submission, + '@recipient' => $recipient, + 'handler_id' => 'os2forms_forloeb', + 'operation' => 'notification sent', + ]); + } + catch (\Exception $exception) { + $this->error('Error sending email notification (@type): @message', [ + '@type' => $notificationType, + '@message' => $exception->getMessage(), + 'webform_submission' => $submission, + 'handler_id' => 'os2forms_forloeb', + 'operation' => 'failed sending notification', + ]); + } + } + + /** + * Send notification digital post. + */ + private function sendNotificationDigitalPost( + string $recipient, + string $subject, + string $content, + string $taskUrl, + string $actionLabel, + WebformSubmissionInterface $submission, + string $notificationType + ): void { + if (!$this->moduleHandler->moduleExists('os2forms_digital_post')) { + throw new RuntimeException('Cannot send digital post. Module os2forms_digital_post not installed.'); + } + + try { + $document = new Document( + $content, + Document::MIME_TYPE_PDF, + $subject . '.pdf' + ); + + $senderLabel = $subject; + $messageLabel = $subject; + + $recipientLookupResult = $this->digitalPostHelper->lookupRecipient($recipient); + $actions = [ + (new Action()) + ->setActionCode(SF1601::ACTION_SELVBETJENING) + ->setEntryPoint((new EntryPoint()) + ->setUrl($taskUrl) + ) + ->setLabel($actionLabel), + ]; + + $message = $this->digitalPostHelper->getMeMoHelper()->buildMessage($recipientLookupResult, $senderLabel, + $messageLabel, $document, $actions); + $forsendelse = $this->digitalPostHelper->getForsendelseHelper()->buildForsendelse($recipientLookupResult, + $messageLabel, $document); + $this->digitalPostHelper->sendDigitalPost( + SF1601::TYPE_AUTOMATISK_VALG, + $message, + $forsendelse, + $submission + ); + + $this->notice('Digital post notification sent (@type)', [ + '@type' => $notificationType, + 'webform_submission' => $submission, + 'handler_id' => 'os2forms_forloeb', + 'operation' => 'notification sent', + ]); + } + catch (\Exception $exception) { + $this->error('Error sending digital post notification (@type): @message', [ + '@type' => $notificationType, + '@message' => $exception->getMessage(), + 'webform_submission' => $submission, + 'handler_id' => 'os2forms_forloeb', + 'operation' => 'failed sending notification', + ]); + } + } + + /** + * Render notification. + * + * @param \Drupal\webform\WebformSubmissionInterface $submission + * The submission. + * @param string $handlerId + * The handler ID. + * @param string $notificationType + * The notification type. + * @param array $templateTask + * The Maestro template task. + * @param int $maestroQueueID + * The Maestro queue ID. + * @param string|null $contentType + * Optional content type. If not set the content type will be compoted based + * on the recipient. + * + * @return array + * The rendered notification with keys + * - content + * - contentType + * - recipient + * - subject + * - taskUrl (for digital post) + * - actionLabel (for digital post) + */ + public function renderNotification(WebformSubmissionInterface $submission, string $handlerId, string $notificationType, array $templateTask, int $maestroQueueID, string $contentType = NULL): array { + $handler = $submission->getWebform()->getHandler($handlerId); + $settings = $handler->getSettings(); + + $data = $submission->getData(); + $recipientElement = $settings[MaestroNotificationHandler::NOTIFICATION][MaestroNotificationHandler::RECIPIENT_ELEMENT] ?? NULL; + // Handle os2forms_person_lookup element. + $recipient = $data[$recipientElement]['cpr_number'] + // Simple element. + ?? $data[$recipientElement] + ?? NULL; + + if ($notificationType === self::OS2FORMS_FORLOEB_NOTIFICATION_ESCALATION) { + $recipient = $settings[MaestroNotificationHandler::NOTIFICATION][$notificationType][MaestroNotificationHandler::NOTIFICATION_RECIPIENT] ?? NULL; + } + + if (NULL !== $recipient) { + // Lifted from MaestroEngine. + $maestroTokenData = [ + 'maestro' => [ + 'task' => $templateTask, + 'queueID' => $maestroQueueID, + ], + ]; + + $notificationSetting = $settings[MaestroNotificationHandler::NOTIFICATION][$notificationType] ?? NULL; + if (NULL === $notificationSetting) { + throw new RuntimeException(sprintf('Cannot get setting for %s notification', $notificationType)); + } + + $processValue = static fn (string $value) => $value; + + // Handle a preview, i.e. not a real Maestro context. + if (empty($templateTask) || 0 === $maestroQueueID) { + $taskUrl = Url::fromRoute('os2forms_forloeb.meastro_notification.preview_message', ['message' => 'This is just a preview'])->toString(TRUE)->getGeneratedUrl(); + + $processValue = static function (string $value) use ($taskUrl) { + // Replace href="[maestro:task-url]" with href="«$taskUrl»". + $value = preg_replace('/href\s*=\s*["\']\[maestro:task-url\]["\']/', sprintf('href="%s"', htmlspecialchars($taskUrl)), $value); + $value = preg_replace('/\[(maestro:[^]]+)\]/', '[\1]', $value); + + return $value; + }; + } + else { + $taskUrl = TaskHandler::getHandlerURL($maestroQueueID); + } + + $subject = $this->tokenManager->replace( + $processValue($notificationSetting[MaestroNotificationHandler::NOTIFICATION_SUBJECT]), + $submission, + $maestroTokenData + ); + + $content = $notificationSetting[MaestroNotificationHandler::NOTIFICATION_CONTENT]; + if (isset($content['value'])) { + // Process tokens in content. + $content['value'] = $this->tokenManager->replace( + $processValue($content['value']), + $submission, + $maestroTokenData + ); + } + + $actionLabel = $this->tokenManager->replace($notificationSetting[MaestroNotificationHandler::NOTIFICATION_ACTION_LABEL], $submission); + + if (NULL === $contentType) { + $contentType = filter_var($recipient, FILTER_VALIDATE_EMAIL) ? 'email' : 'pdf'; + } + + switch ($contentType) { + case 'email': + $content = $this->renderHtml($contentType, $subject, $content, $taskUrl, $actionLabel, $submission); + break; + + case 'pdf': + $pdfContent = $this->renderHtml($contentType, $subject, $content, $taskUrl, $actionLabel, $submission); + + // Get dompdf plugin from entity_print module. + /** @var \Drupal\entity_print\Plugin\EntityPrint\PrintEngine\PdfEngineBase $printer */ + $printer = $this->entityPrintPluginManager->createInstance('dompdf'); + $printer->addPage($pdfContent); + $content = $printer->getBlob(); + break; + + default: + throw new RuntimeException(sprintf('Invalid content type: %s', $contentType)); + } + + return [ + 'content' => $content, + 'contentType' => $contentType, + 'recipient' => $recipient, + 'subject' => $subject, + 'taskUrl' => $taskUrl, + 'actionLabel' => $actionLabel, + ]; + } + + throw new RuntimeException(); + } + + /** + * Build HTML. + */ + private function renderHtml( + string $type, + string $subject, + array $content, + string $taskUrl, + string $actionLabel, + WebformSubmissionInterface $submission + ): string|MarkupInterface { + $template = $this->config->get('templates')['notification_' . $type] ?? NULL; + if (file_exists($template)) { + $template = file_get_contents($template) ?: NULL; + } + if (NULL === $template) { + $template = 'Missing or invalid template'; + } + + $build = [ + '#type' => 'inline_template', + '#template' => $template, + '#context' => [ + 'message' => [ + 'subject' => $subject, + 'content' => $content, + ], + 'task_url' => $taskUrl, + 'action_label' => $actionLabel, + 'webform_submission' => $submission, + 'handler' => $this, + ], + ]; + + return Markup::create(trim((string) $this->webformThemeManager->renderPlain($build))); + } + + /** + * Implements hook_mail(). + */ + public function mail(string $key, array &$message, array $params) { + switch ($key) { + case 'notification': + $message['subject'] = $params['subject']; + $message['body'][] = $params['body']; + if (isset($params['attachments'])) { + foreach ($params['attachments'] as $attachment) { + $message['params']['attachments'][] = $attachment; + } + } + break; + } + } + + /** + * Implements hook_mail_alter(). + */ + public function mailAlter(array &$message) { + if (str_starts_with($message['id'], 'os2forms_forloeb')) { + if (isset($message['params']['html']) && $message['params']['html']) { + $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed'; + } + } + } + + /** + * {@inheritdoc} + */ + public function log($level, $message, array $context = []): void { + $this->logger->log($level, $message, $context); + // @see https://www.drupal.org/node/3020595 + if (isset($context['webform_submission']) && $context['webform_submission'] instanceof WebformSubmissionInterface) { + $this->submissionLogger->log($level, $message, $context); + } + } + + /** + * Implements hook_maestro_can_user_execute_task_alter(). + */ + public function maestroCanUserExecuteTaskAlter(bool &$returnValue, int $queueID, int $userID): void { + // Perform our checks only if an anonymous user has been barred access. + if (0 === $userID && FALSE === $returnValue) { + $templateTask = MaestroEngine::getTemplateTaskByQueueID($queueID); + if (isset($templateTask['assigned'])) { + $assignments = explode(',', $templateTask['assigned']); + + // Check if one of the assignments match our known anonymous roles. + $knownAnonymousAssignments = array_map( + static fn(string $role) => 'role:fixed:' . $role, + array_filter($this->config->get('known_anonymous_roles') ?: []) + ); + + foreach ($assignments as $assignment) { + if (in_array($assignment, $knownAnonymousAssignments, TRUE)) { + $returnValue = TRUE; + } + } + } + } + } + +} diff --git a/modules/os2forms_forloeb/src/Plugin/AdvancedQueue/JobType/SendMeastroNotification.php b/modules/os2forms_forloeb/src/Plugin/AdvancedQueue/JobType/SendMeastroNotification.php new file mode 100644 index 00000000..ab4eb948 --- /dev/null +++ b/modules/os2forms_forloeb/src/Plugin/AdvancedQueue/JobType/SendMeastroNotification.php @@ -0,0 +1,55 @@ +get(MaestroHelper::class) + ); + } + + /** + * {@inheritdoc} + */ + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + readonly private MaestroHelper $helper + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public function process(Job $job): JobResult { + return $this->helper->processJob($job); + } + +} diff --git a/modules/os2forms_forloeb/src/Plugin/EngineTasks/MaestroWebformInheritTask.php b/modules/os2forms_forloeb/src/Plugin/EngineTasks/MaestroWebformInheritTask.php index b33bf4b3..c461163f 100644 --- a/modules/os2forms_forloeb/src/Plugin/EngineTasks/MaestroWebformInheritTask.php +++ b/modules/os2forms_forloeb/src/Plugin/EngineTasks/MaestroWebformInheritTask.php @@ -3,14 +3,10 @@ namespace Drupal\os2forms_forloeb\Plugin\EngineTasks; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Url; use Drupal\maestro\Engine\MaestroEngine; -use Drupal\maestro\Form\MaestroExecuteInteractive; use Drupal\maestro_webform\Plugin\EngineTasks\MaestroWebformTask; use Drupal\webform\Entity\WebformSubmission; -use Drupal\webform\WebformSubmissionForm; -use Drupal\webform\WebformSubmissionInterface; -use Symfony\Component\HttpFoundation\RedirectResponse; +use Drupal\webform\Utility\WebformArrayHelper; /** * Maestro Webform Task Plugin for Multiple Submissions. @@ -21,6 +17,7 @@ * ) */ class MaestroWebformInheritTask extends MaestroWebformTask { + public const INHERIT_WEBFORM_UNIQUE_ID = 'inherit_webform_unique_id'; /** * Constructor. @@ -65,25 +62,66 @@ public function getPluginId() { * {@inheritDoc} */ public function getTaskEditForm(array $task, $templateMachineName) { + // Get all webform tasks in template excluding the current task. + $template = MaestroEngine::getTemplate($templateMachineName); + $webformTasks = array_filter( + $template->tasks, + static fn(array $t) => $t['id'] !== $task['id'] && self::isWebformTask($t) + ); // We call the parent, as we need to add a field to the inherited form. $form = parent::getTaskEditForm($task, $templateMachineName); - $form['inherit_webform_unique_id'] = [ - '#type' => 'textfield', + $form[self::INHERIT_WEBFORM_UNIQUE_ID] = [ + '#type' => 'select', + '#options' => ['submission' => $this->t('Start')] + + array_map( + static fn(array $task) => sprintf('%s (%s)', $task['label'], $task['id']), + $webformTasks + ), '#title' => $this->t('Inherit Webform from:'), '#description' => $this->t('Put the unique identifier of the webform you want to inherit from (start-task=submission'), - '#default_value' => $task['data']['inherit_webform_unique_id'] ?? '', + '#default_value' => $task['data'][self::INHERIT_WEBFORM_UNIQUE_ID] ?? '', '#required' => TRUE, - ]; - $form['inherit_webform_create_submission'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Create submission'), - '#description' => $this->t('Create submission'), - '#default_value' => $task['data']['inherit_webform_create_submission'] ?? FALSE, + '#empty_option' => $this->t('Select task'), ]; return $form; } + /** + * {@inheritdoc} + */ + public function getAssignmentsAndNotificationsForm(array $task, $templateMachineName) { + $form = parent::getAssignmentsAndNotificationsForm($task, $templateMachineName); + + // @todo Find task by unique_id = $task['data']['inherit_webform_unique_id'] and point to webform. + $anonymousNotificationMessage = $this->t('Add a Meastro notification handler to the webform for the task selected under %inherit_webform_from', [ + '%inherit_webform_from' => $this->t('Inherit Webform from:'), + ]); + + WebformArrayHelper::insertBefore( + $form['edit_task_notifications'], 'token_tree', + 'anonymous_notification_message', + [ + '#theme' => 'status_messages', + '#message_list' => [ + 'status' => [$anonymousNotificationMessage], + ], + ] + ); + + return $form; + } + + /** + * Deside if a task is a webform task. + */ + public static function isWebformTask(array $task): bool { + return in_array($task['tasktype'] ?? NULL, [ + 'MaestroWebform', + 'MaestroWebformInherit', + ], TRUE); + } + /** * {@inheritDoc} */ @@ -92,157 +130,51 @@ public function prepareTaskForSave(array &$form, FormStateInterface $form_state, // Inherit from parent. parent::prepareTaskForSave($form, $form_state, $task); // Add custom field(s) to the inherited prepareTaskForSave method. - $task['data']['inherit_webform_unique_id'] = $form_state->getValue('inherit_webform_unique_id'); - $task['data']['inherit_webform_create_submission'] = $form_state->getValue('inherit_webform_create_submission'); + $task['data'][self::INHERIT_WEBFORM_UNIQUE_ID] = $form_state->getValue(self::INHERIT_WEBFORM_UNIQUE_ID); } /** - * {@inheritDoc} + * Implements hook_webform_submission_form_alter(). */ - public function getExecutableForm($modal, MaestroExecuteInteractive $parent) { - - // First, get hold of the interesting previous tasks. - $templateMachineName = MaestroEngine::getTemplateIdFromProcessId($this->processID); - $taskMachineName = MaestroEngine::getTaskIdFromQueueId($this->queueID); - $task = MaestroEngine::getTemplateTaskByID($templateMachineName, $taskMachineName); - - // Get user input from 'inherit_webform_unique_id'. - $webformInheritID = $task['data']['inherit_webform_unique_id']; - - // Load its corresponding webform submission. - $sid = MaestroEngine::getEntityIdentiferByUniqueID($this->processID, $webformInheritID); - if ($sid) { - $webform_submission = WebformSubmission::load($sid); - } - if (!isset($webform_submission)) { - \Drupal::logger('os2forms_forloeb')->error( - "Predecessors must have submissions with webforms attached." - ); - return FALSE; - } - // Copy the fields of the webform submission to the values array. - foreach ($webform_submission->getData() as $key => $value) { - if ($value) { - $field_values[$key] = $value; - } - } - // Now create webform submission, submit and attach to current process. - $templateTask = MaestroEngine::getTemplateTaskByQueueID($this->queueID); - $webformMachineName = $templateTask['data']['webform_machine_name']; - - $values = []; - $values['webform_id'] = $webformMachineName; - $values['data'] = $field_values; - - $createSubmission = (bool) ($task['data']['inherit_webform_create_submission'] ?? FALSE); - if ($createSubmission) { - // Create submission. - $new_submission = WebformSubmission::create($values); - - // Submit the webform submission. - $submission = WebformSubmissionForm::submitWebformSubmission($new_submission); - - // WebformSubmissionForm::submitWebformSubmission returns an array - // if the submission is not valid. - if (is_array($submission)) { - \Drupal::logger('os2forms_forloeb')->error( - "Can't create new submission: " . json_encode($submission) - ); - \Drupal::messenger()->addError('Webform data is invalid and could not be submitted.'); - return FALSE; - } - - $taskUniqueSubmissionId = $templateTask['data']['unique_id']; - - // Attach it to the Maestro process. - $sid = $new_submission->id(); - MaestroEngine::createEntityIdentifier( - $this->processID, $new_submission->getEntityTypeId(), - $new_submission->bundle(), $taskUniqueSubmissionId, $sid - ); - - // Important: Apparently the form must be generated after calling - // MaestroEngine::createEntityIdentifier for this to work. - $form = parent::getExecutableForm($modal, $parent); - // Catch os2forms-forloeb access token and pass it further. - if ($form instanceof RedirectResponse && $token = \Drupal::request()->query->get('os2forms-forloeb-ws-token')) { - // Check token to previous submission and update it to new one. - if ($token === $webform_submission->getToken()) { - $token = $new_submission->getToken(); - $url = Url::fromUserInput($form->getTargetUrl(), ['query' => ['os2forms-forloeb-ws-token' => $token]]); - $form = new RedirectResponse($url->toString()); + public static function webformSubmissionFormAlter(array &$form, FormStateInterface $formState, string $formId) { + // @todo Clean up and align with MaestroHelper::maestroZeroUserNotification(). + if ($queueID = self::getQueueIdFromRequest()) { + $templateTask = MaestroEngine::getTemplateTaskByQueueID($queueID); + if (self::isWebformTask($templateTask)) { + if ($inheritWebformUniqueId = ($templateTask['data'][self::INHERIT_WEBFORM_UNIQUE_ID] ?? NULL)) { + $processID = MaestroEngine::getProcessIdFromQueueId($queueID); + $entityIdentifier = MaestroEngine::getAllEntityIdentifiersForProcess($processID)[$inheritWebformUniqueId] ?? NULL; + if ('webform_submission' === ($entityIdentifier['entity_type'] ?? NULL)) { + $submission = WebformSubmission::load($entityIdentifier['entity_id']); + $data = $submission->getData(); + foreach ($data as $key => $value) { + if (isset($form['elements'][$key])) { + $form['elements'][$key]['#default_value'] = $value; + } + } + } } } } - else { - // Store values in session. - $values['processID'] = $this->processID; - $values['queueID'] = $this->queueID; - $values['webformInheritID'] = $webformInheritID; - - self::setTaskValues($this->queueID, $values); - - $form = parent::getExecutableForm($modal, $parent); - } - - return $form; } /** - * Implements hook_ENTITY_TYPE_prepare_form(). + * Get Maestro queue ID from request. */ - public static function webformSubmissionPrepareForm(WebformSubmissionInterface $webformSubmission, string $operation, FormStateInterface $formState): void { + public static function getQueueIdFromRequest(): ?int { + $queueID = NULL; $request = \Drupal::request(); - $isMaestro = (bool) $request->query->get('maestro', 0); - $queueID = (int) $request->query->get('queueid', 0); - if ($isMaestro && $queueID > 0) { - $values = self::getTaskValues($queueID); - if (isset($values['data'])) { - foreach ($values['data'] as $name => $value) { - $webformSubmission->setElementData($name, $value); - } + if ($sitewideToken = \Drupal::service('config.factory')->get('maestro.settings')->get('maestro_sitewide_token')) { + $token = $request->query->get($sitewideToken); + if (is_string($token)) { + $queueID = MaestroEngine::getQueueIdFromToken($token); } } - } - - /** - * Get task values from session. - * - * @param int $queueID - * The queue ID. - * - * @return array - * The task values if any. - */ - private static function getTaskValues($queueID) { - $sessionKey = self::formatTaskValuesSessionKey($queueID); - return \Drupal::request()->getSession()->get($sessionKey); - } - - /** - * Set task values in session. - * - * @param int $queueID - * The queue ID. - * @param array $values - * The values. - */ - private static function setTaskValues($queueID, array $values) { - $sessionKey = self::formatTaskValuesSessionKey($queueID); - \Drupal::request()->getSession()->set($sessionKey, $values); - } + if (empty($queueID)) { + $queueID = $request->query->get('queueid'); + } - /** - * Format task values session key. - * - * @param int $queueID - * The queue ID. - * - * @return string - * The formatted session key. - */ - private static function formatTaskValuesSessionKey($queueID) { - return sprintf('os2forms_forloeb_inherited_values_%s', $queueID); + return (int) $queueID ?: NULL; } } diff --git a/modules/os2forms_forloeb/src/Plugin/WebformHandler/MaestroNotificationHandler.php b/modules/os2forms_forloeb/src/Plugin/WebformHandler/MaestroNotificationHandler.php new file mode 100644 index 00000000..90262dbb --- /dev/null +++ b/modules/os2forms_forloeb/src/Plugin/WebformHandler/MaestroNotificationHandler.php @@ -0,0 +1,290 @@ +loggerFactory = $container->get('logger.factory'); + $instance->configFactory = $container->get('config.factory'); + $instance->renderer = $container->get('renderer'); + $instance->entityTypeManager = $container->get('entity_type.manager'); + $instance->conditionsValidator = $container->get('webform_submission.conditions_validator'); + + $instance->setConfiguration($configuration); + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getSummary() { + return [ + 'info' => [ + '#prefix' => '
', + '#suffix' => '
', + '#markup' => $this->t('Sends notification (@enabled_notification_types) when triggered by Maestro. The notification will be sent to the person identified by the value of the %element element.', [ + '@enabled_notification_types' => implode(', ', $this->getEnabledNotifications()), + '%element' => $this->configuration[self::NOTIFICATION][self::RECIPIENT_ELEMENT] ?? NULL, + ]), + ], + 'preview' => [ + '#prefix' => '
', + '#suffix' => '
', + ] + + Link::createFromRoute( + $this->t('Preview notifications'), + 'os2forms_forloeb.meastro_notification.preview', [ + 'webform' => $this->getWebform()->id(), + 'handler' => $this->getHandlerId(), + 'content_type' => 'email', + ] + )->toRenderable(), + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $formState) { + $form[self::NOTIFICATION] = [ + '#type' => 'fieldset', + '#title' => $this->t('Notification'), + ]; + + $availableElements = $this->getRecipientElements(); + $form[self::NOTIFICATION][static::RECIPIENT_ELEMENT] = [ + '#type' => 'select', + '#title' => $this->t('Element that contains the recipient identifier (email, CPR or CVR) of the notification'), + '#required' => TRUE, + '#default_value' => $this->configuration[self::NOTIFICATION][self::RECIPIENT_ELEMENT] ?? NULL, + '#options' => $availableElements, + ]; + + $form[self::NOTIFICATION][self::SENDER_LABEL] = [ + '#type' => 'textfield', + '#title' => $this->t('Sender label'), + '#required' => TRUE, + '#default_value' => $this->configuration[self::NOTIFICATION][self::SENDER_LABEL] ?? NULL, + '#maxlength' => self::SENDER_LABEL_MAX_LENGTH, + ]; + + foreach ([ + MaestroHelper::OS2FORMS_FORLOEB_NOTIFICATION_ASSIGNMENT => $this->t('Assignment'), + MaestroHelper::OS2FORMS_FORLOEB_NOTIFICATION_REMINDER => $this->t('Reminder'), + MaestroHelper::OS2FORMS_FORLOEB_NOTIFICATION_ESCALATION => $this->t('Escalation'), + ] as $notificationType => $label) { + $states = static function (bool $required = TRUE) use ($notificationType): array { + $states = [ + 'visible' => [ + ':input[name="settings[notification][' . $notificationType . '][notification_enable]"]' => ['checked' => TRUE], + ], + ]; + + if ($required) { + $states['required'] = [ + ':input[name="settings[notification][' . $notificationType . '][notification_enable]"]' => ['checked' => TRUE], + ]; + } + + return $states; + }; + + $form[self::NOTIFICATION][$notificationType] = [ + '#type' => 'fieldset', + '#title' => $label, + ]; + + $form[self::NOTIFICATION][$notificationType][self::NOTIFICATION_ENABLE] = [ + '#type' => 'checkbox', + '#title' => $this->t('Enable @type notification', ['@type' => $label]), + '#default_value' => $this->configuration[self::NOTIFICATION][$notificationType][self::NOTIFICATION_ENABLE] ?? ($notificationType === MaestroHelper::OS2FORMS_FORLOEB_NOTIFICATION_ASSIGNMENT), + ]; + + if ($notificationType === MaestroHelper::OS2FORMS_FORLOEB_NOTIFICATION_ESCALATION) { + $form[self::NOTIFICATION][$notificationType][self::NOTIFICATION_RECIPIENT] = [ + '#type' => 'email', + '#title' => $this->t('@type recipient', ['@type' => $label]), + '#default_value' => $this->configuration[self::NOTIFICATION][$notificationType][self::NOTIFICATION_RECIPIENT] ?? NULL, + '#states' => $states(), + ]; + } + + $form[self::NOTIFICATION][$notificationType][self::NOTIFICATION_SUBJECT] = [ + '#type' => 'textfield', + '#title' => $this->t('Subject'), + '#default_value' => $this->configuration[self::NOTIFICATION][$notificationType][self::NOTIFICATION_SUBJECT] ?? NULL, + '#maxlength' => self::NOTIFICATION_SUBJECT_MAX_LENGTH, + '#states' => $states(), + ]; + + $content = $this->configuration[self::NOTIFICATION][$notificationType][self::NOTIFICATION_CONTENT] ?? NULL; + if (isset($content['value'])) { + $content = $content['value']; + } + $form[self::NOTIFICATION][$notificationType][self::NOTIFICATION_CONTENT] = [ + '#type' => 'text_format', + '#format' => 'restricted_html', + '#title' => $this->t('Message'), + '#default_value' => $content ?? self::TOKEN_MAESTRO_TASK_URL, + '#description' => $this->t('The actual notification content. Must contain the @token_maestro_task_url token which is the URL to the Maestro task.', + [ + '@token_maestro_task_url' => self::TOKEN_MAESTRO_TASK_URL, + ]), + '#states' => $states(), + ]; + + $form[self::NOTIFICATION][$notificationType][self::NOTIFICATION_ACTION_LABEL] = [ + '#type' => 'textfield', + '#title' => $this->t('Action label'), + '#default_value' => $this->configuration[self::NOTIFICATION][$notificationType][self::NOTIFICATION_ACTION_LABEL] ?? NULL, + '#description' => $this->t('Label of the action in digital post'), + '#states' => $states(required: FALSE), + ]; + } + + return $this->setSettingsParents($form); + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $formState) { + parent::validateConfigurationForm($form, $formState); + + foreach ([ + MaestroHelper::OS2FORMS_FORLOEB_NOTIFICATION_ASSIGNMENT, + MaestroHelper::OS2FORMS_FORLOEB_NOTIFICATION_REMINDER, + MaestroHelper::OS2FORMS_FORLOEB_NOTIFICATION_ESCALATION, + ] as $notificationType) { + $key = [self::NOTIFICATION, $notificationType, self::NOTIFICATION_ENABLE]; + $enabled = $formState->getValue($key); + if (!$enabled) { + break; + } + $key = [self::NOTIFICATION, $notificationType, self::NOTIFICATION_CONTENT]; + $content = $formState->getValue($key); + if (isset($content['value'])) { + $content = $content['value']; + } + if (!str_contains($content, self::TOKEN_MAESTRO_TASK_URL)) { + $formState->setErrorByName( + implode('][', [self::NOTIFICATION, self::NOTIFICATION_CONTENT]), + $this->t('The notification content must contain the @token_maestro_task_url token', [ + '@token_maestro_task_url' => self::TOKEN_MAESTRO_TASK_URL, + ]) + ); + } + } + } + + /** + * Get recipient elements. + */ + private function getRecipientElements(): array { + $elements = $this->getWebform()->getElementsDecodedAndFlattened(); + + $elementTypes = [ + 'email', + 'textfield', + 'cpr_element', + 'cpr_value_element', + 'cvr_element', + 'cvr_value_element', + 'os2forms_person_lookup', + ]; + $elements = array_filter( + $elements, + static function (array $element) use ($elementTypes) { + return in_array($element['#type'], $elementTypes, TRUE); + } + ); + + return array_map(static function (array $element) { + return $element['#title']; + }, $elements); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $formState) { + parent::submitConfigurationForm($form, $formState); + + $this->configuration[self::NOTIFICATION] = $formState->getValue(self::NOTIFICATION); + } + + /** + * Get all notification types. + */ + public function getEnabledNotifications(): array { + $enabledNotificationTypes = []; + + foreach ([ + MaestroHelper::OS2FORMS_FORLOEB_NOTIFICATION_ASSIGNMENT, + MaestroHelper::OS2FORMS_FORLOEB_NOTIFICATION_REMINDER, + MaestroHelper::OS2FORMS_FORLOEB_NOTIFICATION_ESCALATION, + ] as $notificationType) { + if ($this->configuration[self::NOTIFICATION][$notificationType][self::NOTIFICATION_ENABLE] ?? FALSE) { + $enabledNotificationTypes[$notificationType] = $notificationType; + } + } + + return $enabledNotificationTypes; + } + + /** + * Check if a notification type is enabled. + */ + public function isNotificationEnabled(string $notificationType): bool { + return isset($this->getEnabledNotifications()[$notificationType]); + } + +} diff --git a/modules/os2forms_forloeb/templates/os2forms-forloeb-notification-message-email-html.html.twig b/modules/os2forms_forloeb/templates/os2forms-forloeb-notification-message-email-html.html.twig new file mode 100644 index 00000000..17e0f8ed --- /dev/null +++ b/modules/os2forms_forloeb/templates/os2forms-forloeb-notification-message-email-html.html.twig @@ -0,0 +1,35 @@ +{# +/** + * @file + * Template for Maestro notification email. + * + * Available variables: + * - message: The notification message + * - subject: the notification subject + * - contect: the notification content. Must be rendered as `processed_text`, i.e.: + * @code + * {{ { + * '#type': 'processed_text', + * '#text': message.content.value, + * '#format': message.content.format, + * } }} + * @endcode + * - notification_type: The type of notification () + * - task_url: URL of the task. + * - action_label: Optional label for the task action. + */ +#} +
+ {# @see https://api.drupal.org/api/drupal/core%21modules%21filter%21filter.module/function/check_markup/9 #} +
+ {{ { + '#type': 'processed_text', + '#text': message.content.value, + '#format': message.content.format, + } }} +
+ + {% if task_url|default(false) %} + {{ action_label|default('Go to your task'|t) }} + {% endif %} +
diff --git a/modules/os2forms_forloeb/templates/os2forms-forloeb-notification-message-pdf-html.html.twig b/modules/os2forms_forloeb/templates/os2forms-forloeb-notification-message-pdf-html.html.twig new file mode 100644 index 00000000..a3a671d9 --- /dev/null +++ b/modules/os2forms_forloeb/templates/os2forms-forloeb-notification-message-pdf-html.html.twig @@ -0,0 +1,63 @@ +{# +/** + * @file + * Template for Maestro notification PDF. + * + * Available variables: + * - message: The notification message + * - subject: the notification subject + * - contect: the notification content. Must be rendered as `processed_text`, i.e.: + * @code + * {{ { + * '#type': 'processed_text', + * '#text': message.content.value, + * '#format': message.content.format, + * } }} + * @endcode + * - notification_type: The type of notification () + * - task_url: URL of the task. + * - action_label: Optional label for the task action. + */ +#} + + + + + + {{ message.subject }} + + + +
+ Aarhus Kommune-logo +
+ +
+ {# @see https://api.drupal.org/api/drupal/core%21modules%21filter%21filter.module/function/check_markup/9 #} +
+ {{ { + '#type': 'processed_text', + '#text': message.content.value, + '#format': message.content.format, + } }} +
+
+ + diff --git a/modules/os2forms_forloeb/templates/os2forms-forloeb-notification-preview.html.twig b/modules/os2forms_forloeb/templates/os2forms-forloeb-notification-preview.html.twig new file mode 100644 index 00000000..18fa3470 --- /dev/null +++ b/modules/os2forms_forloeb/templates/os2forms-forloeb-notification-preview.html.twig @@ -0,0 +1,87 @@ +{# + /** + * @file + * Template for Maestro notification preview. + * + * Available variables: + * - preview_urls: The preview URLs + * - prev: Previous submission preview URL (if any) + * - self: The current preview URL (if any) + * - next: Next submission preview URL (if any) + * - webform: The webform + * - handler: The handler ID + * - notification_type: The notification type (assignment, reminder, escalation) + * - content_type: The content type (email, pdf) + * - submission: The submission ID + * - return_url: The return URL (to list of webform handlers) + * - render_url: The render URL to render the actual preview + */ + #} +
+ + + + +
+
{{ 'Subject'|t }}: {{ subject }}
+
{{ 'Recipient'|t }}: {{ recipient }}
+
+ + {% if render_url %} + + {% endif %} +