From bf7381b30afdc2347d162ed89357b8f2b336b0ab Mon Sep 17 00:00:00 2001
From: Taslan Graham <gtaslan1@gmail.com>
Date: Wed, 6 Nov 2024 15:08:01 -0500
Subject: [PATCH] pkp/pkp-lib#10571 WIP: Allow admins and managers to assign
 user groups to email templates within a mailable

---
 .../PKPEmailTemplateController.php            |  2 +-
 .../FieldEmailTemplateUserGroupSettings.php   | 33 +++++++++++++++++++
 .../forms/emailTemplate/EmailTemplateForm.php |  4 +++
 classes/emailTemplate/DAO.php                 | 29 ++++++++++++++++
 classes/emailTemplate/Repository.php          | 23 ++++++++++++-
 classes/emailTemplate/maps/Schema.php         | 19 +----------
 classes/mail/Repository.php                   | 16 +++++++++
 locale/en/api.po                              |  3 ++
 locale/en/common.po                           |  3 ++
 9 files changed, 112 insertions(+), 20 deletions(-)
 create mode 100644 classes/components/forms/FieldEmailTemplateUserGroupSettings.php

diff --git a/api/v1/emailTemplates/PKPEmailTemplateController.php b/api/v1/emailTemplates/PKPEmailTemplateController.php
index 726c8ca64c1..b0bfce34567 100644
--- a/api/v1/emailTemplates/PKPEmailTemplateController.php
+++ b/api/v1/emailTemplates/PKPEmailTemplateController.php
@@ -248,7 +248,7 @@ public function edit(Request $illuminateRequest): JsonResponse
             return response()->json($errors, Response::HTTP_BAD_REQUEST);
         }
 
-        Repo::emailTemplate()->edit($emailTemplate, $params);
+        Repo::emailTemplate()->edit($emailTemplate, $params, $requestContext->getId());
 
         $emailTemplate = Repo::emailTemplate()->getByKey(
             // context ID is null if edited for the first time
diff --git a/classes/components/forms/FieldEmailTemplateUserGroupSettings.php b/classes/components/forms/FieldEmailTemplateUserGroupSettings.php
new file mode 100644
index 00000000000..a2609dd632f
--- /dev/null
+++ b/classes/components/forms/FieldEmailTemplateUserGroupSettings.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file classes/components/form/FieldEmailTemplateUserGroupSettings.php
+ *
+ * Copyright (c) 2014-2024 Simon Fraser University
+ * Copyright (c) 2000-2024 John Willinsky
+ * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
+ *
+ * @class FieldEmailTemplateUserGroupSettings
+ *
+ * @ingroup classes_controllers_form
+ *
+ * @brief A component managing user groups assignable to an email template
+ */
+
+namespace PKP\components\forms;
+
+class FieldEmailTemplateUserGroupSettings extends Field
+{
+    /** @copydoc Field::$component */
+    public $component = 'field-email-template-user-group-settings';
+
+    /**
+     * @copydoc Field::getConfig()
+     */
+    public function getConfig()
+    {
+        $config = parent::getConfig();
+
+        return $config;
+    }
+}
diff --git a/classes/components/forms/emailTemplate/EmailTemplateForm.php b/classes/components/forms/emailTemplate/EmailTemplateForm.php
index 8a3c90f5b88..7e42bd2c211 100644
--- a/classes/components/forms/emailTemplate/EmailTemplateForm.php
+++ b/classes/components/forms/emailTemplate/EmailTemplateForm.php
@@ -15,6 +15,7 @@
 
 namespace PKP\components\forms\emailTemplate;
 
+use PKP\components\forms\FieldEmailTemplateUserGroupSettings;
 use PKP\components\forms\FieldPreparedContent;
 use PKP\components\forms\FieldText;
 use PKP\components\forms\FormComponent;
@@ -46,6 +47,9 @@ public function __construct(string $action, array $locales)
                 'isMultilingual' => true,
                 'toolbar' => 'bold italic superscript subscript | link | blockquote bullist numlist',
                 'plugins' => 'paste,link,lists',
+            ]))->addField(new FieldEmailTemplateUserGroupSettings('userGroupIds', [
+                'label' => __('workflow.userGroup.allowed'),
+                'type' => 'checkbox'
             ]));
     }
 }
