diff --git a/CHANGELOG.md b/CHANGELOG.md index deeae66..fec732d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,18 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +## [3.12.0] 2023-10-02 + +- Removing webform_embed +- os2forms_permissions_by_term: removing node access control + ## [3.11.0] 2023-09-25 - [OS-58] New company address fields - Custom permissions by term field - Removing dependency to config_entity_revisions, webform_revisions, coc_forms_auto_export +- [PR-56](https://github.com/OS2Forms/os2forms/pull/56) + Handled anonymous users in notifications and flow tasks ## [3.10.0] 2023-08-23 diff --git a/composer.json b/composer.json index cbb7125..5b9c8af 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "cweagans/composer-patches": "^1.6.5", "dompdf/dompdf": "^2.0", "drupal/admin_toolbar": "^3.0", + "drupal/advancedqueue": "^1.0", "drupal/chosen": "^2.10", "drupal/ckeditor_a11ychecker": "^2.0@alpha", "drupal/clientside_validation": "^3.0", @@ -39,7 +40,7 @@ "drupal/libraries": "^3.0@beta", "drupal/linkit": "^5.0", "drupal/logging_alerts": "^2.0", - "drupal/maestro": "^3.0", + "drupal/maestro": "^3.1", "drupal/mailsystem": "^4.1", "drupal/masquerade": "^2.0@RC", "drupal/pathauto": "^1.5", @@ -74,6 +75,9 @@ "webmozart/path-util": "^2.3", "zaporylie/composer-drupal-optimizations": "^1.2" }, + "suggest": { + "os2forms/os2forms_digital_post": "Send Maestro notifications via digital post (see https://github.com/itk-dev/os2forms_digital_post/blob/main/README.md)" + }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1", "drupal/coder": "^8.3", diff --git a/modules/os2forms_forloeb/CHANGELOG.md b/modules/os2forms_forloeb/CHANGELOG.md index f985467..0ab070a 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 8ebcf2c..e1e0ec3 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 3796741..96f211d 100644 --- a/modules/os2forms_forloeb/os2forms_forloeb.info.yml +++ b/modules/os2forms_forloeb/os2forms_forloeb.info.yml @@ -2,11 +2,13 @@ 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' + - 'drupal:advancedqueue' - 'drupal:diff' - 'drupal:entity_print' - 'drupal:eu_cookie_compliance' @@ -18,35 +20,37 @@ dependencies: - 'drupal:mailsystem' - 'drupal:masquerade' - 'drupal:os2forms' - - 'drupal:os2forms_nemid' - 'drupal:os2forms_dawa' + - 'drupal:os2forms_nemid' - 'drupal:os2forms_sbsys' - 'drupal:os2web_simplesaml' - 'drupal:pathauto' - 'drupal:r4032login' - 'drupal:redirect' - 'drupal:smtp' - - 'drupal:system' - 'drupal:switch_page_theme' + - 'drupal:system' - 'drupal:token' - 'drupal:ultimate_cron' - 'drupal:webform' - - 'drupal:webform_ui' - 'drupal:webform_access' - 'drupal:webform_attachment' - 'drupal:webform_composite' - - 'drupal:webform_embed' - 'drupal:webform_entity_print' - 'drupal:webform_entity_print_attachment' - 'drupal:webform_migrate' - 'drupal:webform_node_element' - 'drupal:webform_remote_handlers' - 'drupal:webform_rest' - - 'drupal:webform_share' - 'drupal:webform_scheduled_email' + - 'drupal:webform_share' - 'drupal:webform_submission_export_import' - 'drupal:webform_submission_log' - 'drupal:webform_templates' + - 'drupal:webform_ui' - 'drupal:workflow_participants' + # os2forms_digital_post may be used for sending Meastro notifications via digital post + # - '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 0000000..a561d49 --- /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 4c5fac0..1878073 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 8304af4..a51f54e 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 eb0fa89..132f3f3 100644 --- a/modules/os2forms_forloeb/os2forms_forloeb.services.yml +++ b/modules/os2forms_forloeb/os2forms_forloeb.services.yml @@ -2,6 +2,20 @@ 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' diff --git a/modules/os2forms_forloeb/src/Controller/ForloebTaskConsoleController.php b/modules/os2forms_forloeb/src/Controller/ForloebTaskConsoleController.php deleted file mode 100644 index 8f2e95b..0000000 --- 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 0000000..989607a --- /dev/null +++ b/modules/os2forms_forloeb/src/Controller/MaestroNotificationController.php @@ -0,0 +1,169 @@ +get('entity_type.manager')->getStorage('webform_submission'), + $container->get(MaestroHelper::class) + ); + } + + /** + * Preview notification action. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * @param \Drupal\webform\WebformInterface $webform + * The webform. + * @param string $handler + * The handler ID. + * @param string $notification_type + * The notification type. + * @param string $content_type + * The content type. + * + * @return array + * A render array. + */ + public function preview(Request $request, WebformInterface $webform, string $handler, string $notification_type, string $content_type): array { + $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. + * + * @param string $handler + * The handler ID. + * @param string $notification_type + * The notification type. + * @param string $content_type + * The content type. + * @param \Drupal\webform\WebformSubmissionInterface $submission + * The webform submission. + * + * @return \Symfony\Component\HttpFoundation\Response + * The response. + */ + public function previewRender(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. + * + * Used only to show a message when a Maestro task link from a notification + * preview is clicked. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * + * @return \Symfony\Component\HttpFoundation\Response + * The response. + */ + public function message(Request $request): Response { + $content[] = '
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 0000000..82c3912
--- /dev/null
+++ b/modules/os2forms_forloeb/src/MaestroHelper.php
@@ -0,0 +1,694 @@
+config = $configFactory->get(SettingsForm::SETTINGS);
+ $this->webformSubmissionStorage = $entityTypeManager->getStorage('webform_submission');
+ $this->queueStorage = $entityTypeManager->getStorage('advancedqueue_queue');
+
+ // The OS2Forms Digital Post (os2forms_digital_post) module may not be
+ // installed.
+ $this->digitalPostHelper = \Drupal::hasService(DigitalPostHelper::class)
+ ? \Drupal::service(DigitalPostHelper::class)
+ : NULL;
+ }
+
+ /**
+ * Implements hook_maestro_zero_user_notification().
+ */
+ public function maestroZeroUserNotification($templateMachineName, $taskMachineName, $queueID, $notificationType) {
+ $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.
+ *
+ * Creates job for sending notification to assigned user.
+ *
+ * @param string $notificationType
+ * The notification type (one of the NOTIFICATION_* constannts).
+ * @param \Drupal\webform\WebformSubmissionInterface $submission
+ * The webform submission.
+ * @param array $templateTask
+ * The template task.
+ * @param int $maestroQueueID
+ * The Maestro queue ID.
+ *
+ * @see self::processJob()
+ */
+ 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 to send out a notification.
+ *
+ * @param \Drupal\advancedqueue\Job $job
+ * The job (created by handleSubmissionNotification)
+ *
+ * @see self::handleSubmissionNotification()
+ */
+ 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.
+ *
+ * @param string $notificationType
+ * The notification type (one of the NOTIFICATION_* constannts).
+ * @param \Drupal\webform\WebformSubmissionInterface $submission
+ * The webform submission.
+ * @param array $templateTask
+ * The template task.
+ * @param int $maestroQueueID
+ * The Maestro queue ID.
+ */
+ 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,
+ ]);
+ }
+ }
+
+ /**
+ * Load advanced queue used to process notification jobs.
+ *
+ * @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.
+ *
+ * @param string $recipient
+ * The recipient.
+ * @param string $subject
+ * The subject.
+ * @param string $body
+ * The body.
+ * @param \Drupal\webform\WebformSubmissionInterface $submission
+ * The webform submission.
+ * @param string $notificationType
+ * The notification type (one of the NOTIFICATION_* constannts).
+ */
+ 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.
+ *
+ * @param string $recipient
+ * The recipient.
+ * @param string $subject
+ * The subject.
+ * @param string $content
+ * The content.
+ * @param string $taskUrl
+ * The Maestro task URL.
+ * @param string $actionLabel
+ * The action label.
+ * @param \Drupal\webform\WebformSubmissionInterface $submission
+ * The webform submission.
+ * @param string $notificationType
+ * The notification type (one of the NOTIFICATION_* constannts).
+ */
+ private function sendNotificationDigitalPost(
+ string $recipient,
+ string $subject,
+ string $content,
+ string $taskUrl,
+ string $actionLabel,
+ WebformSubmissionInterface $submission,
+ string $notificationType
+ ): void {
+ if (NULL === $this->digitalPostHelper) {
+ throw new RuntimeException('Cannot send digital post. Module OS2Forms Digital Post (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 (one of the NOTIFICATION_* constannts).
+ * @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)
+ *
+ * @see self::renderHtml()
+ */
+ 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::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();
+ }
+
+ /**
+ * Render HTML for a notification.
+ *
+ * @param string $type
+ * The notification content type ('email' or 'pdf').
+ * @param string $subject
+ * The subject.
+ * @param array $content
+ * The content.
+ * @param string $taskUrl
+ * The Maestro taks URL.
+ * @param string $actionLabel
+ * The action label.
+ * @param \Drupal\webform\WebformSubmissionInterface $submission
+ * The webform submission.
+ *
+ * @return string|MarkupInterface
+ * The rendered content.
+ */
+ 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 0000000..8968e03
--- /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,
+ private readonly 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 b33bf4b..b04137c 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,71 @@ 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)
+ );
+ // Index by tasks' unique id.
+ $webformTasksByUniqueId = [];
+ foreach ($webformTasks as $id => $webformTask) {
+ $webformTasksByUniqueId[$webformTask['data']['unique_id'] ?? $id] = $webformTask;
+ }
// 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['data']['unique_id'] ?? $task['id']),
+ $webformTasksByUniqueId
+ ),
'#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,
+ '#empty_option' => $this->t('Select task'),
];
- $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,
- ];
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 +135,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 0000000..d0afc20
--- /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' => '@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::NOTIFICATION_ASSIGNMENT,
+ MaestroHelper::NOTIFICATION_REMINDER,
+ MaestroHelper::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::NOTIFICATION_ASSIGNMENT,
+ MaestroHelper::NOTIFICATION_REMINDER,
+ MaestroHelper::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 0000000..17e0f8e
--- /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.
+ */
+#}
+