diff --git a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-05-22.sql b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-05-22.sql new file mode 100644 index 0000000000000..d9465dbcad1c3 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-05-22.sql @@ -0,0 +1,2 @@ +INSERT IGNORE INTO `#__mail_templates` (`template_id`, `extension`, `language`, `subject`, `body`, `htmlbody`, `attachments`, `params`) VALUES +('plg_workflow_notification.mail', 'plg_workflow_notification', '', 'PLG_WORKFLOW_NOTIFICATION_EMAIL_ON_TRANSITION_SUBJECT', 'PLG_WORKFLOW_NOTIFICATION_EMAIL_ON_TRANSITION_MSG', '', '', '{"tags":["siteurl","title","user","transitionname","tostage","extratext"]}'); diff --git a/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-05-22.sql b/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-05-22.sql new file mode 100644 index 0000000000000..67d73cb2b15e0 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-05-22.sql @@ -0,0 +1,3 @@ +INSERT INTO "#__mail_templates" ("template_id", "extension", "language", "subject", "body", "htmlbody", "attachments", "params") VALUES +('plg_workflow_notification.mail', 'plg_workflow_notification', '', 'PLG_WORKFLOW_NOTIFICATION_EMAIL_ON_TRANSITION_SUBJECT', 'PLG_WORKFLOW_NOTIFICATION_EMAIL_ON_TRANSITION_MSG', '', '', '{"tags":["siteurl","title","user","transitionname","tostage","extratext"]}') +ON CONFLICT DO NOTHING; diff --git a/administrator/language/en-GB/plg_workflow_notification.ini b/administrator/language/en-GB/plg_workflow_notification.ini index f6ee4e71faeb7..2d89b085c83c3 100644 --- a/administrator/language/en-GB/plg_workflow_notification.ini +++ b/administrator/language/en-GB/plg_workflow_notification.ini @@ -9,6 +9,13 @@ PLG_WORKFLOW_NOTIFICATION="Workflow - Notification" PLG_WORKFLOW_NOTIFICATION_ADDTEXT="The stage has changed" PLG_WORKFLOW_NOTIFICATION_ADDTEXT_DESC="This text will be sent: Title [title], changed by [user], new state: [state]. You can add your own text to this message and you can localise the text by using a language key and creating a language override." PLG_WORKFLOW_NOTIFICATION_ADDTEXT_LABEL="Additional Message Text" +PLG_WORKFLOW_NOTIFICATION_EMAIL_ON_TRANSITION_MSG="Title: {TITLE}. Transition {TRANSITIONNAME} performed by {USER}. New state: {TOSTAGE}. {EXTRATEXT}" +PLG_WORKFLOW_NOTIFICATION_EMAIL_ON_TRANSITION_SUBJECT="The status of {TITLE} has been changed" +PLG_WORKFLOW_NOTIFICATION_MAIL_MAIL_DESC="Mail sent to the selected usergroup and/or users set for the transition" +PLG_WORKFLOW_NOTIFICATION_MAIL_MAIL_TITLE="Workflow: Transition Mail" +PLG_WORKFLOW_NOTIFICATION_NO_NOTIFICATION_TYPE_SELECTED="At least 1 notification type should be selected" +PLG_WORKFLOW_NOTIFICATION_NO_RECIPIENTS_SELECTED="No users or usergroups have been selected as recipients." +PLG_WORKFLOW_NOTIFICATION_NO_TITLE="Unknown title" PLG_WORKFLOW_NOTIFICATION_ON_TRANSITION_MSG="Title: %1$s. Transition \"%2$s\" performed by %3$s. New state: %4$s." PLG_WORKFLOW_NOTIFICATION_ON_TRANSITION_SUBJECT="The status of \"%s\" has been changed" PLG_WORKFLOW_NOTIFICATION_NO_RECEIVER="No notifications sent as there are no users to send this message to." @@ -16,6 +23,10 @@ PLG_WORKFLOW_NOTIFICATION_NO_TITLE="Unknown title" PLG_WORKFLOW_NOTIFICATION_RECEIVERS_LABEL="Users" PLG_WORKFLOW_NOTIFICATION_SENDMAIL_LABEL="Send Notification" PLG_WORKFLOW_NOTIFICATION_SENT="Notifications sent" +PLG_WORKFLOW_NOTIFICATION_TYPE_DESC="The private messages option will only notify users who have administrator access." +PLG_WORKFLOW_NOTIFICATION_TYPE_EMAIL="Email" +PLG_WORKFLOW_NOTIFICATION_TYPE_LABEL="Notification Type" +PLG_WORKFLOW_NOTIFICATION_TYPE_MESSAGES="Private Messages" PLG_WORKFLOW_NOTIFICATION_USERGROUP_DESC="The users in this usergroup get a notification if this transition has been performed." PLG_WORKFLOW_NOTIFICATION_USERGROUP_LABEL="Usergroups" PLG_WORKFLOW_NOTIFICATION_XML_DESCRIPTION="Send notification if a transition has been performed in a workflow." diff --git a/installation/sql/mysql/supports.sql b/installation/sql/mysql/supports.sql index 9632026c26087..3694900bdd80d 100644 --- a/installation/sql/mysql/supports.sql +++ b/installation/sql/mysql/supports.sql @@ -439,4 +439,5 @@ INSERT INTO `#__mail_templates` (`template_id`, `extension`, `language`, `subjec ('plg_system_tasknotification.fatal_recovery_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title"]}'), ('plg_system_tasknotification.orphan_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title"]}'), ('plg_system_tasknotification.success_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_BODY', '', '', '{"tags":["task_id", "task_title", "exec_data_time", "task_output"]}'), -('plg_multifactorauth_email.mail', 'plg_multifactorauth_email', '', 'PLG_MULTIFACTORAUTH_EMAIL_EMAIL_SUBJECT', 'PLG_MULTIFACTORAUTH_EMAIL_EMAIL_BODY', '', '', '{"tags":["code","sitename","siteurl","username","email","fullname"]}'); +('plg_multifactorauth_email.mail', 'plg_multifactorauth_email', '', 'PLG_MULTIFACTORAUTH_EMAIL_EMAIL_SUBJECT', 'PLG_MULTIFACTORAUTH_EMAIL_EMAIL_BODY', '', '', '{"tags":["code","sitename","siteurl","username","email","fullname"]}'), +('plg_workflow_notification.mail', 'plg_workflow_notification', '', 'PLG_WORKFLOW_NOTIFICATION_EMAIL_ON_TRANSITION_SUBJECT', 'PLG_WORKFLOW_NOTIFICATION_EMAIL_ON_TRANSITION_MSG', '', '', '{"tags":["siteurl","title","user","transitionname","tostage","extratext"]}'); diff --git a/installation/sql/postgresql/supports.sql b/installation/sql/postgresql/supports.sql index 9187e94e804f3..c38b8f595caac 100644 --- a/installation/sql/postgresql/supports.sql +++ b/installation/sql/postgresql/supports.sql @@ -450,4 +450,5 @@ INSERT INTO "#__mail_templates" ("template_id", "extension", "language", "subjec ('plg_system_tasknotification.fatal_recovery_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title"]}'), ('plg_system_tasknotification.orphan_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title"]}'), ('plg_system_tasknotification.success_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_BODY', '', '', '{"tags":["task_id", "task_title", "exec_data_time", "task_output"]}'), -('plg_multifactorauth_email.mail', 'plg_multifactorauth_email', '', 'PLG_MULTIFACTORAUTH_EMAIL_EMAIL_SUBJECT', 'PLG_MULTIFACTORAUTH_EMAIL_EMAIL_BODY', '', '', '{"tags":["code","sitename","siteurl","username","email","fullname"]}'); +('plg_multifactorauth_email.mail', 'plg_multifactorauth_email', '', 'PLG_MULTIFACTORAUTH_EMAIL_EMAIL_SUBJECT', 'PLG_MULTIFACTORAUTH_EMAIL_EMAIL_BODY', '', '', '{"tags":["code","sitename","siteurl","username","email","fullname"]}'), +('plg_workflow_notification.mail', 'plg_workflow_notification', '', 'PLG_WORKFLOW_NOTIFICATION_EMAIL_ON_TRANSITION_SUBJECT', 'PLG_WORKFLOW_NOTIFICATION_EMAIL_ON_TRANSITION_MSG', '', '', '{"tags":["siteurl","title","user","transitionname","tostage","extratext"]}'); diff --git a/plugins/workflow/notification/forms/action.xml b/plugins/workflow/notification/forms/action.xml index 9387471d98026..6fce82a3f6209 100644 --- a/plugins/workflow/notification/forms/action.xml +++ b/plugins/workflow/notification/forms/action.xml @@ -13,6 +13,21 @@ + + + + + + 'onContentPrepareForm', - 'onWorkflowAfterTransition' => 'onWorkflowAfterTransition', + 'onContentPrepareForm' => 'onContentPrepareForm', + 'onWorkflowAfterTransition' => 'onWorkflowAfterTransition', + 'onContentBeforeSave' => 'onContentBeforeSave', ]; } /** - * The form event. + * Check if a transition is being saved and all fields are set correctly. * - * @param Form $form The form - * @param stdClass $data The data + * @param Event $data The event data ($context, $table, $isNew, $data) * - * @return boolean + * @return boolean True if all necessary fields are filled. + * + * @since __DEPLOY_VERSION__ + * + */ + public function onContentBeforeSave(Event $data): bool + { + $context = $data->getArgument(0); + + if ($context !== 'com_workflow.transition') { + return true; + } + + $values = $data->getArgument(3); + + if ($values['options']['notification_send_mail'] === false) { + return true; + } + + if (empty($values['options']['notification_type'])) { + throw new InvalidArgumentException(Text::_('PLG_WORKFLOW_NOTIFICATION_NO_NOTIFICATION_TYPE_SELECTED')); + } + + if ( + !isset($values['options']['notification_receivers']) && !isset($values['options']['notification_groups']) + || (empty($values['options']['notification_receivers']) && empty($values['options']['notification_groups'])) + ) { + throw new InvalidArgumentException(Text::_('PLG_WORKFLOW_NOTIFICATION_NO_RECIPIENTS_SELECTED')); + } + + return true; + } + + /** + * The event contains two arguments: + * + * Form $form The form + * stdClass $data The data + * + * @param EventInterface $event The event data + * + * @return boolean * * @since 4.0.0 */ - public function onContentPrepareForm(EventInterface $event) + public function onContentPrepareForm(EventInterface $event): bool { $form = $event->getArgument('0'); $data = $event->getArgument('1'); @@ -104,15 +149,23 @@ public function onContentPrepareForm(EventInterface $event) /** * Send a Notification to defined users a transition is performed * - * @param string $context The context for the content passed to the plugin. - * @param array $pks A list of primary key ids of the content that has changed stage. - * @param object $data Object containing data about the transition + * The event contains several arguments: * - * @return boolean + * string $context The context for the content passed to the plugin. + * string $extensionName The extension name throwing the trigger event + * array $pks A list of primary key ids of the content that has changed stage. + * object $data Object containing data about the transition + * + * @param WorkflowTransitionEvent $event The event data + * + * @return void * * @since 4.0.0 + * @throws Exception + * @see sendMessages() + * @see sendEmail() */ - public function onWorkflowAfterTransition(WorkflowTransitionEvent $event) + public function onWorkflowAfterTransition(WorkflowTransitionEvent $event): void { $context = $event->getArgument('extension'); $extensionName = $event->getArgument('extensionName'); @@ -145,10 +198,11 @@ public function onWorkflowAfterTransition(WorkflowTransitionEvent $event) // Prepare Language for messages $defaultLanguage = ComponentHelper::getParams('com_languages')->get('administrator'); - $debug = $this->app->get('debug_lang'); + $debug = $this->app->get('debug_lang'); $modelName = $component->getModelName($context); - $model = $component->getMVCFactory()->createModel($modelName, $this->app->getName(), ['ignore_request' => true]); + $model = $component->getMVCFactory() + ->createModel($modelName, $this->app->getName(), ['ignore_request' => true]); // Don't send the notification to the active user $key = array_search($user->id, $userIds); @@ -157,11 +211,6 @@ public function onWorkflowAfterTransition(WorkflowTransitionEvent $event) unset($userIds[$key]); } - // Remove users with locked input box from the list of receivers - if (!empty($userIds)) { - $userIds = $this->removeLocked($userIds); - } - // If there are no receivers, stop here if (empty($userIds)) { $this->app->enqueueMessage(Text::_('PLG_WORKFLOW_NOTIFICATION_NO_RECEIVER'), 'error'); @@ -169,63 +218,65 @@ public function onWorkflowAfterTransition(WorkflowTransitionEvent $event) return; } - // Get the model for private messages - $model_message = $this->app->bootComponent('com_messages') - ->getMVCFactory()->createModel('Message', 'Administrator'); - // Get the title of the stage - $model_stage = $this->app->bootComponent('com_workflow') + $modelStage = $this->app->bootComponent('com_workflow') ->getMVCFactory()->createModel('Stage', 'Administrator'); - $toStage = $model_stage->getItem($transition->to_stage_id)->title; + $toStage = $modelStage->getItem($transition->to_stage_id)->title; // Get the name of the transition - $model_transition = $this->app->bootComponent('com_workflow') + $modelTransition = $this->app->bootComponent('com_workflow') ->getMVCFactory()->createModel('Transition', 'Administrator'); - $transitionName = $model_transition->getItem($transition->id)->title; + $transitionName = $modelTransition->getItem($transition->id)->title; $hasGetItem = method_exists($model, 'getItem'); - $container = Factory::getContainer(); + $container = Factory::getContainer(); + + // Load language for messaging + $lang = $container->get(LanguageFactoryInterface::class) + ->createLanguage($user->getParam('admin_language', $defaultLanguage), $debug); + $lang->load('plg_workflow_notification'); foreach ($pks as $pk) { // Get the title of the item which has changed, unknown as fallback $title = Text::_('PLG_WORKFLOW_NOTIFICATION_NO_TITLE'); if ($hasGetItem) { - $item = $model->getItem($pk); + $item = $model->getItem($pk); $title = !empty($item->title) ? $item->title : $title; } + $notificationTypes = $transition->options['notification_type'] ?? []; + + if (!is_array($notificationTypes)) { + $notificationTypes = (array) $notificationTypes; + } + // Send Email to receivers - foreach ($userIds as $user_id) { - $receiver = $container->get(UserFactoryInterface::class)->loadUserById($user_id); - - if ($receiver->authorise('core.manage', 'com_message')) { - // Load language for messaging - $lang = $container->get(LanguageFactoryInterface::class) - ->createLanguage($user->getParam('admin_language', $defaultLanguage), $debug); - $lang->load('plg_workflow_notification'); - $messageText = sprintf( - $lang->_('PLG_WORKFLOW_NOTIFICATION_ON_TRANSITION_MSG'), - $title, - $lang->_($transitionName), - $user->name, - $lang->_($toStage) - ); - - if (!empty($transition->options['notification_text'])) { - $messageText .= '
' . htmlspecialchars($lang->_($transition->options['notification_text'])); - } + foreach ($userIds as $userId) { + $recipient = $container->get(UserFactoryInterface::class)->loadUserById($userId); + $extraText = ''; - $message = [ - 'id' => 0, - 'user_id_to' => $receiver->id, - 'subject' => sprintf($lang->_('PLG_WORKFLOW_NOTIFICATION_ON_TRANSITION_SUBJECT'), $title), - 'message' => $messageText, - ]; + if (!empty($transition->options['notification_text'])) { + $extraText = htmlspecialchars($lang->_($transition->options['notification_text'])); + } - $model_message->save($message); + foreach ($notificationTypes as $type) { + try { + $functionName = 'send' . ucfirst($type); + $this->$functionName( + $recipient, + $user->name, + $title, + $lang->_($transitionName), + $lang->_($toStage), + $lang, + $extraText + ); + } catch (Exception $exception) { + $this->app->enqueueMessage($exception->getMessage(), 'error'); + } } } } @@ -233,16 +284,111 @@ public function onWorkflowAfterTransition(WorkflowTransitionEvent $event) $this->app->enqueueMessage(Text::_('PLG_WORKFLOW_NOTIFICATION_SENT'), 'message'); } + /** + * Send a message to com_messages when a stage changes transition. + * + * @param \Joomla\CMS\User\User $recipient The user receiving the message + * @param string $user The user making the transition + * @param string $title The title of the item transitioned + * @param string $transitionName The name of the transition executed + * @param string $toStage The stage moving to + * @param Language $language The language to use for translating the message + * @param string $extraText The additional text to add to the end of the message + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function sendMessages( + \Joomla\CMS\User\User $recipient, + string $user, + string $title, + string $transitionName, + string $toStage, + Language $language, + string $extraText + ): void { + if ($recipient->authorise('core.manage', 'com_message')) { + // Get the model for private messages + $modelMessage = $this->app->bootComponent('com_messages') + ->getMVCFactory()->createModel('Message', 'Administrator'); + + // Remove users with locked input box from the list of receivers + if ($this->isMessageBoxLocked($recipient->id)) { + return; + } + + $subject = sprintf($language->_('PLG_WORKFLOW_NOTIFICATION_ON_TRANSITION_SUBJECT'), $title); + $messageText = sprintf( + $language->_('PLG_WORKFLOW_NOTIFICATION_ON_TRANSITION_MSG'), + $title, + $transitionName, + $user, + $toStage + ); + $messageText .= '
' . $extraText; + + $message = [ + 'id' => 0, + 'user_id_to' => $recipient->id, + 'subject' => $subject, + 'message' => $messageText, + ]; + + $modelMessage->save($message); + } + } + + /** + * Send an email when a stage changes transition. + * + * @param \Joomla\CMS\User\User $recipient The user receiving the message + * @param string $user The user making the transition + * @param string $title The title of the item transitioned + * @param string $transitionName The name of the transition executed + * @param string $toStage The stage moving to + * @param Language $language The language to use for translating the message + * @param string $extraText The additional text to add to the end of the message + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \PHPMailer\PHPMailer\Exception + */ + private function sendEmail( + \Joomla\CMS\User\User $recipient, + string $user, + string $title, + string $transitionName, + string $toStage, + Language $language, + string $extraText + ): void { + $data = []; + $data['siteurl'] = Uri::base(); + $data['title'] = $title; + $data['user'] = $user; + $data['transitionName'] = $transitionName; + $data['toStage'] = $toStage; + $data['extraText'] = $extraText; + + $mailer = new MailTemplate('plg_workflow_notification.mail', $this->app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($recipient->email); + $mailer->send(); + } + /** * Get user_ids of receivers * - * @param object $data Object containing data about the transition + * @param stdClass $data Object containing data about the transition * * @return array $userIds The receivers * * @since 4.0.0 + * @throws Exception */ - private function getUsersFromGroup($data): array + private function getUsersFromGroup(stdClass $data): array { $users = []; @@ -282,13 +428,13 @@ private function getUsersFromGroup($data): array /** * Check if the current plugin should execute workflow related activities * - * @param string $context + * @param string $context The context to validate * * @return boolean * * @since 4.0.0 */ - protected function isSupported($context) + protected function isSupported(string $context): bool { if (!$this->checkAllowedAndForbiddenlist($context)) { return false; @@ -314,18 +460,18 @@ protected function isSupported($context) } /** - * Remove receivers who have locked their message inputbox + * Check if the message box is locked * - * @param array $userIds The userIds which must be checked + * @param int $userId The user ID which must be checked * - * @return array users with active message input box + * @return boolean Return status of message box is locked * * @since 4.0.0 */ - private function removeLocked(array $userIds): array + private function isMessageBoxLocked(int $userId): bool { - if (empty($userIds)) { - return []; + if (empty($userId)) { + return false; } // Check for locked inboxes would be better to have _cdf settings in the user_object or a filter in users model @@ -333,12 +479,10 @@ private function removeLocked(array $userIds): array $query->select($this->db->quoteName('user_id')) ->from($this->db->quoteName('#__messages_cfg')) - ->whereIn($this->db->quoteName('user_id'), $userIds) + ->where($this->db->quoteName('user_id') . ' = ' . $userId) ->where($this->db->quoteName('cfg_name') . ' = ' . $this->db->quote('locked')) ->where($this->db->quoteName('cfg_value') . ' = 1'); - $locked = $this->db->setQuery($query)->loadColumn(); - - return array_diff($userIds, $locked); + return (int) $this->db->setQuery($query)->loadResult() === $userId; } }