diff --git a/classes/emailTemplate/DAO.php b/classes/emailTemplate/DAO.php
index ff388308a58..66e1587c990 100644
--- a/classes/emailTemplate/DAO.php
+++ b/classes/emailTemplate/DAO.php
@@ -448,4 +448,33 @@ protected function getUniqueKey(EmailTemplate $emailTemplate): string
 
         return $key;
     }
+
+
+    public function updateTemplateAccessGroups(EmailTemplate $emailTemplate, array $newUserGroupIds, $contextId)
+    {
+
+        // Delete old entries for user groups IDs not found in new list of user group IDs
+        DB::table('email_template_role_access')
+            ->where('email_key', $emailTemplate->getData('key'))
+            ->where('context_id', $contextId)
+            ->whereNotIn('user_group_id', $newUserGroupIds)
+            ->delete();
+
+        foreach ($newUserGroupIds as $id) {
+            DB::table('email_template_role_access')
+                ->updateOrInsert(
+                    [   // The where conditions (keys that should match)
+                        'email_key' => $emailTemplate->getData('key'),
+                        'user_group_id' => $id,
+                        'context_id' => $contextId
+                    ],
+                    [   // The data to insert or update (values to set)
+                        'email_key' => $emailTemplate->getData('key'),
+                        'user_group_id' => $id,
+                        'context_id' => $contextId,
+                    ]
+                );
+        }
+
+    }
 }
diff --git a/classes/emailTemplate/Repository.php b/classes/emailTemplate/Repository.php
index 1b8470fce32..41f1a2790c3 100644
--- a/classes/emailTemplate/Repository.php
+++ b/classes/emailTemplate/Repository.php
@@ -129,6 +129,20 @@ public function validate(?EmailTemplate $object, array $props, Context $context)
             });
         }
 
+        //  If groupIds were passed to limit email access, check that groups exists within the context
+        if (isset($props['userGroupIds'])) {
+            $validator->after(function () use ($validator, $props, $context) {
+                $existingGroupIds = Repo::userGroup()->getCollector()
+                    ->filterByContextIds([$context->getId()])
+                    ->filterByUserGroupIds($props['userGroupIds'])->getIds()->toArray();
+
+                if (!empty(array_diff($existingGroupIds, $props['userGroupIds']))) {
+                    $validator->errors()->add('userGroupIds', __('api.emailTemplates.404.userGroupIds'));
+                }
+            });
+
+        }
+
         // Check for input from disallowed locales
         ValidatorFactory::allowedLocales($validator, $this->schemaService->getMultilingualProps($this->dao->schema), $allowedLocales);
 
@@ -156,11 +170,14 @@ public function add(EmailTemplate $emailTemplate): string
     }
 
     /** @copydoc DAO::update() */
-    public function edit(EmailTemplate $emailTemplate, array $params)
+    public function edit(EmailTemplate $emailTemplate, array $params, $contextId)
     {
         $newEmailTemplate = clone $emailTemplate;
         $newEmailTemplate->setAllData(array_merge($newEmailTemplate->_data, $params));
 
+        $userGroupIds = $params['userGroupIds'];
+        unset($params['userGroupIds']);
+
         Hook::call('EmailTemplate::edit', [$newEmailTemplate, $emailTemplate, $params]);
 
         if ($newEmailTemplate->getId()) {
@@ -168,6 +185,10 @@ public function edit(EmailTemplate $emailTemplate, array $params)
         } else {
             $this->dao->insert($newEmailTemplate);
         }
+
+        if($userGroupIds) {
+            $this->dao->updateTemplateAccessGroups($emailTemplate, $userGroupIds, $contextId);
+        }
     }
 
     /** @copydoc DAO::delete() */
diff --git a/classes/emailTemplate/maps/Schema.php b/classes/emailTemplate/maps/Schema.php
index 52d1fe4571d..299dcb292c0 100644
--- a/classes/emailTemplate/maps/Schema.php
+++ b/classes/emailTemplate/maps/Schema.php
@@ -85,18 +85,11 @@ protected function mapByProperties(array $props, EmailTemplate $item, string $ma
 
         $mailableClass = $mailableClass ?? Repo::mailable()->getMailableByEmailTemplate($item);
 
-        if(!$mailableClass) {
-            error_log('TEMPLATE NAME ' . $item->getData('key'));
-            error_log('TEMPLATE ALTERNATE TO ' . $item->getData('alternateTo') ?? '');
-        }
-
-
         // some mailable are not found during some operations such as performing a search for templates. So ensure mailable exist before using
         if($mailableClass) {
             $isUserGroupsAssignable = Repo::mailable()->isGroupsAssignableToTemplates($mailableClass);
 
-            if ($isUserGroupsAssignable) {
-                $output['assignableUserGroups'] = [];
+            if (!$isUserGroupsAssignable) {
                 $output['assignedUserGroupIds'] = [];
             } else {
                 // get roles for mailable
@@ -105,16 +98,6 @@ protected function mapByProperties(array $props, EmailTemplate $item, string $ma
                 $userGroups = [];
                 $roleNames = Application::get()->getRoleNames();
 
-                foreach (Repo::userGroup()->getByRoleIds($roles, $this->context->getId())->all() as $group) {
-                    $roleId = $group->getRoleId();
-                    $userGroups[] = [
-                        'id' => $group->getId(),
-                        'name' => $group->getLocalizedName(),
-                        'roleId' => $roleId,
-                        'roleName' => $roleNames[$roleId]];
-                }
-
-                $output['assignableUserGroups'] = $userGroups;
                 //        Get the current user groups assigned to the template
                 $output['assignedUserGroupIds'] = Repo::emailTemplate()->getGroupsAssignedToTemplate($item->getData('key'), Application::get()->getRequest()->getContext()->getId());
             }
diff --git a/classes/mail/Repository.php b/classes/mail/Repository.php
index b50c37ca634..4f69043a989 100644
--- a/classes/mail/Repository.php
+++ b/classes/mail/Repository.php
@@ -102,6 +102,21 @@ public function summarizeMailable(string $class): array
         $dataDescriptions = $class::getDataDescriptions();
         ksort($dataDescriptions);
 
+        // get roles for mailable
+        $roles = $class::getFromRoleIds();
+        // Get the groups for each role
+        $userGroups = [];
+        $roleNames = Application::get()->getRoleNames();
+
+        foreach (Repo::userGroup()->getByRoleIds($roles, Application::get()->getRequest()->getContext()->getId())->all() as $group) {
+            $roleId = $group->getRoleId();
+            $userGroups[] = [
+                'id' => $group->getId(),
+                'name' => $group->getLocalizedName(),
+                'roleId' => $roleId,
+                'roleName' => $roleNames[$roleId]];
+        }
+
         return [
             '_href' => Application::get()->getRequest()->getDispatcher()->url(
                 Application::get()->getRequest(),
@@ -118,6 +133,7 @@ public function summarizeMailable(string $class): array
             'supportsTemplates' => $class::getSupportsTemplates(),
             'toRoleIds' => $class::getToRoleIds(),
             'canAssignUserGroupToTemplates' => $this->isGroupsAssignableToTemplates($class),
+            'assignableTemplateUserGroups' => $userGroups
         ];
     }
 
diff --git a/locale/en/api.po b/locale/en/api.po
index e53bce54eed..d9f9f51f62d 100644
--- a/locale/en/api.po
+++ b/locale/en/api.po
@@ -343,3 +343,6 @@ msgstr "The provided section does not exist."
 
 msgid "api.publications.403.noEnabledIdentifiers"
 msgstr "Publication identifiers form is unavailable as there are no enabled Identifiers."
+
+msgid "api.emailTemplates.404.userGroupIds"
+msgstr "One or more of the provided user groups does not exist."
diff --git a/locale/en/common.po b/locale/en/common.po
index e2f967f80eb..0c26a08a3c9 100644
--- a/locale/en/common.po
+++ b/locale/en/common.po
@@ -2343,3 +2343,6 @@ msgstr "Application running in sandbox mode."
 
 msgid "common.fromUntil"
 msgstr "{$from} &ndash; {$until}"
+
+msgid "workflow.userGroup.allowed"
+msgstr "Allowed User Groups"